---
title: Fetching invoices from a Lightning Address
slug: fetching-invoices-from-a-lightning-address
docTags: 
createdAt: 2025-03-17T12:56:23.457Z
---

# Getting BOLT11 Invoices from Lightning Addresses and LNURL

## Introduction

Lightning Addresses provide a simple, email-like addressing system for Lightning Network payments. This guide shows how to retrieve BOLT11 invoices from either a Lightning Address (using the LUD-16 specification) or an LNURL string.

Lightning Addresses are built with the [LNURL spec](https://github.com/lnurl/luds) by implementing three features of the spec: [LUD-01](https://github.com/lnurl/luds/blob/luds/01.md) (base LNURL encoding/decoding), [LUD-06](https://github.com/lnurl/luds/blob/luds/06.md) (payRequest base spec), and [LUD-16](https://github.com/lnurl/luds/blob/luds/16.md) (paying to static internet identifiers). Some Lightning Address providers also implement LUD-21 for verification directly with the Lightning Address host.

The spec implementation is only required for the Lightning Address host. A wallet or service only has to adhere to the LNURL scheme of requesting from the correct URLs in order to support fetching invoices.

## Lightning Address Format

A Lightning Address follows the format: username\@domain.com

An LNURL string starts with LNURL followed by a bech32-encoded URL.

## Step-by-Step Process

### 1. Parse the Lightning Address

:::CodeblockTabs
```javascript
function parseLightningAddress(lightningAddress) {
  const parts = lightningAddress.split('@');
  if (parts.length !== 2) {
    throw new Error('Invalid Lightning Address format');
  }
  
  return {
    username: parts[0],
    domain: parts[1]
  };
}
```

```python
def parse_lightning_address(lightning_address: str) -> tuple[str, str]:
    """
    Split a Lightning Address into (username, domain).

    Raises
    ------
    ValueError
        If the string is not in the expected format.
    """
    try:
        username, domain = lightning_address.split("@", maxsplit=1)
    except ValueError:
        raise ValueError("Invalid Lightning Address format") from None

    if not username or not domain:
        raise ValueError("Invalid Lightning Address format")

    return username, domain
```

```php
function parseLightningAddress($lightningAddress) {
    $parts = explode('@', $lightningAddress);
    if (count($parts) !== 2) {
        throw new Exception('Invalid Lightning Address format');
    }
    
    return [
        'username' => $parts[0],
        'domain' => $parts[1]
    ];
}
```
:::

### 2. Request a BOLT11 Invoice

This function handles both Lightning Addresses and LNURL strings:

:::CodeblockTabs
```javascript
async function requestBolt11Invoice(input, amountSats, memo = '') {
  try {
    // Check if input is a Lightning Address (contains @)
    if (input.includes('@')) {
      // Parse the Lightning Address
      const { username, domain } = parseLightningAddress(input);
      
      // Make request to the Lightning Address provider
      const response = await fetch(`https://${domain}/.well-known/lnurlp/${username}`);
      
      if (!response.ok) {
        throw new Error('Direct LUD-16 approach failed');
      }
      
      const lnurlpData = await response.json();
      
      // Check if we can request an invoice
      if (lnurlpData.tag !== 'payRequest') {
        throw new Error('Invalid LNURL-pay response');
      }
      
      // Check amount limits in millisats
      if (amountSats < lnurlpData.minSendable / 1000 || amountSats > lnurlpData.maxSendable / 1000) {
        throw new Error(`Amount must be between ${lnurlpData.minSendable / 1000} and ${lnurlpData.maxSendable / 1000} sats`);
      }
      
      // Request the actual invoice
      let callbackUrl = lnurlpData.callback;
      const separator = callbackUrl.includes('?') ? '&' : '?';
      callbackUrl += `${separator}amount=${amountSats * 1000}`;
      
      if (memo) {
        callbackUrl += `&comment=${encodeURIComponent(memo)}`;
      }
      
      const invoiceResponse = await fetch(callbackUrl);
      
      if (!invoiceResponse.ok) {
        throw new Error(`Failed to get invoice: ${invoiceResponse.status}`);
      }
      
      const invoiceData = await invoiceResponse.json();
      
      if (!invoiceData.pr) {
        throw new Error('No BOLT11 invoice in response');
      }
      
      return invoiceData.pr; // This is the BOLT11 invoice
    } else {
      // Assume it's an LNURL string
      return await handleLnurlString(input, amountSats, memo);
    }
  } catch (error) {
    console.error('Error requesting BOLT11 invoice:', error);
    throw error;
  }
}
```

```python
import requests
from urllib.parse import urlencode

