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
BIP-321 Integration¶
NUT-26 payment requests can be included in BIP-321 Bitcoin URIs using the creq query parameter. This enables unified QR codes that support multiple payment methods.
The full Bech32m-encoded string (including the creqb1 prefix) is used as the parameter value.
[!NOTE] Implementations SHOULD use uppercase for optimal QR code compatibility. Decoders MUST accept both uppercase and lowercase input.
Unified QR Codes¶
By including both Lightning payment data and Cashu payment requests in a single BIP-321 URI, a single QR code can serve as an entry point for multiple payment methods. Wallets that support a given payment method can use it, while others can fall back to supported methods.
Examples¶
Cashu Only¶
A Bitcoin URI containing only a Cashu payment request:
bitcoin:?creq=CREQB1QYQQWER9D4HNZV3NQGQQSQQQQQQQQQQRAQPSQQGQQSQQZQG9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RQQRJRDANXVET9YPCXZ7TDV4H8GXHR3TQ
BOLT11 + Cashu¶
A unified QR code supporting both Lightning (BOLT11) and Cashu:
bitcoin:?lightning=lnbc210n1p56amv8sp5v5gvxh0swyje66pcxtqtqh3qmzxd74fkxhjmzgzw7nff9fuhcdgqpp566zkpvgxn832cg06ghlk48tqntffkp6nsemw8g836pjfw4tdhdmsdqgde6hgv3kxqyjw5qcqpjrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7rf05uqqg8gqqqqqqqlgqqqqrucqjq9qxpqysgqrdvjgsemgtxs3wa38xf8qs3awqf5ksw0d3mpm07t9yl7xkasyzgz8rw5qlas6r4ers68u7nmgvqsgar4t9lr47fwlaue302nrasdekgqnvfjmp&creq=CREQB1QYQQWER9D4HNZV3NQGQQSQQQQQQQQQQRAQPSQQGQQSQQZQG9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RQQRJRDANXVET9YPCXZ7TDV4H8GXHR3TQ
BOLT12 + Cashu¶
A unified QR code supporting both LightningOffers (BOLT12) and Cashu:
bitcoin:?lno=lno1pgzkcctzv4kpvggzu2th0tw73fx2ygyd7gyuul490zkhkmz75ncz6q9nkyp9m78932tq&creq=CREQB1QYQQWER9D4HNZV3NQGQQSQQQQQQQQQQRAQPSQQGQQSQQZQG9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RQQRJRDANXVET9YPCXZ7TDV4H8GXHR3TQ