Main Menu
...
Monitoring
REST
5 min
monitoring via lnd's rest interface lnd's rest interface is well documented on https //lightning engineering/api docs/api/lnd/ https //lightning engineering/api docs/api/lnd/ but it is important to pay attention to the rest encoding section and ensure all base64 strings are passed as url safe base64 encoded strings (unless the endpoint allows you to pass a vlaue as hex in which case it is safe to put in a url) polling for single invoice polling for an invoice created by you on your node identified by it's url safe base64 hash lnd method https //lightning engineering/api docs/api/lnd/invoices/lookup invoice v2/ https //lightning engineering/api docs/api/lnd/invoices/lookup invoice v2/ // invoice polling demo monitor incoming payments const axios = require('axios'); // configure lnd connection const lnd = axios create({ baseurl `https //${process env lnd host} 8080`, headers { "content type" "application/json", "grpc metadata macaroon" process env lnd macaroon } }); // convert base64 to url safe base64 // lnd rest api requires url safe base64 for byte fields in url parameters // https //lightning engineering/api docs/api/lnd/#rest encoding const tourlsafebase64 = (base64string) => { return base64string replace(/\\+/g, " ") // replace + with replace(/\\//g, " "); // replace / with // keep trailing = as is }; // poll invoice status until settled or canceled const pollinvoicestatus = async ( paymenthash, maxattempts = 60, delayms = 1000, ) => { console log(`original payment hash ${paymenthash}`); // convert to url safe base64 for the api call const urlsafehash = tourlsafebase64(paymenthash); console log(`url safe payment hash ${urlsafehash}`); for (let attempt = 1; attempt <= maxattempts; attempt++) { try { const response = await lnd get(`/v2/invoices/lookup`, { params { payment hash urlsafehash, }, }); const invoice = response data; console log(`invoice status response `, invoice); console log(`attempt ${attempt} invoice state ${invoice state}`); // check final states if (invoice state === "settled") { console log("✅ invoice paid!"); console log(`amount received ${invoice amt paid sat} sats`); console log( `settled at ${new date(parseint(invoice settle date) 1000) toisostring()}`, ); return invoice; } else if (invoice state === "canceled") { console log("❌ invoice canceled!"); return invoice; } else if (invoice state === "accepted") { console log("⏳ invoice accepted, waiting for settlement "); } else if (invoice state === "open") { console log("📄 invoice still open, waiting for payment "); } // invoice still pending, wait before next poll await new promise((resolve) => settimeout(resolve, delayms)); } catch (error) { console error(`error polling invoice ${error message}`); await new promise((resolve) => settimeout(resolve, delayms)); } } throw new error("invoice polling timeout invoice still pending"); }; // alternative poll using payment address instead of hash const pollinvoicebypaymentaddr = async ( paymentaddr, maxattempts = 60, delayms = 1000, ) => { console log(`payment address ${paymentaddr}`); const urlsafeaddr = tourlsafebase64(paymentaddr); for (let attempt = 1; attempt <= maxattempts; attempt++) { try { const response = await lnd get(`/v2/invoices/lookup`, { params { payment addr urlsafeaddr, }, }); const invoice = response data; console log(`attempt ${attempt} invoice state ${invoice state}`); if (invoice state === "settled") { console log("✅ invoice paid!"); return invoice; } else if (invoice state === "canceled") { console log("❌ invoice canceled!"); return invoice; } await new promise((resolve) => settimeout(resolve, delayms)); } catch (error) { console error(`error polling invoice ${error message}`); await new promise((resolve) => settimeout(resolve, delayms)); } } throw new error("invoice polling timeout"); }; // demo create invoice and monitor for payment const runinvoicedemo = async () => { try { // step 1 create invoice console log("creating invoice "); const invoiceresponse = await lnd post("/v1/invoices", { value 1000, // 1000 sats memo "test invoice for polling demo", expiry 3600, // 1 hour expiry }); const { payment request, r hash, payment addr } = invoiceresponse data; console log(`\n📄 invoice created!`); console log(`payment request ${payment request}`); console log(`payment hash (base64) ${r hash}`); console log(`payment address (base64) ${payment addr}`); console log( `\n💡 send payment to this invoice to see polling in action!\n`, ); // step 2 poll for payment using payment hash const settledinvoice = await pollinvoicestatus(r hash); console log("\n🎉 invoice settled!", settledinvoice); // display final invoice details console log(`\n📊 final invoice details \ amount ${settledinvoice value} sats requested \ amount paid ${settledinvoice amt paid sat} sats received \ memo ${settledinvoice memo} \ created ${new date(parseint(settledinvoice creation date) 1000) toisostring()} \ settled ${new date(parseint(settledinvoice settle date) 1000) toisostring()} \ htlcs ${settledinvoice htlcs? length || 0} payment(s) `); } catch (error) { console error("demo failed ", error message); } }; // run the demo runinvoicedemo();import os, time, base64, requests \# configuration from environment rest url = os getenv("lnd rest url", "https //localhost 8080") macaroon = os getenv("lnd macaroon") # hex string of admin macaroon invoice hash hex = os getenv("lnd invoice hash") # hex string of the invoice's payment hash if not (rest url and macaroon and invoice hash hex) raise environmenterror("please set lnd rest url, lnd macaroon, and lnd invoice hash env variables ") \# prepare request url and headers (macaroon as grpc metadata header) invoice hash bytes = bytes fromhex(invoice hash hex) invoice hash b64 = base64 urlsafe b64encode(invoice hash bytes) rstrip(b"=") decode() # url safe base64 (no padding) url = f"{rest url}/v1/invoice/{invoice hash b64}" headers = {"grpc metadata macaroon" macaroon} print(f"polling invoice (hash={invoice hash hex}) for settlement status ") while true try resp = requests get(url, headers=headers, timeout=5, verify=false) # verify=false for self signed certs except requests requestexception as e print(f"request error {e}") break if resp status code != 200 print(f"error fetching invoice http {resp status code} {resp text}") break invoice = resp json() if invoice get("settled") print("invoice settled! ✅") break else print("invoice not yet settled, checking again in 5 seconds ") time sleep(5)\<?php require 'vendor/autoload php'; // ensure guzzle is installed via composer use guzzlehttpclient; use guzzlehttpexception\requestexception; // configuration from environment or defaults $resthost = getenv('rest host') ? 'localhost 8080'; $macaroonpath = getenv('lnd macaroon path') ? '/path/to/admin macaroon'; $tlscertpath = getenv('lnd tls cert') ? '/path/to/tls cert'; $invoicehash = getenv('invoice hash hex') ? '\<invoice hash in hex>'; // 32 byte payment hash in hex // read macaroon as hex string for header $macaroonhex = trim(bin2hex(file get contents($macaroonpath))); // initialize http client (disable tls verification for self signed cert in dev) $client = new client(\[ 'base uri' => "https //{$resthost}", 'verify' => false, // set 'verify' => $tlscertpath to use lnd's tls certificate ]); / poll the invoice status until settled or max attempts reached @param string $invoicehashhex 32 byte payment hash in hex @param int $intervalsec seconds to wait between polls @param int $maxattempts max number of polling attempts / function pollinvoicestatus(string $invoicehashhex, int $intervalsec = 5, int $maxattempts = 12) void { global $client, $macaroonhex; $url = "/v1/invoice/{$invoicehashhex}"; // lnd rest endpoint for invoice lookup for ($attempt = 1; $attempt <= $maxattempts; $attempt++) { try { $response = $client >request('get', $url, \[ 'headers' => \[ 'grpc metadata macaroon' => $macaroonhex ], 'http errors' => false // don't throw exceptions on non 200 responses ]); } catch (requestexception $e) { echo "request error {$e >getmessage()}\n"; break; } $invoice = json decode((string)$response >getbody(), true); if (empty($invoice)) { echo "no invoice data (attempt {$attempt}) \n"; } elseif (!empty($invoice\['settled']) && $invoice\['settled'] == true) { echo "✅ invoice settled! invoice details \n"; print r($invoice); break; } else { echo "invoice not settled yet (attempt {$attempt}) retrying in {$intervalsec}s \n"; } sleep($intervalsec); } echo "polling finished \n"; } // start polling the invoice until it is settled or times out pollinvoicestatus($invoicehash); polling for all new and settled invoices list out all new and settled invoices utilizing index offset field lnd method https //lightning engineering/api docs/api/lnd/lightning/list invoices/ https //lightning engineering/api docs/api/lnd/lightning/list invoices/ // poll invoices js – continuously listinvoices // // • uses listinvoices pagination (index offset) to fetch “new since last poll” // • prints state transitions open → accepted → settled / canceled // • keeps going forever – ctrl c to quit // const axios = require('axios'); // env const lnd host = process env lnd host; // your node voltageapp io const macaroon hex = process env lnd macaroon; // hex encoded admin/read macaroon const poll ms = parseint(process env poll ms || '2000', 10); // 2 s default if (!lnd host || !macaroon hex) { console error('❌ set lnd host and lnd macaroon env vars first '); process exit(1); } // axios instance const lnd = axios create({ baseurl `https //${lnd host} 8080`, headers { 'grpc metadata macaroon' macaroon hex }, timeout 10 000, httpsagent new (require('https') agent)({ rejectunauthorized false }), }); // poll loop let indexoffset = '0'; // track “where we left off” (add index cursor) console log(`📡 polling invoices every ${poll ms} ms …`); const poll = async () => { try { const { data } = await lnd get('/v1/invoices', { params { index offset indexoffset, // only entries with add index > offset num max invoices 100, // batch size reversed false, // oldest → newest }, }); const { invoices = \[], last index offset } = data; if (invoices length) { console log(`\n🧾 ${invoices length} new invoice${invoices length > 1 ? 's' ''} `); invoices foreach(inv => { console log(` • #${inv add index padstart(4)} ${inv state} ` \+ `${inv value sat} sat (hash=${inv r hash? slice(0,8)}…)`); }); indexoffset = last index offset; // advance cursor } } catch (err) { console error('❌ poll error ', err message); } finally { settimeout(poll, poll ms); } }; poll(); import os, time, requests rest url = os getenv("lnd rest url", "https //localhost 8080") macaroon = os getenv("lnd macaroon") if not (rest url and macaroon) raise environmenterror("please set lnd rest url and lnd macaroon env variables ") url = f"{rest url}/v1/invoices" headers = {"grpc metadata macaroon" macaroon} last index = 0 # track last processed add index (initialize to 0 to get all from beginning) print("polling for new or settled invoices ") while true try \# use index offset to get invoices with add index > last index resp = requests get(url, headers=headers, params={"index offset" last index}, timeout=5, verify=false) except requests requestexception as e print(f"request error {e}") break if resp status code != 200 print(f"error polling invoices http {resp status code} {resp text}") break data = resp json() invoices = data get("invoices", \[]) \# log each new invoice or settlement for inv in invoices idx = inv get("add index") or inv get("settle index") # use whichever index is present (settled invoices have settle index) if inv get("settled") print(f"invoice {idx} has been settled ✅") else print(f"new invoice added (add index={idx})") \# update the last index offset for next poll last index = int(data get("last index offset", last index)) time sleep(5)\<?php require 'vendor/autoload php'; use guzzlehttpclient; $resthost = getenv('rest host') ? 'localhost 8080'; $macaroonpath = getenv('lnd macaroon path') ? '/path/to/admin macaroon'; $tlscertpath = getenv('lnd tls cert') ? '/path/to/tls cert'; $macaroonhex = trim(bin2hex(file get contents($macaroonpath))); $client = new client(\[ 'base uri' => "https //{$resthost}", 'verify' => false, // disable tls cert verification for self signed ]); try { $response = $client >get('/v1/invoices', \[ 'headers' => \[ 'grpc metadata macaroon' => $macaroonhex ], 'http errors' => false ]); $data = json decode((string)$response >getbody(), true); echo "total invoices " count($data\['invoices'] ?? \[]) php eol; print r($data); } catch (\exception $e) { echo "error listing invoices {$e >getmessage()}\n"; } polling for single payment polling for a payment made by you from your node identified by it's url safe base64 hash lnd method https //lightning engineering/api docs/api/lnd/router/track payment v2/ https //lightning engineering/api docs/api/lnd/router/track payment v2/ // payment polling demo monitor outgoing payments const axios = require('axios'); // configure lnd connection const lnd = axios create({ baseurl `https //${process env lnd host} 8080`, headers { "content type" "application/json", "grpc metadata macaroon" process env lnd macaroon } }); // convert base64 to url safe base64 // lnd rest api requires url safe base64 for byte fields in url paths // https //lightning engineering/api docs/api/lnd/#rest encoding const tourlsafebase64 = (base64string) => { return base64string replace(/\\+/g, " ") // replace + with replace(/\\//g, " "); // replace / with // keep trailing = as is }; // poll payment status until succeeded or failed const pollpaymentstatus = async ( paymenthash, maxattempts = 60, delayms = 1000, ) => { console log(`original payment hash ${paymenthash}`); // convert to url safe base64 for the api call const urlsafehash = tourlsafebase64(paymenthash); console log(`url safe payment hash ${urlsafehash}`); for (let attempt = 1; attempt <= maxattempts; attempt++) { try { const response = await lnd get(`/v2/router/track/${urlsafehash}`); const payment = response data result; console log(`payment status response `, payment); console log(`attempt ${attempt} payment status ${payment status}`); // check final states if (payment status === "succeeded") { console log("✅ payment successful!"); console log(`amount sent ${payment value sat} sats`); console log(`fees paid ${payment fee sat} sats`); return payment; } else if (payment status === "failed") { console log("❌ payment failed!"); console log(`failure reason ${payment failure reason}`); return payment; } // payment still in flight, wait before next poll await new promise((resolve) => settimeout(resolve, delayms)); } catch (error) { console error(`error polling payment ${error message}`); await new promise((resolve) => settimeout(resolve, delayms)); } } throw new error("payment polling timeout payment still pending"); }; // demo send payment and monitor status const runpaymentdemo = async () => { try { // example payment request (replace with actual invoice) const paymentrequest = "lntbs100n1p5yrdt2pp59kqswqngtjpu320ehkw0h8z5xr97jg93z22zmzvd7umdl7eucvyshp50ehzrzt69ed4ezkgwjretuaa9ju5yyvvl0q8xshprp04jg4mnk2scqzzsxqyz5vqsp5zc0t5tuk2qtq974jm4ac4kn3malxcp6c7a2sljtku6lmztfw9g7q9qxpqysgqvr9vypgvy35l6yh9wfllhan330nm8kuec0j64y58wznt0syxrjnhwffcfn032n7kdczlk7qfuwccd8fadp79xfcdmh9m372qfjzddscpm750xa"; // step 1 decode invoice to verify details console log("decoding payment request "); const decoded = await lnd get(`/v1/payreq/${paymentrequest}`); console log(`📄 invoice details \ destination ${decoded data destination} \ amount ${decoded data num satoshis} sats \ description ${decoded data description} `); // step 2 send payment console log("sending payment "); const paymentresponse = await lnd post("/v1/channels/transactions", { payment request paymentrequest, fee limit { fixed 1000 }, // max 1000 sats fee }); const { payment hash } = paymentresponse data; console log(`\n⚡ payment initiated!`); console log(`payment hash (base64) ${payment hash}\n`); // step 3 poll for completion const completedpayment = await pollpaymentstatus(payment hash); console log("\n🎉 payment complete!", completedpayment); } catch (error) { console error("demo failed ", error message); } }; // run the demo runpaymentdemo();import os, time, requests rest url = os getenv("lnd rest url", "https //localhost 8080") macaroon = os getenv("lnd macaroon") payment hash = os getenv("lnd payment hash") # hex string of the outgoing payment's hash if not (rest url and macaroon and payment hash) raise environmenterror("please set lnd rest url, lnd macaroon, and lnd payment hash env variables ") url = f"{rest url}/v1/payments" headers = {"grpc metadata macaroon" macaroon} print(f"polling for payment (hash={payment hash}) status ") found = false while true try resp = requests get(url, headers=headers, timeout=5, verify=false) except requests requestexception as e print(f"request error {e}") break if resp status code != 200 print(f"error fetching payments http {resp status code}") break payments = resp json() get("payments", \[]) \# search for the target payment in the list for p in payments if p get("payment hash") == payment hash or p get("payment hash") == payment hash lower() \# found the payment – log its status and result status = p get("status") or "unknown" if status upper() == "succeeded" print(f"payment {payment hash} succeeded 🎉 (paid {p get('value sat')} sat, fee {p get('fee sat')} sat)") elif status upper() == "failed" print(f"payment {payment hash} failed ❌ (failure reason {p get('failure reason')})") else print(f"payment {payment hash} status {status}") found = true break if found break \# if not found or not yet completed, wait and retry print("payment not completed yet, checking again in 5 seconds ") time sleep(5) \<?php require 'vendor/autoload php'; use guzzlehttpclient; use guzzlehttpexception\requestexception; // configuration $resthost = getenv('rest host') ? 'localhost 8080'; $macaroonpath = getenv('lnd macaroon path') ? '/path/to/admin macaroon'; $paymenthash = getenv('payment hash hex') ? '\<payment hash in hex>'; // prepare macaroon $macaroonhex = trim(bin2hex(file get contents($macaroonpath))); // set up guzzle http client $client = new client(\[ 'base uri' => "https //{$resthost}", 'verify' => false, // or use your tls cert if verifying ]); / poll a single payment status until it's succeeded or failed @param string $paymenthash payment hash (hex) @param int $intervalsec interval between polling attempts @param int $maxattempts maximum polling attempts / function pollpaymentstatus(string $paymenthash, int $intervalsec = 5, int $maxattempts = 12) void { global $client, $macaroonhex; $url = "/v1/payment/{$paymenthash}"; for ($attempt = 1; $attempt <= $maxattempts; $attempt++) { try { $response = $client >get($url, \[ 'headers' => \['grpc metadata macaroon' => $macaroonhex], 'http errors' => false ]); } catch (requestexception $e) { echo "request error {$e >getmessage()}\n"; break; } $payment = json decode((string)$response >getbody(), true); if (empty($payment) || isset($payment\['error'])) { echo "payment data not available yet or error occurred (attempt {$attempt}) \n"; } else { $status = $payment\['status'] ?? 'unknown'; echo "payment status {$status} (attempt {$attempt})\n"; if ($status === 'succeeded') { echo "✅ payment succeeded!\n"; print r($payment); break; } elseif ($status === 'failed') { echo "❌ payment failed \n"; print r($payment); break; } } sleep($intervalsec); } echo "polling completed \n"; } // start polling the payment status pollpaymentstatus($paymenthash); poll all outgoing payments list out all outgoing payments utilizing index offset field lnd method https //lightning engineering/api docs/api/lnd/lightning/list payments/ https //lightning engineering/api docs/api/lnd/lightning/list payments/ // poll payments js – continuously listpayments // // • uses listpayments pagination (index offset) exactly like invoices // • include incomplete=true lets us watch in flight → succeeded/failed // • ideal companion to lncli sendpayment / sendpayment rpcs // const axios = require('axios'); // env const lnd host = process env lnd host; const macaroon hex = process env lnd macaroon; const poll ms = parseint(process env poll ms || '2000', 10); if (!lnd host || !macaroon hex) { console error('❌ set lnd host and lnd macaroon env vars first '); process exit(1); } // axios const lnd = axios create({ baseurl `https //${lnd host} 8080`, headers { 'grpc metadata macaroon' macaroon hex }, timeout 10 000, httpsagent new (require('https') agent)({ rejectunauthorized false }), }); // poll loop let indexoffset = '0'; // start from very first payment console log(`📡 polling payments every ${poll ms} ms …`); const poll = async () => { try { const { data } = await lnd get('/v1/payments', { params { include incomplete true, // show in flight as they happen index offset indexoffset, max payments 100, reversed false, }, }); const { payments = \[], last index offset } = data; if (payments length) { console log(`\n💸 ${payments length} new payment${payments length>1?'s' ''} `); payments foreach(p => { console log(` • #${p payment index tostring() padstart(4)} ` \+ `${p status} sent=${p value sat} sat fee=${p fee sat} sat ` \+ `(hash=${p payment hash slice(0,8)}…)`); }); indexoffset = last index offset; } } catch (err) { console error('❌ poll error ', err message); } finally { settimeout(poll, poll ms); } }; poll();import os, time, requests rest url = os getenv("lnd rest url", "https //localhost 8080") macaroon = os getenv("lnd macaroon") if not (rest url and macaroon) raise environmenterror("please set lnd rest url and lnd macaroon env variables ") url = f"{rest url}/v1/payments" headers = {"grpc metadata macaroon" macaroon} seen count = 0 print("polling for outgoing payment completions ") while true try resp = requests get(url, headers=headers, timeout=5, verify=false) except requests requestexception as e print(f"request error {e}") break if resp status code != 200 print(f"error fetching payments http {resp status code}") break payments = resp json() get("payments", \[]) total = len(payments) if total > seen count \# new payments have been recorded since last check new payments = payments\[seen count ] for p in new payments status = (p get("status") or "") upper() pay hash = p get("payment hash") if status == "succeeded" print(f"payment {pay hash} succeeded 🎉 (amount {p get('value sat')} sat, fee {p get('fee sat')} sat)") elif status == "failed" print(f"payment {pay hash} failed ❌ (failure reason {p get('failure reason')})") else print(f"payment {pay hash} completed with status {status}") seen count = total \# if no new payments, nothing is printed time sleep(5)\<?php require 'vendor/autoload php'; use guzzlehttpclient; $resthost = getenv('rest host') ? 'localhost 8080'; $macaroonpath = getenv('lnd macaroon path') ? '/path/to/admin macaroon'; $macaroonhex = trim(bin2hex(file get contents($macaroonpath))); $client = new client(\[ 'base uri' => "https //{$resthost}", 'verify' => false, ]); try { // include pending payments as well by adding query param if needed (e g ?include incomplete=true) $response = $client >get('/v1/payments?include incomplete=true', \[ 'headers' => \[ 'grpc metadata macaroon' => $macaroonhex ] ]); $paymentsdata = json decode((string)$response >getbody(), true); $payments = $paymentsdata\['payments'] ?? \[]; echo "found " count($payments) " payments \n"; // example print each payment's hash and status foreach ($payments as $p) { echo " payment {$p\['payment hash']} status {$p\['status']}\n"; } } catch (\exception $e) { echo "error listing payments {$e >getmessage()}\n"; }