Main Menu
...
Monitoring
Websockets
7 min
monitoring with lnd's websockets lnd’s websocket interface provides real time streaming for specific endpoints, typically those with "subscribe" in their names (e g , subscribeinvoices, subscribechannelevents) not every api method supports websockets — only those designed for synchronous, long lived event streams websocket support is especially useful in environments where low latency updates are needed, such as dashboards, mobile apps, or browser based monitoring tools you can open a persistent websocket connection to receive updates as they happen, without the need for polling also note that in the official lnd documentation websocket examples are at the bottom of the code samples for"rest", they do not have their own tab example subscribe to single invoice listens for all updates on a single invoice identified by it's url safe base64 hash lnd method https //lightning engineering/api docs/api/lnd/invoices/subscribe single invoice/ https //lightning engineering/api docs/api/lnd/invoices/subscribe single invoice/ // subscribe to a single invoice over websocket const websocket = require('ws'); // configuration const lnd host = process env lnd host; // e g "your node voltageapp io" const r hash = process env r hash; // base64 encoded r hash of the invoice const macaroon hex = process env macaroon hex; // hex encoded admin/read macaroon if (!lnd host || !r hash || !macaroon hex) { console error('❌ please set lnd host, r hash, and macaroon hex environment variables '); process exit(1); } // convert base64 to url safe base64 const tourlsafebase64 = (b64) => b64 replace(/\\+/g, ' ') replace(/\\//g, ' '); const urlsafehash = tourlsafebase64(r hash); // websocket connection // for streaming rpcs over rest, lnd expects the `method=get` query param // ( 8080 is the default rest port for lnd’s tls proxy ) const wsurl = `wss\ //${lnd host} 8080/v2/invoices/subscribe/${urlsafehash}?method=get`; console log(`📡 connecting to ${wsurl} …`); const ws = new websocket(wsurl, { rejectunauthorized false, // set to true if you’re using a trusted tls cert headers { 'grpc metadata macaroon' macaroon hex, }, }); ws on('open', () => { console log('✅ websocket connected waiting for invoice updates…'); }); ws on('message', (data) => { try { console log('\n📥 raw invoice message received '); console log(data); // the rest proxy wraps each message in { result \<invoice> } const payload = json parse(data); const inv = payload result ?? payload; // fallback if already flat console log('\n📦 parsed invoice '); console dir(inv, { depth null }); console log(` state ${inv state}`); console log(` amount paid ${inv amt paid sat} sats`); console log(` settled ${inv settled}`); if (inv settled) { console log('✅ invoice has been settled! closing websocket…'); ws close(); } } catch (err) { console error('❌ failed to parse invoice update ', err message); } }); ws on('error', (err) => { console error('❌ websocket error ', err message); }); ws on('close', () => { console log('🔌 websocket connection closed '); });import os, base64, json from websocket import websocketapp rest url = os getenv("lnd rest url", "https //localhost 8080") macaroon = os getenv("lnd macaroon") invoice hash hex = os getenv("lnd invoice 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 ") \# convert invoice hash to url safe base64 for the path invoice hash bytes = bytes fromhex(invoice hash hex) invoice hash b64 = base64 urlsafe b64encode(invoice hash bytes) rstrip(b"=") decode() ws url = rest url replace("https //", "wss\ //") replace("http //", "ws\ //") # use websocket scheme ws url += f"/v2/invoices/subscribe/{invoice hash b64}" \# define websocket event handlers def on open(ws) print(f"✅ websocket connected for invoice {invoice hash hex\[ 8]}…") def on message(ws, message) \# lnd sends invoice updates as json strings print(f"invoice update received {message}") \# optionally, parse the json \# data = json loads(message) \# print(f"invoice state {data get('state')}, settled={data get('settled')}") def on error(ws, error) print(f"websocket error {error}") def on close(ws, status code, msg) print("websocket connection closed ") \# open the websocket connection ws = websocketapp(ws url, header=\[f"grpc metadata macaroon {macaroon}"], on open=on open, on message=on message, on error=on error, on close=on close) \# note if using a self signed cert, you may disable ssl verification with sslopt in run forever ws run forever() # blocks and listens for invoice updates\<?php require 'vendor/autoload php'; use websocket\client; $lnd host = getenv('lnd host') ? 'localhost 8080'; $macaroon hex = bin2hex(file get contents(getenv('lnd macaroon path'))); $r hash = getenv('r hash base64'); // base64 encoded payment hash of invoice // url safe base64 function urlsafebase64($base64) { return str replace(\['+', '/'], \[' ', ' '], $base64); } $urlsafehash = urlsafebase64($r hash); // create websocket connection $wsurl = "wss\ //{$lnd host}/v2/invoices/subscribe/{$urlsafehash}?method=get"; $headers = \["grpc metadata macaroon {$macaroon hex}"]; $client = new client($wsurl, \[ 'headers' => $headers, 'timeout' => 600, 'ssl' => \['verify peer' => false, 'verify peer name' => false] ]); echo "📡 connected to invoice subscription websocket \n"; try { while ($msg = $client >receive()) { $invoice = json decode($msg, true)\['result'] ?? json decode($msg, true); echo "📥 invoice update received \n"; echo " state {$invoice\['state']}\n"; echo " amount paid {$invoice\['amt paid sat']} sats\n"; echo " settled " ($invoice\['settled'] ? 'yes' 'no') "\n"; if ($invoice\['settled']) { echo "✅ invoice settled! closing websocket \n"; break; } } } catch (exception $e) { echo "error {$e >getmessage()}\n"; } $client >close(); echo "🔌 websocket closed \n"; subscribe to set of invoices listens for all new and settled invoices starting at a fixed starting point ( add index = 0 , settle index = 0 ) add index — “new invoice offset” every time an invoice is created (added) lnd assigns it the next add index (0, 1, 2 …) if you pass add index = n when you subscribe, lnd first replays every invoice whose add index is greater than n —letting you catch up on any invoices that were created while you were offline settle index — “settled invoice offset” similarly, each time an invoice becomes settled (paid) it gets the next settle index supplying settle index = m makes lnd replay all settlement events with a settle index greater than m , so you don’t miss any payments that arrived while you were disconnected run this example and then create or pay an invoice on your node and you should see the message come in immediately lnd method https //lightning engineering/api docs/api/lnd/lightning/subscribe invoices/ https //lightning engineering/api docs/api/lnd/lightning/subscribe invoices/ // subscribe to a set of invoices over websocket const websocket = require('ws'); // configuration const lnd host = process env lnd host; const macaroon hex = process env macaroon hex; if (!lnd host || !macaroon hex) { console error('❌ please set both lnd host and macaroon hex environment variables '); process exit(1); } // hard‑coded subscription indices (start at the very beginning) const add index = '0'; const settle index = '0'; // websocket connection // the rest proxy for streaming rpcs requires the `method=get` query param const wsurl = `wss\ //${lnd host} 8080/v1/invoices/subscribe?method=get`; console log(`📡 connecting to ${wsurl} …`); const ws = new websocket(wsurl, { rejectunauthorized false, // set to true if you have a trusted tls cert headers { 'grpc metadata macaroon' macaroon hex, }, }); ws on('open', () => { console log('✅ websocket connected subscribing…'); // send subscription request with fixed indices ws send(json stringify({ add index add index, settle index settle index })); }); ws on('message', (data) => { try { // the rest proxy wraps each message in { result \<invoice> } const payload = json parse(data); const inv = payload result ?? payload; // fallback if already flat console log('\n📦 invoice update received '); console log(` • hash (r hash) ${inv r hash}`); console log(` • state ${inv state}`); console log(` • amount paid ${inv amt paid sat} sats`); console log(` • settled ${inv settled}`); } catch (err) { console error('❌ failed to parse message ', err message); console log('raw data ', data tostring()); } }); ws on('error', (err) => { console error('❌ websocket error ', err message); }); ws on('close', (code, reason) => { console log(`🔌 websocket closed (${code}) ${reason || ''}`); });import os, base64, threading from websocket import websocketapp rest url = os getenv("lnd rest url", "https //localhost 8080") macaroon = os getenv("lnd macaroon") invoice hashes = os getenv("lnd invoice hashes") # comma separated hex hashes for invoices if not (rest url and macaroon and invoice hashes) raise environmenterror("please set lnd rest url, lnd macaroon, and lnd invoice hashes env variables ") invoice list = \[h strip() for h in invoice hashes split(",") if h strip()] ws base = rest url replace("https //", "wss\ //") replace("http //", "ws\ //") + "/v2/invoices/subscribe/" def make handlers(invoice hex) short id = invoice hex\[ 8] # short identifier for logs inv bytes = bytes fromhex(invoice hex) inv b64 = base64 urlsafe b64encode(inv bytes) rstrip(b"=") decode() url = ws base + inv b64 def on open(ws) print(f"🔗 opened ws for invoice {short id}…") def on message(ws, message) print(f"\[invoice {short id}…] update {message}") def on error(ws, error) print(f"\[invoice {short id}…] ws error {error}") def on close(ws, status code, msg) print(f"\[invoice {short id}…] ws closed ") \# return a websocketapp instance for this invoice return websocketapp(url, header=\[f"grpc metadata macaroon {macaroon}"], on open=on open, on message=on message, on error=on error, on close=on close) \# launch a websocket for each invoice in separate threads threads = \[] for inv hex in invoice list ws app = make handlers(inv hex) t = threading thread(target=ws app run forever, daemon=true) threads append(t) t start() time str = "" # (if needed, import time at top; here we assume it's imported elsewhere in this context ) \# keep the main thread alive to allow background threads to run print(f"subscribed to {len(invoice list)} invoices via websockets waiting for updates ") try import time while true time sleep(1) except keyboardinterrupt print("exiting invoice subscription monitor ")\<?php require 'vendor/autoload php'; use websocket\client; $resthost = getenv('rest host') ? 'localhost 8080'; $macaroonpath = getenv('lnd macaroon path') ? '/path/to/admin macaroon'; // prepare the macaroon for header $macaroonhex = trim(bin2hex(file get contents($macaroonpath))); // connect to lnd's subscribeinvoices via websocket (wss) $wsurl = "wss\ //{$resthost}/v1/invoices/subscribe?method=get"; $client = new client($wsurl, \[ 'stream context' => \[ 'ssl' => \[ 'verify peer' => false, // accept self signed cert for demo 'verify peer name' => false ] ], 'headers' => \[ 'grpc metadata macaroon' => $macaroonhex ] ]); // optionally, specify indices to start from (to avoid missing events) // here we send an empty subscription request (to get events from now on) $requestbody = \['add index' => 0, 'settle index' => 0]; $client >send(json encode($requestbody)); echo "subscribed to invoice updates, waiting for events \n"; // continuously read messages (this loop runs indefinitely until the socket closes) while (true) { try { $message = $client >receive(); } catch (\websocket\connectionexception $e) { echo "websocket error or connection closed {$e >getmessage()}\n"; break; } if ($message === null) { // no more messages or stream ended break; } // parse and handle the invoice update $invoiceupdate = json decode($message, true); echo "📥 invoice update received add index={$invoiceupdate\['add index']}, settled=" ($invoiceupdate\['settled'] ? 'yes' 'no') "\n"; } subscribe to single payment listens for all updates on a single invoice 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/ // track a single payment over websocket const websocket = require('ws'); // configuration const lnd host = process env lnd host; // e g "your node voltageapp io" const payment hash = process env payment hash; // hex or base64 payment hash const macaroon hex = process env macaroon hex; // admin macaroon in hex if (!lnd host || !payment hash || !macaroon hex) { console error('❌ please set lnd host, payment hash, and macaroon hex env vars '); process exit(1); } // build ws url trackpaymentv2 lives under /v2/router/track/{payment hash} const wsurl = `wss\ //${lnd host} 8080/v2/router/track/${payment hash}?method=get`; console log(`📡 connecting to ${wsurl} …`); const ws = new websocket(wsurl, { rejectunauthorized false, // set true if you use a trusted tls cert headers { 'grpc metadata macaroon' macaroon hex }, }); ws on('open', () => { console log('✅ websocket connected subscribing…'); // for ws, the rest proxy expects an initial json body // (even though the hash is already in the path) ws send(json stringify({ payment hash payment hash, no inflight updates false, // change to true for “only final outcome” })); }); ws on('message', (data) => { try { const payload = json parse(data); const pay = payload result ?? payload; // unwrap { result … } if present console log('\n💸 payment update '); console log(` • status ${pay status}`); // in flight | succeeded | failed console log(` • fee (sat) ${pay fee sat}`); console log(` • paid (sat) ${pay value sat}`); console log(` • attempts ${pay htlcs? length ?? 0}`); if (pay status === 'succeeded' || pay status === 'failed') { console log(`🏁 payment ${pay status === 'succeeded' ? 'succeeded' 'failed'} `); ws close(); } } catch (err) { console error('❌ failed to parse message ', err message); console log('raw data ', data tostring()); } }); ws on('error', (err) => console error('❌ websocket error ', err message)); ws on('close', (code, r) => console log(`🔌 websocket closed (${code}) ${r || ''}`));import os, base64 from websocket import websocketapp rest url = os getenv("lnd rest url", "https //localhost 8080") macaroon = os getenv("lnd macaroon") payment hash hex = os getenv("lnd payment hash") if not (rest url and macaroon and payment hash hex) raise environmenterror("please set lnd rest url, lnd macaroon, and lnd payment hash env variables ") \# convert payment hash to url safe base64 pay hash bytes = bytes fromhex(payment hash hex) pay hash b64 = base64 urlsafe b64encode(pay hash bytes) rstrip(b"=") decode() ws url = rest url replace("https //", "wss\ //") replace("http //", "ws\ //") ws url += f"/v2/router/track/{pay hash b64}" def on open(ws) print(f"✅ websocket connected for payment {payment hash hex\[ 8]}…") def on message(ws, message) print(f"payment update received {message}") def on error(ws, error) print(f"websocket error {error}") def on close(ws, status code, msg) print("websocket closed ") ws = websocketapp(ws url, header=\[f"grpc metadata macaroon {macaroon}"], on open=on open, on message=on message, on error=on error, on close=on close) ws run forever()\<?php require 'vendor/autoload php'; use websocket\client; $resthost = getenv('rest host') ? 'localhost 8080'; $macaroonpath = getenv('lnd macaroon path') ? '/path/to/admin macaroon'; $paymenthashhex = getenv('payment hash hex') ? '\<payment hash in hex>'; // payment hash to track (hex) // read macaroon and connect to the trackpayment websocket endpoint $macaroonhex = trim(bin2hex(file get contents($macaroonpath))); $wsurl = "wss\ //{$resthost}/v2/router/track/{$paymenthashhex}?method=get"; $client = new client($wsurl, \[ 'stream context' => \[ 'ssl' => \[ 'verify peer' => false, 'verify peer name' => false ] ], 'headers' => \[ 'grpc metadata macaroon' => $macaroonhex ] ]); // send initial request body (here we can disable inflight updates if we only want final result) $requestbody = \[ 'payment hash' => $paymenthashhex, 'no inflight updates' => false ]; $client >send(json encode($requestbody)); echo "tracking payment {$paymenthashhex} \n"; // read streaming payment updates while (true) { try { $message = $client >receive(); } catch (\websocket\connectionexception $e) { echo "connection closed or error {$e >getmessage()}\n"; break; } if (!$message) { break; } $paymentupdate = json decode($message, true); $status = $paymentupdate\['status'] ?? '(unknown)'; echo "💸 payment update status={$status}"; if ($status === 'succeeded') { // if succeeded, you can retrieve the preimage and other details echo ", preimage={$paymentupdate\['payment preimage']}\n"; echo "✅ payment succeeded, stopping tracking \n"; break; } elseif ($status === 'failed') { echo "\n❌ payment failed, stopping tracking \n"; break; } else { echo " (in progress)\n"; // continue looping for more updates } } subscribe to all outgoing payments tracks all new outgoing payments start this script (ideally before sending any new payments) use lncli sendpayment / sendpayment from your node or an app that routes through it watch each payment move through in flight ➜ succeeded/failed in real time, complete with fee and attempt details lnd method https //lightning engineering/api docs/api/lnd/router/track payments/ https //lightning engineering/api docs/api/lnd/router/track payments/ // track all in flight payments over websocket const websocket = require('ws'); // configuration const lnd host = process env lnd host; // e g "your node voltageapp io" const macaroon hex = process env macaroon hex; // admin macaroon in hex const only finals = process env only finals === 'true'; // optional toggle if (!lnd host || !macaroon hex) { console error('❌ please set lnd host and macaroon hex env vars '); process exit(1); } // build ws url — trackpayments lives at /v2/router/payments const wsurl = `wss\ //${lnd host} 8080/v2/router/payments?method=get`; console log(`📡 connecting to ${wsurl} …`); const ws = new websocket(wsurl, { rejectunauthorized false, // set true if you use a trusted tls cert headers { 'grpc metadata macaroon' macaroon hex }, }); ws on('open', () => { console log('✅ websocket connected subscribing…'); // initial body (required by rest proxy even though there’s no path param) ws send(json stringify({ no inflight updates only finals, // false = full progress, true = finals only })); }); ws on('message', (data) => { try { const payload = json parse(data); const pay = payload result ?? payload; // unwrap { result … } if needed console log('\n💸 payment event '); console log(` • hash ${pay payment hash}`); console log(` • status ${pay status}`); // in flight | succeeded | failed console log(` • sent (sat) ${pay value sat}`); console log(` • fee (sat) ${pay fee sat}`); console log(` • attempts ${pay htlcs? length ?? 0}`); // optionally close when payment reaches a terminal state if (pay status === 'succeeded' || pay status === 'failed') { console log(`🏁 payment ${pay status tolowercase()} `); // don’t close the socket — other payments might still be streaming! } } catch (err) { console error('❌ failed to parse message ', err message); console log('raw data ', data tostring()); } }); ws on('error', (err) => console error('❌ websocket error ', err message)); ws on('close', (code,r) => console log(`🔌 websocket closed (${code}) ${r||''}`)); import os from websocket import websocketapp 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 ") ws url = rest url replace("https //", "wss\ //") replace("http //", "ws\ //") + "/v2/router/payments" def on open(ws) print("✅ websocket connected for all payment subscription ") def on message(ws, message) print(f"outgoing payment event {message}") def on error(ws, error) print(f"websocket error {error}") def on close(ws, status code, msg) print("websocket closed ") ws = websocketapp(ws url, header=\[f"grpc metadata macaroon {macaroon}"], on open=on open, on message=on message, on error=on error, on close=on close) ws run forever()\<?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, ]); // poll for payments periodically function pollallpayments(int $intervalsec = 5) { global $client, $macaroonhex; while (true) { try { $response = $client >get('/v1/payments?include incomplete=true', \[ 'headers' => \[ 'grpc metadata macaroon' => $macaroonhex ] ]); $paymentsdata = json decode((string)$response >getbody(), true); $payments = $paymentsdata\['payments'] ?? \[]; echo "payments snapshot at " date('y m d h\ i s') " \n"; foreach ($payments as $payment) { echo " hash {$payment\['payment hash']} status {$payment\['status']}\n"; } } catch (\exception $e) { echo "polling error {$e >getmessage()}\n"; } sleep($intervalsec); } } pollallpayments();