def request_bolt11_invoice(target: str, amount_sats: int, memo: str = "") -> str:
    """
    Obtain a BOLT11 invoice for the requested amount (in sats).

    Parameters
    ----------
    target : str
        Lightning Address (`name@domain`) or bech32 LNURL string.
    amount_sats : int
    memo : str, optional
        Optional comment to attach to the invoice.

    Returns
    -------
    str
        The BOLT11 invoice string.

    Raises
    ------
    RuntimeError
        For any network or protocol-level failure.
    """
    if "@" in target:                    # treat as Lightning Address
        username, domain = parse_lightning_address(target)
        lnurlp_url = f"https://{domain}/.well-known/lnurlp/{username}"
        lnurlp_data = _get_json(lnurlp_url)

        if lnurlp_data.get("tag") != "payRequest":
            raise RuntimeError("Invalid LNURL-pay response")

        _enforce_amount_limits(lnurlp_data, amount_sats)

        callback_url = _build_callback_url(
            lnurlp_data["callback"], amount_sats, memo
        )
        invoice_data = _get_json(callback_url)
        return _extract_invoice(invoice_data)

    # otherwise assume a raw LNURL
    return handle_lnurl_string(target, amount_sats, memo)

# utility helpers
def _get_json(url: str, *, timeout: int = 10) -> dict:
    resp = requests.get(url, timeout=timeout)
    if not resp.ok:
        raise RuntimeError(f"HTTP {resp.status_code} from {url}")
    return resp.json()

def _enforce_amount_limits(meta: dict, sats: int) -> None:
    min_sat = meta["minSendable"] // 1000
    max_sat = meta["maxSendable"] // 1000
    if not (min_sat <= sats <= max_sat):
        raise RuntimeError(
            f"Amount must be between {min_sat} and {max_sat} sats"
        )

def _build_callback_url(base: str, sats: int, memo: str) -> str:
    params = {"amount": sats * 1000}
    if memo:
        params["comment"] = memo
    connector = "&" if "?" in base else "?"
    return f"{base}{connector}{urlencode(params)}"

def _extract_invoice(data: dict) -> str:
    pr = data.get("pr")
    if not pr:
        raise RuntimeError("No BOLT11 invoice in response")
    return pr

