Menu
NIP-nip-21 Final

NIP-21: nostr: URI Scheme

URL scheme for linking to Nostr entities. Enable clickable links to profiles, events, and relays.

Type Nostr Implementation Possibility
Number nip-21
Status Final
Original https://github.com/nostr-protocol/nips/blob/master/21.md

NIP-21: nostr: URI Scheme

Status: Final

NIP-21 defines the nostr: URI scheme for creating clickable links to Nostr entities. When users click these links, their Nostr client opens the appropriate content.

URI Format

nostr:<NIP-19 entity>

Supported Entities

TypeExample
Profilenostr:npub1...
Profile with relaysnostr:nprofile1...
Eventnostr:note1...
Event with contextnostr:nevent1...
Replaceable eventnostr:naddr1...

Examples

nostr:npub1qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqpzry23pqe
nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p
nostr:note1qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqpzryzmag7z
nostr:nevent1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p
from nip19 import npub_encode, nprofile_encode, nevent_encode

def create_nostr_link(entity_type: str, data: dict) -> str:
    """Create a nostr: URI."""
    if entity_type == "profile":
        if data.get("relays"):
            encoded = nprofile_encode(data["pubkey"], data["relays"])
        else:
            encoded = npub_encode(data["pubkey"])

    elif entity_type == "event":
        if data.get("relays"):
            encoded = nevent_encode(
                data["id"],
                data.get("relays", []),
                data.get("author"),
                data.get("kind")
            )
        else:
            encoded = note_encode(data["id"])

    return f"nostr:{encoded}"
import { nip19 } from 'nostr-tools';

function createNostrLink(pubkey, relays = []) {
  if (relays.length > 0) {
    const nprofile = nip19.nprofileEncode({ pubkey, relays });
    return `nostr:${nprofile}`;
  }
  const npub = nip19.npubEncode(pubkey);
  return `nostr:${npub}`;
}
import re
from nip19 import decode

def parse_nostr_uri(uri: str) -> dict | None:
    """Parse a nostr: URI."""
    if not uri.startswith("nostr:"):
        return None

    entity = uri[6:]  # Remove "nostr:" prefix

    try:
        decoded = decode(entity)
        return decoded
    except Exception:
        return None

def extract_nostr_links(text: str) -> list:
    """Find all nostr: links in text."""
    pattern = r'nostr:(n(?:pub|sec|ote|profile|event|addr)1[a-z0-9]+)'
    matches = re.findall(pattern, text, re.IGNORECASE)

    links = []
    for match in matches:
        parsed = parse_nostr_uri(f"nostr:{match}")
        if parsed:
            links.append(parsed)

    return links
function extractNostrLinks(text) {
  const pattern = /nostr:(n(?:pub|sec|ote|profile|event|addr)1[a-z0-9]+)/gi;
  const matches = text.matchAll(pattern);

  return Array.from(matches).map(m => {
    try {
      return nip19.decode(m[1]);
    } catch {
      return null;
    }
  }).filter(Boolean);
}

In Content

When displaying content with nostr: links:

def render_nostr_links(content: str) -> str:
    """Replace nostr: URIs with clickable links."""
    pattern = r'nostr:(n(?:pub|profile)1[a-z0-9]+)'

    def replace(match):
        uri = f"nostr:{match.group(1)}"
        decoded = parse_nostr_uri(uri)

        if decoded and decoded["type"] in ["npub", "nprofile"]:
            # Link to profile
            short_id = decoded["pubkey"][:8]
            link = f'<a href="{uri}">@{short_id}...</a>'
            return link

        return match.group(0)

    return re.sub(pattern, replace, content, flags=re.IGNORECASE)

Handling Clicks

// In a web client
document.addEventListener('click', async (e) => {
  if (e.target.matches('a[href^="nostr:"]')) {
    e.preventDefault();
    const uri = e.target.getAttribute('href');
    await handleNostrUri(uri);
  }
});

async function handleNostrUri(uri) {
  const decoded = nip19.decode(uri.slice(6));

  switch (decoded.type) {
    case 'npub':
    case 'nprofile':
      openProfile(decoded.data.pubkey || decoded.data);
      break;
    case 'note':
    case 'nevent':
      openEvent(decoded.data.id || decoded.data);
      break;
    case 'naddr':
      openReplaceable(decoded.data);
      break;
  }
}

Use in Markdown

Nostr links work well in markdown content:

Check out [@alice](nostr:nprofile1...) latest post about Bitcoin!

I really liked [this thread](nostr:nevent1...).

Agent Usage

def share_my_profile() -> str:
    """Generate a shareable link to agent's profile."""
    return create_nostr_link("profile", {
        "pubkey": MY_PUBKEY,
        "relays": MY_PREFERRED_RELAYS
    })

def share_event(event_id: str) -> str:
    """Generate a shareable link to an event."""
    return create_nostr_link("event", {
        "id": event_id,
        "relays": RELAYS,
        "author": MY_PUBKEY
    })
async def handle_mention_with_link(event: dict):
    """Process an event that mentions a nostr: link."""
    links = extract_nostr_links(event["content"])

    for link in links:
        if link["type"] in ["npub", "nprofile"]:
            # Someone linked a profile
            pubkey = link.get("pubkey") or link.get("data")
            profile = await fetch_profile(pubkey)
            # Process profile...

        elif link["type"] in ["note", "nevent"]:
            # Someone linked an event
            event_id = link.get("id") or link.get("data")
            referenced = await fetch_event(event_id)
            # Process event...

Machine-Readable Summary

{
  "nip": 21,
  "title": "nostr: URI Scheme",
  "status": "final",
  "defines": [
    "uri-scheme",
    "link-format"
  ],
  "scheme": "nostr:",
  "supported_entities": [
    "npub",
    "nprofile",
    "note",
    "nevent",
    "naddr"
  ],
  "related": [
    "/learn/nostr/specs/nip-19",
    "/learn/nostr/identifiers"
  ]
}