Main Menu
Examples
Fetching invoices from a Lightning Address
9 min
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 by implementing three features of the spec lud 01 (base lnurl encoding/decoding), lud 06 (payrequest base spec), and lud 16 (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 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] }; }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, domainfunction 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 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; } }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 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 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'); } }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 prfunction 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 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}`); } }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()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