BOLT-11: Invoice Protocol
Lightning invoice encoding specification. BOLT11 format, fields, and payment request generation.
| Type | Basis of Lightning Technology |
| Number | bolt-11 |
| Status | Final |
| Authors | Lightning Network Developers |
| Original | https://github.com/lightning/bolts/blob/master/11-payment-encoding.md |
| Requires |
BOLT-11: Invoice Protocol
BOLT-11 defines the standard format for Lightning payment requests (invoices). These bech32-encoded strings contain all information needed to make a payment.
Specification Summary
| Aspect | Value |
|---|---|
| Status | Final |
| Layer | Application |
| Purpose | Payment requests |
| Encoding | Bech32 |
Invoice Structure
lnbc10u1pj9nrfzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp
│ │ │ │ │
│ │ │ │ └─ Signature
│ │ │ └─ Data (bech32)
│ │ └─ Timestamp
│ └─ Amount: 10u = 10 microsatoshis
└─ Prefix: lnbc = Bitcoin mainnet
Prefix
Human-Readable Part
ln + [currency] + [amount]
Currency Codes
| Prefix | Network |
|---|---|
lnbc | Bitcoin mainnet |
lntb | Bitcoin testnet |
lnbcrt | Bitcoin regtest |
lntbs | Bitcoin signet |
Amount Encoding
| Suffix | Multiplier | Example |
|---|---|---|
| (none) | 1 BTC | lnbc1 = 1 BTC |
m | 0.001 | lnbc1m = 1 mBTC = 100,000 sats |
u | 0.000001 | lnbc1u = 1 μBTC = 100 sats |
n | 0.000000001 | lnbc1n = 1 nBTC = 0.1 sats |
p | 0.000000000001 | lnbc10p = 10 pBTC = 1 msat |
Note: p amounts must be divisible by 10 (minimum 10 pBTC = 1 msat).
Data Section
Timestamp
Unix timestamp (seconds since 1970) when invoice was created.
import time
timestamp = int(time.time()) # Current time
Tagged Fields
Each field has:
- Type (5 bits)
- Data length (10 bits, in 5-bit words)
- Data (variable)
Field Types
p - Payment Hash (type 1)
Required. 52 words (256 bits).
Tag: 1 (0b00001)
Data: SHA256(preimage)
s - Payment Secret (type 16)
Required for MPP. 52 words (256 bits).
Tag: 16 (0b10000)
Data: 32-byte random secret
d - Description (type 13)
Human-readable payment description.
Tag: 13 (0b01101)
Data: UTF-8 string
h - Description Hash (type 23)
SHA256 of long description (when too long for d).
Tag: 23 (0b10111)
Data: SHA256(description)
x - Expiry (type 6)
Seconds after timestamp until invoice expires. Default: 3600 (1 hour).
Tag: 6 (0b00110)
Data: Variable-length integer
c - Min CLTV Expiry (type 24)
Minimum CLTV delta for final hop. Default: 18 blocks.
Tag: 24 (0b11000)
Data: Variable-length integer
n - Payee Pubkey (type 19)
Destination node public key (33 bytes). Optional if recoverable from signature.
Tag: 19 (0b10011)
Data: 53 words (compressed pubkey)
f - Fallback Address (type 9)
On-chain fallback if Lightning payment fails.
Tag: 9 (0b01001)
Data: Version byte + address data
Version bytes:
- 0: P2WPKH (20 bytes)
- 17: P2PKH (20 bytes)
- 18: P2SH (20 bytes)
r - Route Hints (type 3)
Routing information for private channels.
Tag: 3 (0b00011)
Data: [
pubkey (33 bytes)
short_channel_id (8 bytes)
fee_base_msat (4 bytes)
fee_proportional_millionths (4 bytes)
cltv_expiry_delta (2 bytes)
] × n
9 - Feature Bits (type 5)
Required/supported features for payment.
Tag: 5 (0b00101)
Data: Feature bit vector
m - Metadata (type 27)
Arbitrary metadata for payment.
Tag: 27 (0b11011)
Data: Application-specific bytes
Signature
ECDSA signature over the invoice (excluding signature):
Data signed = hrp || data (before signature)
Signature = ECDSA(SHA256(SHA256(data_signed)), node_private_key)
Signature: 104 words (65 bytes: 64-byte signature + 1-byte recovery ID)
Parsing Implementation
import bech32
import hashlib
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class Invoice:
currency: str
amount_msat: Optional[int]
timestamp: int
payment_hash: bytes
payment_secret: Optional[bytes]
description: Optional[str]
description_hash: Optional[bytes]
expiry: int
min_cltv: int
payee: Optional[bytes]
route_hints: List[dict]
features: bytes
signature: bytes
def decode_invoice(invoice_str: str) -> Invoice:
"""Decode BOLT11 invoice."""
# Lowercase and decode bech32
invoice_str = invoice_str.lower()
hrp, data = bech32.bech32_decode(invoice_str)
if not hrp.startswith('ln'):
raise ValueError("Invalid Lightning invoice")
# Parse prefix
currency, amount = parse_hrp(hrp)
# Convert 5-bit to 8-bit
data_bytes = bech32.convertbits(data, 5, 8, False)
# Extract timestamp (first 7 bytes as 35 bits)
timestamp = extract_timestamp(data[:7])
# Parse tagged fields
fields = parse_tagged_fields(data[7:-104])
# Extract signature (last 104 words = 65 bytes)
signature = bytes(bech32.convertbits(data[-104:], 5, 8, False))
return Invoice(
currency=currency,
amount_msat=amount,
timestamp=timestamp,
payment_hash=fields.get('p'),
payment_secret=fields.get('s'),
description=fields.get('d'),
description_hash=fields.get('h'),
expiry=fields.get('x', 3600),
min_cltv=fields.get('c', 18),
payee=fields.get('n'),
route_hints=fields.get('r', []),
features=fields.get('9', b''),
signature=signature
)
def parse_hrp(hrp: str) -> tuple:
"""Parse human-readable part for currency and amount."""
# Remove 'ln' prefix
remaining = hrp[2:]
# Find currency (bc, tb, bcrt, tbs)
for currency in ['bcrt', 'tbs', 'bc', 'tb']:
if remaining.startswith(currency):
amount_str = remaining[len(currency):]
break
else:
raise ValueError(f"Unknown currency in {hrp}")
# Parse amount
if not amount_str:
amount = None
else:
amount = parse_amount(amount_str)
return currency, amount
def parse_amount(amount_str: str) -> int:
"""Parse amount string to millisatoshis."""
multipliers = {
'm': 100_000_000, # milli-bitcoin = 0.001 BTC
'u': 100_000, # micro-bitcoin
'n': 100, # nano-bitcoin
'p': 0.1, # pico-bitcoin (must be multiple of 10)
}
if amount_str[-1] in multipliers:
value = int(amount_str[:-1])
multiplier = multipliers[amount_str[-1]]
else:
value = int(amount_str)
multiplier = 100_000_000_000 # 1 BTC in msat
return int(value * multiplier)
Creating Invoices
def create_invoice(
payment_hash: bytes,
amount_msat: int,
description: str,
expiry: int = 3600,
private_key: bytes = None,
route_hints: list = None
) -> str:
"""Create BOLT11 invoice."""
# Build human-readable part
hrp = 'lnbc' + format_amount(amount_msat)
# Build data
data = []
# Timestamp (35 bits)
timestamp = int(time.time())
data.extend(encode_timestamp(timestamp))
# Payment hash (required)
data.extend(encode_tagged_field('p', payment_hash))
# Description
if len(description) <= 639: # Max description length
data.extend(encode_tagged_field('d', description.encode()))
else:
desc_hash = hashlib.sha256(description.encode()).digest()
data.extend(encode_tagged_field('h', desc_hash))
# Expiry
if expiry != 3600:
data.extend(encode_tagged_field('x', encode_varint(expiry)))
# Payment secret (required for MPP)
payment_secret = os.urandom(32)
data.extend(encode_tagged_field('s', payment_secret))
# Route hints
if route_hints:
for hint in route_hints:
data.extend(encode_tagged_field('r', encode_route_hint(hint)))
# Sign
signature = sign_invoice(hrp, data, private_key)
data.extend(signature)
# Encode as bech32
return bech32.bech32_encode(hrp, data)
Validation
def validate_invoice(invoice: Invoice) -> bool:
"""Validate invoice fields."""
# Check expiry
if invoice.timestamp + invoice.expiry < time.time():
raise ValueError("Invoice expired")
# Check required fields
if not invoice.payment_hash:
raise ValueError("Missing payment hash")
# Check description
if not invoice.description and not invoice.description_hash:
raise ValueError("Missing description")
# Verify signature
if not verify_signature(invoice):
raise ValueError("Invalid signature")
return True
Related BOLTs
Machine-Readable Summary
{
"bolt": "11",
"title": "Invoice Protocol",
"status": "final",
"encoding": "bech32",
"prefixes": {
"mainnet": "lnbc",
"testnet": "lntb",
"regtest": "lnbcrt"
},
"required_fields": ["payment_hash"],
"optional_fields": [
"payment_secret",
"description",
"description_hash",
"expiry",
"min_cltv",
"payee",
"route_hints",
"features",
"fallback"
]
}