Menu
NIP-nip-19 Final

NIP-19: Bech32-Encoded Entities

Human-readable encoding for Nostr identifiers. npub, nsec, note, nprofile, nevent, and naddr formats.

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

NIP-19: Bech32-Encoded Entities

Status: Final

NIP-19 defines human-readable encodings for Nostr identifiers using bech32. This makes keys and references easier to share and reduces errors.

Basic Entities

Simple bech32 encoding of 32-byte values:

PrefixContainsExample
npubPublic keynpub1qqqsyq...
nsecPrivate keynsec1qqqsyq...
noteEvent IDnote1qqqsyq...

Encoding

import bech32

def encode_npub(pubkey_hex: str) -> str:
    """Encode hex pubkey as npub."""
    data = bytes.fromhex(pubkey_hex)
    converted = bech32.convertbits(data, 8, 5)
    return bech32.bech32_encode("npub", converted)

def encode_nsec(privkey_hex: str) -> str:
    """Encode hex private key as nsec."""
    data = bytes.fromhex(privkey_hex)
    converted = bech32.convertbits(data, 8, 5)
    return bech32.bech32_encode("nsec", converted)

def encode_note(event_id_hex: str) -> str:
    """Encode event ID as note."""
    data = bytes.fromhex(event_id_hex)
    converted = bech32.convertbits(data, 8, 5)
    return bech32.bech32_encode("note", converted)

Decoding

def decode_bech32(bech32_str: str) -> tuple[str, bytes]:
    """Decode any NIP-19 bech32 string."""
    hrp, data = bech32.bech32_decode(bech32_str)
    converted = bech32.convertbits(data, 5, 8, False)
    return hrp, bytes(converted)

# Usage
prefix, data = decode_bech32("npub1...")
print(f"Type: {prefix}, Hex: {data.hex()}")

JavaScript (nostr-tools)

import { nip19 } from 'nostr-tools';

// Encode
const npub = nip19.npubEncode(pubkeyHex);
const nsec = nip19.nsecEncode(secretKeyBytes);
const noteId = nip19.noteEncode(eventIdHex);

// Decode
const { type, data } = nip19.decode(npub);
console.log(type);  // 'npub'
console.log(data);  // hex string

Shareable Entities (TLV)

Extended entities use Type-Length-Value encoding to include additional metadata:

PrefixContainsPurpose
nprofilePubkey + relaysShare profile with relay hints
neventEvent ID + relays + authorShare event with context
naddrKind + pubkey + d-tag + relaysReference replaceable event

TLV Types

TypeValueContains
special0Main data (pubkey, event ID, etc.)
relay1Relay URL
author2Author pubkey (for nevent)
kind3Event kind (for nevent, naddr)

nprofile — Shareable Profile

def encode_nprofile(pubkey_hex: str, relays: list[str]) -> str:
    """Encode pubkey with relay hints."""
    data = bytearray()

    # Type 0: pubkey (special)
    data.append(0)  # type
    data.append(32)  # length
    data.extend(bytes.fromhex(pubkey_hex))

    # Type 1: relays
    for relay in relays:
        relay_bytes = relay.encode('utf-8')
        data.append(1)  # type
        data.append(len(relay_bytes))
        data.extend(relay_bytes)

    converted = bech32.convertbits(data, 8, 5)
    return bech32.bech32_encode("nprofile", converted)
import { nip19 } from 'nostr-tools';

const nprofile = nip19.nprofileEncode({
  pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
  relays: ['wss://relay.damus.io', 'wss://nos.lol']
});

// Decode
const { type, data } = nip19.decode(nprofile);
console.log(data.pubkey);  // hex pubkey
console.log(data.relays);  // ['wss://...', ...]

nevent — Shareable Event

def encode_nevent(event_id_hex: str, relays: list[str],
                  author_hex: str = None, kind: int = None) -> str:
    """Encode event with context."""
    data = bytearray()

    # Type 0: event ID
    data.append(0)
    data.append(32)
    data.extend(bytes.fromhex(event_id_hex))

    # Type 1: relays
    for relay in relays:
        relay_bytes = relay.encode('utf-8')
        data.append(1)
        data.append(len(relay_bytes))
        data.extend(relay_bytes)

    # Type 2: author (optional)
    if author_hex:
        data.append(2)
        data.append(32)
        data.extend(bytes.fromhex(author_hex))

    # Type 3: kind (optional)
    if kind is not None:
        data.append(3)
        data.append(4)
        data.extend(kind.to_bytes(4, 'big'))

    converted = bech32.convertbits(data, 8, 5)
    return bech32.bech32_encode("nevent", converted)
const nevent = nip19.neventEncode({
  id: 'event-id-hex',
  relays: ['wss://relay.damus.io'],
  author: 'author-pubkey-hex',
  kind: 1
});

naddr — Replaceable Event Address

For parameterized replaceable events (kinds 30000-39999):

