Menu
Lightning Python Executable Jan 31, 2026

Pay Lightning Invoices with LNbits

Send Lightning payments by paying BOLT11 invoices via LNbits API

#lnbits #payment #bolt11 #api

Overview

Pay Lightning invoices to send instant Bitcoin payments. This requires an Admin key (not just Invoice key) because it spends funds.

The Code

"""
Lightning Invoice Payer
Send payments via LNbits API

Requirements:
- requests (pip install requests)
- LNbits instance with Admin key

Environment variables:
- LNBITS_URL: Your LNbits instance URL
- LNBITS_ADMIN_KEY: Admin API key (required for sending)

SECURITY: Admin key can spend all funds. Never expose it.
"""

import os
import requests
from typing import Optional
from dataclasses import dataclass

# Configuration
LNBITS_URL = os.getenv("LNBITS_URL", "https://legend.lnbits.com")
LNBITS_ADMIN_KEY = os.getenv("LNBITS_ADMIN_KEY", "")


@dataclass
class PaymentResult:
    """Result of a Lightning payment."""
    success: bool
    payment_hash: str
    preimage: Optional[str]
    fee_msat: int
    error: Optional[str]


def get_wallet_balance() -> dict:
    """
    Get current wallet balance.

    Returns:
        dict with balance in sats and msat
    """
    if not LNBITS_ADMIN_KEY:
        raise ValueError("LNBITS_ADMIN_KEY not set")

    headers = {"X-Api-Key": LNBITS_ADMIN_KEY}

    response = requests.get(
        f"{LNBITS_URL}/api/v1/wallet",
        headers=headers,
        timeout=10
    )
    response.raise_for_status()

    data = response.json()
    balance_msat = data.get("balance", 0)

    return {
        "balance_msat": balance_msat,
        "balance_sats": balance_msat // 1000,
        "name": data.get("name", "Unknown")
    }


def decode_invoice(bolt11: str) -> dict:
    """
    Decode a BOLT11 invoice to inspect before paying.

    Args:
        bolt11: BOLT11 invoice string

    Returns:
        dict with invoice details
    """
    if not LNBITS_ADMIN_KEY:
        raise ValueError("LNBITS_ADMIN_KEY not set")

    headers = {"X-Api-Key": LNBITS_ADMIN_KEY}

    response = requests.post(
        f"{LNBITS_URL}/api/v1/payments/decode",
        headers=headers,
        json={"data": bolt11},
        timeout=10
    )
    response.raise_for_status()

    data = response.json()

    return {
        "payment_hash": data.get("payment_hash"),
        "amount_msat": data.get("amount_msat", 0),
        "amount_sats": data.get("amount_msat", 0) // 1000,
        "description": data.get("description", ""),
        "expiry": data.get("expiry"),
        "timestamp": data.get("date")
    }


def pay_invoice(
    bolt11: str,
    max_fee_sats: Optional[int] = None
) -> PaymentResult:
    """
    Pay a Lightning invoice.

    Args:
        bolt11: BOLT11 invoice string
        max_fee_sats: Maximum routing fee (optional)

    Returns:
        PaymentResult with success status and details

    Raises:
        ValueError: If API key not configured
    """
    if not LNBITS_ADMIN_KEY:
        raise ValueError("LNBITS_ADMIN_KEY not set")

    headers = {
        "X-Api-Key": LNBITS_ADMIN_KEY,
        "Content-Type": "application/json"
    }

    payload = {
        "out": True,  # True = outgoing payment
        "bolt11": bolt11
    }

    if max_fee_sats:
        payload["max_fee"] = max_fee_sats * 1000  # Convert to msat

    try:
        response = requests.post(
            f"{LNBITS_URL}/api/v1/payments",
            headers=headers,
            json=payload,
            timeout=60  # Payments can take time to route
        )
        response.raise_for_status()

        data = response.json()

        return PaymentResult(
            success=True,
            payment_hash=data["payment_hash"],
            preimage=data.get("preimage"),
            fee_msat=data.get("fee", 0),
            error=None
        )

    except requests.HTTPError as e:
        error_msg = str(e)
        try:
            error_data = e.response.json()
            error_msg = error_data.get("detail", str(e))
        except:
            pass

        return PaymentResult(
            success=False,
            payment_hash="",
            preimage=None,
            fee_msat=0,
            error=error_msg
        )