```

```php
function requestBolt11Invoice($input, $amountSats, $memo = '') {
    try {
        // Check if input is a Lightning Address (contains @)
        if (strpos($input, '@') !== false) {
            // Parse the Lightning Address
            $parsed = parseLightningAddress($input);
            $username = $parsed['username'];
            $domain = $parsed['domain'];

            // Make request to the Lightning Address provider
            $response = file_get_contents("https://{$domain}/.well-known/lnurlp/{$username}");

            if ($response === false) {
                throw new Exception('Direct LUD-16 approach failed');
            }

            $lnurlpData = json_decode($response, true);

            // Check if we can request an invoice
            if (!isset($lnurlpData['tag']) || $lnurlpData['tag'] !== 'payRequest') {
                throw new Exception('Invalid LNURL-pay response');
            }

            // Check amount limits in millisats
            if ($amountSats < $lnurlpData['minSendable'] / 1000 || $amountSats > $lnurlpData['maxSendable'] / 1000) {
                throw new Exception("Amount must be between " . ($lnurlpData['minSendable'] / 1000) . " and " . ($lnurlpData['maxSendable'] / 1000) . " sats");
            }

            // Request the actual invoice
            $callbackUrl = $lnurlpData['callback'];
            $separator = (strpos($callbackUrl, '?') !== false) ? '&' : '?';
            $callbackUrl .= "{$separator}amount=" . ($amountSats * 1000);

            if (!empty($memo)) {
                $callbackUrl .= "&comment=" . urlencode($memo);
            }

            $invoiceResponse = file_get_contents($callbackUrl);

            if ($invoiceResponse === false) {
                throw new Exception('Failed to get invoice');
            }

            $invoiceData = json_decode($invoiceResponse, true);

            if (!isset($invoiceData['pr'])) {
                throw new Exception('No BOLT11 invoice in response');
            }

            return $invoiceData['pr']; // This is the BOLT11 invoice
        } else {
            // Assume it's an LNURL string
            return handleLnurlString($input, $amountSats, $memo);
        }
    } catch (Exception $error) {
        error_log('Error requesting BOLT11 invoice: ' . $error->getMessage());
        throw $error;
    }
}
```
:::

### 3. Handle LNURL Strings

:::CodeblockTabs
```javascript
async function handleLnurlString(lnurlString, amountSats, memo = '') {
  // Validate if it's a bech32 LNURL string
  if (lnurlString.toUpperCase().startsWith('LNURL')) {
    // Decode the bech32 LNURL
    const decodedUrl = decodeBech32Url(lnurlString);
    
    // Make request to the decoded URL
    const response = await fetch(decodedUrl);
    
    if (!response.ok) {
      throw new Error('Failed to get response from LNURL');
    }
    
    const lnurlData = await response.json();
    
    // Process LNURL-pay (LUD-06)
    if (lnurlData.tag === 'payRequest') {
      // Check amount limits in millisats
      if (amountSats < lnurlData.minSendable / 1000 || amountSats > lnurlData.maxSendable / 1000) {
        throw new Error(`Amount must be between ${lnurlData.minSendable / 1000} and ${lnurlData.maxSendable / 1000} sats`);
      }
      
      // Request the actual invoice
      let callbackUrl = lnurlData.callback;
      const separator = callbackUrl.includes('?') ? '&' : '?';
      callbackUrl += `${separator}amount=${amountSats * 1000}`;
      
      if (memo) {
        callbackUrl += `&comment=${encodeURIComponent(memo)}`;
      }
      
      const invoiceResponse = await fetch(callbackUrl);
      
      if (!invoiceResponse.ok) {
        throw new Error(`Failed to get invoice: ${invoiceResponse.status}`);
      }
      
      const invoiceData = await invoiceResponse.json();
      
      if (!invoiceData.pr) {
        throw new Error('No BOLT11 invoice in response');
      }
      
      return invoiceData.pr; // Return the BOLT11 invoice
    } else {
      throw new Error('Not a valid LNURL-pay response');
    }
  } else {
    throw new Error('Not a valid LNURL');
  }
}
```

```python
from bech32 import bech32_decode, convertbits

def handle_lnurl_string(lnurl: str, amount_sats: int, memo: str = "") -> str:
    if not lnurl.upper().startswith("LNURL"):
        raise RuntimeError("Not a valid LNURL")

    decoded_url = decode_bech32_url(lnurl)
    lnurl_meta = _get_json(decoded_url)

    if lnurl_meta.get("tag") != "payRequest":
        raise RuntimeError("Not a valid LNURL-pay response")

    _enforce_amount_limits(lnurl_meta, amount_sats)

    callback_url = _build_callback_url(
        lnurl_meta["callback"], amount_sats, memo
    )
    invoice_data = _get_json(callback_url)
    return _extract_invoice(invoice_data)
    
# utility helpers
def _get_json(url: str, *, timeout: int = 10) -> dict:
    resp = requests.get(url, timeout=timeout)
    if not resp.ok:
        raise RuntimeError(f"HTTP {resp.status_code} from {url}")
    return resp.json()

def _enforce_amount_limits(meta: dict, sats: int) -> None:
    min_sat = meta["minSendable"] // 1000
    max_sat = meta["maxSendable"] // 1000
    if not (min_sat <= sats <= max_sat):
        raise RuntimeError(
            f"Amount must be between {min_sat} and {max_sat} sats"
        )

def _build_callback_url(base: str, sats: int, memo: str) -> str:
    params = {"amount": sats * 1000}
    if memo:
        params["comment"] = memo
    connector = "&" if "?" in base else "?"
    return f"{base}{connector}{urlencode(params)}"

def _extract_invoice(data: dict) -> str:
    pr = data.get("pr")
    if not pr:
        raise RuntimeError("No BOLT11 invoice in response")
    return pr