def encode_naddr(kind: int, pubkey_hex: str, d_tag: str,
                 relays: list[str] = None) -> str:
    """Encode replaceable event address."""
    data = bytearray()

    # Type 0: d-tag identifier
    d_bytes = d_tag.encode('utf-8')
    data.append(0)
    data.append(len(d_bytes))
    data.extend(d_bytes)

    # Type 1: relays
    for relay in (relays or []):
        relay_bytes = relay.encode('utf-8')
        data.append(1)
        data.append(len(relay_bytes))
        data.extend(relay_bytes)

    # Type 2: author
    data.append(2)
    data.append(32)
    data.extend(bytes.fromhex(pubkey_hex))

    # Type 3: kind
    data.append(3)
    data.append(4)
    data.extend(kind.to_bytes(4, 'big'))

    converted = bech32.convertbits(data, 8, 5)
    return bech32.bech32_encode("naddr", converted)
const naddr = nip19.naddrEncode({
  kind: 30023,
  pubkey: 'author-pubkey-hex',
  identifier: 'my-article-slug',
  relays: ['wss://relay.damus.io']
});

Decoding Any NIP-19 Entity

import { nip19 } from 'nostr-tools';

function parseNostrEntity(input) {
  // Remove nostr: prefix if present
  const cleaned = input.replace(/^nostr:/, '');

  try {
    const decoded = nip19.decode(cleaned);

    switch (decoded.type) {
      case 'npub':
        return { type: 'pubkey', pubkey: decoded.data };

      case 'nsec':
        return { type: 'privkey', privkey: decoded.data };

      case 'note':
        return { type: 'event_id', id: decoded.data };

      case 'nprofile':
        return {
          type: 'profile',
          pubkey: decoded.data.pubkey,
          relays: decoded.data.relays
        };

      case 'nevent':
        return {
          type: 'event',
          id: decoded.data.id,
          relays: decoded.data.relays,
          author: decoded.data.author,
          kind: decoded.data.kind
        };

      case 'naddr':
        return {
          type: 'address',
          kind: decoded.data.kind,
          pubkey: decoded.data.pubkey,
          identifier: decoded.data.identifier,
          relays: decoded.data.relays
        };

      default:
        return null;
    }
  } catch {
    return null;
  }
}

URI Scheme (NIP-21)

The nostr: prefix creates clickable links:

nostr:npub1...
nostr:nprofile1...
nostr:nevent1...
nostr:naddr1...

Clients recognize these and handle appropriately.

Best Practices

Sharing Profiles

Always include relay hints:

// Good: Includes where to find the user
const shareable = nip19.nprofileEncode({
  pubkey: pk,
  relays: ['wss://relay.damus.io', 'wss://nos.lol']
});

// Less useful: No relay hints
const basic = nip19.npubEncode(pk);

Sharing Events

Include author for better routing:

const shareable = nip19.neventEncode({
  id: eventId,
  relays: ['wss://relay.damus.io'],
  author: authorPk  // Helps clients find it
});

Handling Unknown Prefixes

function isValidNostrEntity(input) {
  const validPrefixes = ['npub', 'nsec', 'note', 'nprofile', 'nevent', 'naddr'];
  const cleaned = input.replace(/^nostr:/, '');

  for (const prefix of validPrefixes) {
    if (cleaned.startsWith(prefix + '1')) {
      return true;
    }
  }
  return false;
}

Security Considerations

Never Share nsec

# DANGER: This exposes your private key!
nsec = encode_nsec(private_key_hex)
print(f"My key: {nsec}")  # DON'T DO THIS

# Safe to share
npub = encode_npub(public_key_hex)
print(f"Find me at: {npub}")  # OK

Validate Before Use

def safe_decode(bech32_str):
    try:
        hrp, data = bech32.bech32_decode(bech32_str)
        if hrp is None:
            raise ValueError("Invalid bech32")

        valid_hrps = ['npub', 'nsec', 'note', 'nprofile', 'nevent', 'naddr']
        if hrp not in valid_hrps:
            raise ValueError(f"Unknown prefix: {hrp}")

        return hrp, bech32.convertbits(data, 5, 8, False)
    except Exception as e:
        raise ValueError(f"Decode error: {e}")

Machine-Readable Summary

{
  "nip": 19,
  "title": "Bech32-Encoded Entities",
  "status": "final",
  "defines": [
    "npub-encoding",
    "nsec-encoding",
    "note-encoding",
    "nprofile-encoding",
    "nevent-encoding",
    "naddr-encoding"
  ],
  "prefixes": {
    "npub": "public-key",
    "nsec": "private-key",
    "note": "event-id",
    "nprofile": "profile-with-relays",
    "nevent": "event-with-context",
    "naddr": "replaceable-address"
  },
  "tlv_types": {
    "0": "special",
    "1": "relay",
    "2": "author",
    "3": "kind"
  },
  "related": [
    "/learn/nostr/identifiers",
    "/learn/nostr/keys",
    "/learn/nostr/specs/nip-01"
  ]
}