Menu
BOLT-bolt-11 Final

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

AspectValue
StatusFinal
LayerApplication
PurposePayment requests
EncodingBech32

Invoice Structure

lnbc10u1pj9nrfzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp
│    │ │        │                                    │
│    │ │        │                                    └─ Signature
│    │ │        └─ Data (bech32)
│    │ └─ Timestamp
│    └─ Amount: 10u = 10 microsatoshis
└─ Prefix: lnbc = Bitcoin mainnet

Prefix

Human-Readable Part

ln + [currency] + [amount]

Currency Codes

PrefixNetwork
lnbcBitcoin mainnet
lntbBitcoin testnet
lnbcrtBitcoin regtest
lntbsBitcoin signet

Amount Encoding

SuffixMultiplierExample
(none)1 BTClnbc1 = 1 BTC
m0.001lnbc1m = 1 mBTC = 100,000 sats
u0.000001lnbc1u = 1 μBTC = 100 sats
n0.000000001lnbc1n = 1 nBTC = 0.1 sats
p0.000000000001lnbc10p = 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

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"
  ]
}