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:
- If the parameter starts with
creqA(case-insensitive), parse as NUT-18 CBOR+base64 format - If the parameter is valid Bech32m with HRP
creqb, parse as NUT-26 format - 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):
- Parse the
nprofileornpubfrom the JSON target field using NIP-19 - Store the raw 32-byte X-only public key in target (sub-tag 0x02)
- Store any relay URLs from the nprofile as tag tuples with key
"r" - 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 asnpub - If
"r"tag tuples are present: encode asnprofileusing NIP-19 format
Tag Tuple Encoding (Sub-Tag 0x03)¶
Generic tag tuples are encoded as:
- Key length (1 byte)
- Key string (UTF-8)
- For each value:
- Value length (1 byte)
- 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