---
title: REST
slug: rest
docTags: 
createdAt: 2025-07-14T19:04:55.736Z
---

# 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](https://lightning.engineering/api-docs/api/lnd/#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/)

:::CodeblockTabs
```javascript
// 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();
```

```python
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
<?php
require 'vendor/autoload.php';  // Ensure Guzzle is installed via Composer

use GuzzleHttp\Client;
use GuzzleHttp\Exception\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/)

:::CodeblockTabs
```javascript
// 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();

```

```python
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
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$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/)

:::CodeblockTabs
```javascript
// 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();
```

```python
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
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\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/)

:::CodeblockTabs
```javascript
// 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();
```

```python
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
<?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,
]);

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";
}
```
:::