```

```php
function handleLnurlString($lnurlString, $amountSats, $memo = '') {
    // Validate if it's a bech32 LNURL string
    if (strtoupper(substr($lnurlString, 0, 5)) === 'LNURL') {
        // Decode the bech32 LNURL
        $decodedUrl = decodeBech32Url($lnurlString);

        // Make request to the decoded URL
        $response = file_get_contents($decodedUrl);

        if ($response === false) {
            throw new Exception('Failed to get response from LNURL');
        }

        $lnurlData = json_decode($response, true);

        // Process LNURL-pay (LUD-06)
        if (isset($lnurlData['tag']) && $lnurlData['tag'] === 'payRequest') {
            // Check amount limits in millisats
            if ($amountSats < $lnurlData['minSendable'] / 1000 || $amountSats > $lnurlData['maxSendable'] / 1000) {
                throw new Exception("Amount must be between " . ($lnurlData['minSendable'] / 1000) . " and " . ($lnurlData['maxSendable'] / 1000) . " sats");
            }

            // Request the actual invoice
            $callbackUrl = $lnurlData['callback'];
            $separator = (strpos($callbackUrl, '?') !== false) ? '&' : '?';
            $callbackUrl .= "{$separator}amount=" . ($amountSats * 1000);

            if (!empty($memo)) {
                $callbackUrl .= "&comment=" . urlencode($memo);
            }

            $invoiceResponse = file_get_contents($callbackUrl);

            if ($invoiceResponse === false) {
                throw new Exception('Failed to get invoice');
            }

            $invoiceData = json_decode($invoiceResponse, true);

            if (!isset($invoiceData['pr'])) {
                throw new Exception('No BOLT11 invoice in response');
            }

            return $invoiceData['pr']; // Return the BOLT11 invoice
        } else {
            throw new Exception('Not a valid LNURL-pay response');
        }
    } else {
        throw new Exception('Not a valid LNURL');
    }
}
```
:::

### 4. Decode Bech32 LNURL

:::CodeblockTabs
```javascript
function decodeBech32Url(lnurlString) {
  try {
    // Use bech32 library to decode
    const { prefix, words } = bech32.decode(lnurlString, 1023);
    
    if (prefix !== 'lnurl') {
      throw new Error('Not a valid LNURL string');
    }
    
    // Convert the 5-bit words to 8-bit bytes
    const bytes = bech32.fromWords(words);
    
    // Convert bytes to string
    return Buffer.from(bytes).toString();
  } catch (error) {
    throw new Error(`Invalid LNURL string: ${error.message}`);
  }
}
```

```python
def decode_bech32_url(lnurl: str) -> str:
    """
    Convert bech32-encoded LNURL -> clear-text URL.
    """
    hrp, data = bech32_decode(lnurl)
    if hrp is None or hrp.lower() != "lnurl":
        raise ValueError("Invalid LNURL string")

    # Convert 5-bit groups back into raw bytes
    decoded_bytes = bytes(convertbits(data, 5, 8, False))
    return decoded_bytes.decode()
```

```php
function decodeBech32Url($lnurlString) {
    try {
        // Use BitWasp's bech32 decoder
        $decoded = \BitWasp\Bech32\decode($lnurlString);
        if ($decoded[0] !== 'lnurl') {
            throw new Exception('Not a valid LNURL string');
        }
        
        // Convert the 5-bit words to 8-bit bytes
        $bytes = \BitWasp\Bech32\convertBits($decoded[1], count($decoded[1]), 5, 8, false);
        
        // Convert bytes to string
        return implode('', array_map('chr', $bytes));
    } catch (\Exception $e) {
        throw new Exception('Invalid LNURL string: ' . $e->getMessage());
    }
}
```
:::

## Error Handling Tips

- Always validate the input format before processing (Lightning Address or LNURL)
- Check for minimum and maximum sendable amounts
- Handle network errors gracefully
- Implement timeouts for fetch requests
- Verify that the response contains a valid BOLT11 invoice

## Additional Notes

- LUD-16 is an extension of the LNURL-pay protocol (LUD-06)
- LUD-01 defines the base LNURL encoding and decoding process
- Some Lightning Address providers may have rate limits
- The BOLT11 invoice will have an expiry time, typically 24 hours
- Consider implementing payment verification using the provider's API or LUD-21 (if available)
- When constructing callback URLs, be careful about query parameters that might already exist

