Menu
Lightning Beginner 6 min read

BOLT11 Invoices

Lightning invoice format and parsing. Payment requests, amount encoding, expiry, and route hints.

bolt11 invoice payment-request bech32

BOLT11 Invoices

A BOLT11 invoice is a payment request—a string that encodes everything needed to make a Lightning payment. It’s the standard way to request payment on Lightning.

Why Invoices Matter for Agents

BenefitDescription
Self-containedAll payment info in one string
VerifiableCryptographically signed
ExpiringAutomatic invalidation
ParseableMachine-readable format

Invoice Structure

lnbc10u1pjnrfzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq...
│    │ │        │
│    │ │        └─ Data (bech32)
│    │ └─ Timestamp
│    └─ Amount (10 microsatoshis)
└─ Network (mainnet)

Network Prefixes

PrefixNetwork
lnbcBitcoin mainnet
lntbBitcoin testnet
lnbcrtBitcoin regtest
lnsbBitcoin signet

Amount Encoding

SuffixMultiplierExample
(none)1 BTClnbc1... = 1 BTC
m0.001 BTClnbc1m... = 1 mBTC
u0.000001 BTClnbc1u... = 1 μBTC
n0.000000001 BTClnbc1n... = 1 nBTC
p0.000000000001 BTClnbc1p... = 1 pBTC

Common amounts:

  • lnbc10u = 10 μBTC = 1,000 sats
  • lnbc1m = 1 mBTC = 100,000 sats
  • lnbc100n = 100 nBTC = 10 sats

Invoice Fields

Required Fields

FieldTagDescription
Payment hashp32-byte hash of preimage
Timestamp-Unix timestamp of creation
Signature-Node’s signature

Optional Fields

FieldTagDescription
DescriptiondHuman-readable purpose
Description hashhSHA256 of long description
ExpiryxSeconds until expiration (default: 3600)
Min CLTVcMinimum CLTV delta (default: 18)
Fallback addressfOn-chain address if payment fails
Route hintsrPrivate channel routing info
Features9Feature bit vector
Payee pubkeynDestination node (if not in signature recovery)

Parsing an Invoice

Using Python

import bolt11

# Parse invoice string
invoice = bolt11.decode(
    "lnbc10u1pj9nrfzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqf..."
)

print(f"Network: {invoice.currency}")
print(f"Amount: {invoice.amount_msat} msat")
print(f"Payment hash: {invoice.payment_hash}")
print(f"Description: {invoice.description}")
print(f"Expiry: {invoice.expiry} seconds")
print(f"Destination: {invoice.payee}")

Using JavaScript

import { decode } from 'bolt11';

const invoice = decode(
  'lnbc10u1pj9nrfzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqf...'
);

console.log(`Amount: ${invoice.satoshis} sats`);
console.log(`Description: ${invoice.tags.find(t => t.tagName === 'description')?.data}`);
console.log(`Payment hash: ${invoice.tags.find(t => t.tagName === 'payment_hash')?.data}`);

Using LND CLI

lncli decodepayreq lnbc10u1pj9nrfzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqf...

Output:

{
  "destination": "03abc...",
  "payment_hash": "0001...",
  "num_satoshis": "1000",
  "timestamp": "1706745600",
  "expiry": "3600",
  "description": "Coffee payment",
  "cltv_expiry": "40"
}

Creating an Invoice

Via LNbits API

curl -X POST https://legend.lnbits.com/api/v1/payments \
  -H "X-Api-Key: $INVOICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "out": false,
    "amount": 1000,
    "memo": "Payment for service",
    "expiry": 3600
  }'

Response:

{
  "payment_hash": "abc123...",
  "payment_request": "lnbc10u1pj9nrfz..."
}

Via LND

invoice = lnd.add_invoice(
    value=1000,  # satoshis
    memo="Payment for service",
    expiry=3600  # seconds
)

print(f"Invoice: {invoice.payment_request}")
print(f"Payment hash: {invoice.r_hash.hex()}")

Invoice States

┌──────────┐
│  Open    │ ─── payment received ───→ ┌──────────┐
└──────────┘                           │  Paid    │
     │                                 └──────────┘

     └─── time > expiry ──────────────→ ┌──────────┐
                                        │ Expired  │
                                        └──────────┘

Checking Invoice Status

# Via LNbits
response = requests.get(
    f"https://legend.lnbits.com/api/v1/payments/{payment_hash}",
    headers={"X-Api-Key": INVOICE_KEY}
)

status = response.json()
print(f"Paid: {status['paid']}")

Route Hints

Private channels aren’t in the gossip network. Route hints tell senders how to reach the recipient:

Invoice with route hints:
  Route 1:
    - Pubkey: 03abc...
    - Short channel ID: 123x456x0
    - Fee base: 1000 msat
    - Fee rate: 100 ppm
    - CLTV delta: 40

Adding Route Hints

# LND: Invoice for private channel
invoice = lnd.add_invoice(
    value=1000,
    memo="Private channel payment",
    private=True  # Includes route hints automatically
)

Amount-less Invoices

Some invoices don’t specify an amount—the payer decides:

lnbc1pj9nrfzpp5...  # No amount suffix

Use cases:

  • Donations
  • Tips
  • Variable pricing

Security note: Amount-less invoices should verify payment amounts application-side.

Expiry Best Practices

Use CaseExpiryReason
Point of sale60-300sImmediate payment expected
E-commerce3600s (1 hour)Checkout flow
Subscriptions86400s (1 day)User convenience
Donations604800s (1 week)Maximum flexibility

Default: 3600 seconds (1 hour)

Invoice Features (Bits)

Feature bits indicate supported capabilities:

BitFeatureRequired?
8/9var_onion_optinVariable onion
14/15payment_secretPayment secret
16/17basic_mppMulti-path payments

Error Handling

Common Issues

ErrorCauseSolution
Invoice expiredPast expiry timeRequest new invoice
Unknown payment hashAlready paid or invalidCheck payment status
Amount mismatchSent wrong amountUse exact invoice amount
Route not foundNo path to destinationRequest route hints

Validating Before Payment

def validate_invoice(invoice_str: str, expected_amount: int) -> bool:
    invoice = bolt11.decode(invoice_str)

    # Check expiry
    if invoice.timestamp + invoice.expiry < time.time():
        raise ValueError("Invoice expired")

    # Check amount (if specified)
    if invoice.amount_msat and invoice.amount_msat != expected_amount * 1000:
        raise ValueError("Amount mismatch")

    # Check network
    if invoice.currency != "bc":  # mainnet
        raise ValueError("Wrong network")

    return True

BOLT12 Offers (Preview)

BOLT12 is an upcoming improvement:

FeatureBOLT11BOLT12
ReusableNoYes
Payer privacyLimitedBlinded paths
Amount negotiationNoYes
SubscriptionsNoBuilt-in

BOLT12 is still in draft status but supported by some nodes (CLN).


Machine-Readable Summary

{
  "topic": "bolt11-invoices",
  "key_concepts": [
    "payment-request",
    "bech32-encoding",
    "expiry",
    "route-hints"
  ],
  "fields": {
    "required": ["payment_hash", "timestamp", "signature"],
    "optional": ["description", "expiry", "fallback", "route_hints"]
  },
  "prefixes": {
    "mainnet": "lnbc",
    "testnet": "lntb",
    "regtest": "lnbcrt"
  }
}