Websockets
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
// 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.');
});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.
<?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
<?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.
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$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();