REST
5 min
Monitoring via LND's REST interface
LND's REST interface is well documented on 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.
// 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();Polling for all new and settled invoices
List out all new and settled invoices utilizing index_offset field
// 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();
Polling for Single Payment
Polling for a payment made by you from your node identified by it's URL safe base64 hash
// 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();Poll all outgoing payments
List out all outgoing payments utilizing index_offset field
// 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();