def safe_pay(
    bolt11: str,
    max_amount_sats: int,
    max_fee_percent: float = 1.0
) -> PaymentResult:
    """
    Pay an invoice with safety checks.

    Args:
        bolt11: BOLT11 invoice string
        max_amount_sats: Maximum amount willing to pay
        max_fee_percent: Maximum fee as percentage of amount

    Returns:
        PaymentResult
    """
    # Check balance first
    wallet = get_wallet_balance()
    print(f"Wallet balance: {wallet['balance_sats']} sats")

    # Decode invoice
    invoice = decode_invoice(bolt11)
    amount = invoice["amount_sats"]

    print(f"Invoice amount: {amount} sats")
    print(f"Description: {invoice['description']}")

    # Safety checks
    if amount > max_amount_sats:
        return PaymentResult(
            success=False,
            payment_hash="",
            preimage=None,
            fee_msat=0,
            error=f"Amount {amount} exceeds max {max_amount_sats}"
        )

    if amount > wallet["balance_sats"]:
        return PaymentResult(
            success=False,
            payment_hash="",
            preimage=None,
            fee_msat=0,
            error=f"Insufficient balance: {wallet['balance_sats']} < {amount}"
        )

    # Calculate max fee
    max_fee = int(amount * max_fee_percent / 100)
    print(f"Max fee: {max_fee} sats ({max_fee_percent}%)")

    # Execute payment
    return pay_invoice(bolt11, max_fee_sats=max_fee)


# Example usage
if __name__ == "__main__":
    if not LNBITS_ADMIN_KEY:
        print("Set LNBITS_ADMIN_KEY environment variable")
        print("WARNING: Admin key can spend funds. Keep it secret!")
        exit(1)

    # Example invoice (replace with real one)
    test_invoice = input("Enter BOLT11 invoice: ").strip()

    if not test_invoice:
        # Just show balance if no invoice
        wallet = get_wallet_balance()
        print(f"\nWallet: {wallet['name']}")
        print(f"Balance: {wallet['balance_sats']:,} sats")
        exit(0)

    print("\n=== Decoding Invoice ===")
    try:
        decoded = decode_invoice(test_invoice)
        print(f"Amount:      {decoded['amount_sats']} sats")
        print(f"Description: {decoded['description']}")
        print(f"Hash:        {decoded['payment_hash'][:32]}...")

        # Confirm before paying
        confirm = input("\nPay this invoice? (yes/no): ").strip().lower()
        if confirm != "yes":
            print("Payment cancelled")
            exit(0)

        print("\n=== Sending Payment ===")
        result = safe_pay(
            bolt11=test_invoice,
            max_amount_sats=100000,  # Max 100k sats
            max_fee_percent=1.0      # Max 1% fee
        )

        if result.success:
            print(f"\nPayment successful!")
            print(f"Preimage: {result.preimage}")
            print(f"Fee: {result.fee_msat} msat")
        else:
            print(f"\nPayment failed: {result.error}")

    except Exception as e:
        print(f"Error: {e}")

Usage

# Set environment variables
export LNBITS_URL="https://your-lnbits.com"
export LNBITS_ADMIN_KEY="your-admin-key"  # KEEP SECRET!

# Install and run
pip install requests
python pay_invoice.py

Example Output

=== Decoding Invoice ===
Amount:      1000 sats
Description: Coffee payment
Hash:        abc123def456789...

Pay this invoice? (yes/no): yes

=== Sending Payment ===
Wallet balance: 50000 sats
Invoice amount: 1000 sats
Description: Coffee payment
Max fee: 10 sats (1.0%)

Payment successful!
Preimage: xyz789abc123...
Fee: 1200 msat

Agent Notes

Security is critical:

  • Admin key = full access to funds
  • Never log or expose the admin key
  • Use environment variables, not hardcoded values
  • Consider using a dedicated wallet with limited funds

Pre-payment checks:

  1. Decode invoice first to verify amount
  2. Check wallet balance
  3. Validate amount against your limits
  4. Set max fee to avoid routing attacks

Payment failures: Common causes are insufficient balance, expired invoice, routing failures, or the recipient being offline.