Skip to content

NUT-26: Payment Request Bech32m Encoding

optional depends on: NUT-18


This specification defines an alternative encoding format for Payment Requests using Bech32m encoding with TLV (Tag-Length-Value) serialization. This format provides better QR code compatibility and typically 30-60% size reduction compared to the CBOR+base64 encoding defined in NUT-18.

Encoded Request Format

Payment requests are serialized using TLV encoding, then encoded with Bech32m:

"creqb" + "1" + bech32m(TLV(PaymentRequest))

The human-readable part (HRP) is "creqb" and the version separator is "1". The data payload is TLV-encoded as described below, then encoded with Bech32m (not standard Bech32).

[!NOTE] Implementations SHOULD output uppercase Bech32m strings for optimal QR code compatibility. Uppercase alphanumeric characters use QR "alphanumeric mode" which is more space-efficient than "byte mode" required for mixed-case. Decoders MUST accept both uppercase and lowercase input.

When parsing a creq parameter, implementations SHOULD support both formats:

  1. If the parameter starts with creqA (case-insensitive), parse as NUT-18 CBOR+base64 format
  2. If the parameter is valid Bech32m with HRP creqb, parse as NUT-26 format
  3. Otherwise, return an error

TLV Structure

The payment request is encoded as a sequence of TLV fields. Each TLV entry consists of:

  • Type (1 byte): Field identifier
  • Length (2 bytes, big-endian): Length of value in bytes
  • Value (variable): Field data

Top-Level TLV Tags

Tag Field Type Description
0x01 id string Payment identifier (corresponds to i in JSON)
0x02 amount u64 Amount in base units (corresponds to a in JSON)
0x03 unit u8/string Currency unit (corresponds to u in JSON)
0x04 single_use u8 Single-use flag: 0=false, 1=true (corresponds to s in JSON)
0x05 mint string Mint URL (repeatable for multiple mints, corresponds to m in JSON)
0x06 description string Human-readable description (corresponds to d in JSON)
0x07 transport sub-TLV Transport configuration (repeatable, corresponds to t in JSON)
0x08 nut10 sub-TLV NUT-10 spending conditions (corresponds to nut10 in JSON)

All fields are optional. Unknown tags MUST be ignored to maintain forward compatibility.

Unit Encoding (Tag 0x03)

The unit field uses a compact encoding:

  • Value 0x00: Represents sat (Bitcoin satoshis)
  • String value: Any other unit is encoded as a UTF-8 string (e.g., "msat", "usd", "eur")

Transport Sub-TLV (Tag 0x07)

Transport configurations are encoded as nested TLV structures. Each transport has the following sub-tags:

Sub-Tag Field Type Description
0x01 kind u8 Transport type: 0=nostr, 1=http_post
0x02 target bytes Transport target (interpretation depends on kind)
0x03 tag_tuple sub-sub-TLV Generic tag tuple (repeatable)

Transport Type Mapping

The kind field (sub-tag 0x01) identifies the transport method. The following transport types are defined:

Kind Value Transport Type Description Target Format
0x00 nostr Nostr-based transport using NIP-04 DMs 32-byte X-only public key (raw bytes)
0x01 http_post HTTP POST to specified URL UTF-8 encoded URL string

[!NOTE] If no transport is specified (tag 0x07 is absent), the payment is assumed to be in-band, consistent with NUT-18 semantics.

JSON Representation:

In the NUT-18 JSON format, transports are represented with a type field:

{
  "t": [
    { "type": "nostr", "target": "npub1...", "tags": [["n", "17"]] },
    { "type": "post", "target": "https://callback.example.com/pay" }
  ]
}

When encoding to TLV, the type string is converted to the corresponding numeric kind value.

Transport Target Encoding (Sub-Tag 0x02)

The target field is interpreted based on the transport kind:

  • kind=0 (nostr): 32-byte X-only public key (raw bytes, not bech32-encoded)
  • kind=1 (http_post): UTF-8 encoded URL string

Nostr Transport Details

For Nostr transports (kind=0), the target field contains the raw 32-byte X-only public key (not bech32-encoded). NIPs and relay URLs are encoded using generic tag tuples (sub-tag 0x03), consistent with NUT-18's tags array.

Encoding (JSON to TLV):

  1. Parse the nprofile or npub from the JSON target field using NIP-19
  2. Store the raw 32-byte X-only public key in target (sub-tag 0x02)
  3. Store any relay URLs from the nprofile as tag tuples with key "r"
  4. Store NIPs from the tags array as tag tuples with key "n"

Decoding (TLV to JSON):

  • If no "r" tag tuples are present: encode public key as npub
  • If "r" tag tuples are present: encode as nprofile using NIP-19 format

Tag Tuple Encoding (Sub-Tag 0x03)

Generic tag tuples are encoded as:

  1. Key length (1 byte)
  2. Key string (UTF-8)
  3. For each value:
  4. Value length (1 byte)
  5. Value string (UTF-8)

This allows encoding arbitrary key-value pairs for extensibility.

NUT-10 Sub-TLV (Tag 0x08)

NUT-10 spending conditions are encoded as nested TLV structures:

Sub-Tag Field Type Description
0x01 kind u8 Secret kind (0=P2PK, 1=HTLC, etc.)
0x02 data bytes Kind-specific data (UTF-8 encoded)
0x03 tag_tuple sub-sub-TLV Tag tuple (repeatable, uses same encoding as transport tags)

NUT-10 Kind Enumeration

The following kind values are defined for NUT-10 spending conditions:

Kind Value Name Description
0x00 P2PK Pay to Public Key - requires signature from specified public key
0x01 HTLC Hash Time Locked Contract - requires preimage of hash

Additional kind values may be defined in future NUT specifications. Unknown kind values SHOULD be preserved when re-encoding but MAY be ignored during validation.

Example

This is an example payment request expressed as JSON:

{
  "i": "demo123",
  "a": 1000,
  "u": "sat",
  "s": true,
  "m": ["https://mint.example.com"],
  "d": "Coffee payment"
}

This payment request encodes to the NUT-26 format as:

CREQB1QYQQWER9D4HNZV3NQGQQSQQQQQQQQQQRAQPSQQGQQSQQZQG9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RQQRJRDANXVET9YPCXZ7TDV4H8GXHR3TQ