Skip to content

Developer Guide

Migration Guide

You’re currently integrating directly with LND — via gRPC, REST, or lncli — using TLS certificates and macaroons for authentication. You want to move to the Voltage Payments API, which gives you a single REST API for sending and receiving payments without managing gRPC stubs, protobuf definitions, macaroon files, or route-finding logic. This guide covers what changes regardless of your target setup, then links you to the step-by-step path that fits your needs.

Before diving into the how, here’s what you gain by moving from direct LND to the Payments API.

With direct LND, your node has a single on-chain wallet and a single pool of channel liquidity. If you serve multiple customers, products, or use cases, you need to build a double-entry ledger system on top of LND to track who owns what. This is complex, error-prone, and a significant engineering investment.

The Payments API gives you wallets — each with its own tracked balance — all backed by a single node. Fund segregation is handled for you at the API layer. You can create as many wallets as you need per environment without building or maintaining your own accounting system.

LND doesn’t have native webhooks. To know when a payment arrives or completes, you either poll or hold open a gRPC stream (SubscribeInvoices, SubscribeChannelEvents) and manage reconnection logic yourself.

The Payments API delivers real-time webhook notifications — HTTP POST requests to your endpoint for every payment event. Paired with a lightweight reconciliation loop as a safety net, this gives you reliable, event-driven payment monitoring without managing persistent connections.

When you use LND directly, every payment forces you to think about the underlying plumbing: channel balances, inbound vs. outbound liquidity, UTXO management, fee estimation, route finding. Your application code is tightly coupled to node operations.

The Payments API separates payment logic from liquidity management. Your application talks to wallets and payments — clean balances, simple send/receive calls, payments that just work. The LND node operates underneath as a liquidity reserve, but those concerns are isolated. You (and Voltage, via the Support Macaroon) manage channels and capacity separately from your payment integration.

With a credit-backed setup, this separation goes even further — there’s no node to manage at all. With a node-backed setup, you still handle liquidity, but your payment code never has to know about it.

There are two ways to use the Voltage Payments API. Choose based on whether you want to keep running your own node.

Node-backedCredit-backed
Plan requiredEnterpriseEnterprise + Line of Credit approval
InfrastructureYou keep your Voltage LND nodeNo node — Voltage handles it
FundingYour node’s on-chain + channel balanceLine of Credit (collateral-backed)
Channel managementYou + Voltage (via Support Macaroon)Not needed
LiquidityYou manage treasury; Voltage assistsNot needed
USD supportBTC onlyBTC wallets or USD reconciliation
Best forTeams that need full node control, existing channel relationships, or custom routingTeams that want zero infrastructure overhead and/or USD-denominated payments

Ready to start?


The sections below apply to both migration paths. Read through them before starting your step-by-step guide.

Before (Direct LND)After (Payments API)
TransportTLS (self-signed or CA-signed cert)HTTPS (standard TLS, no cert file needed)
CredentialMacaroon (admin, invoice, readonly, or custom-baked)x-api-key header
Credential formatBinary or hex-encoded fileString token
ScopePer-macaroon permission caveatsPer-environment API key
Rotationlncli bakemacaroon or ChangePassword RPCGenerate new key in dashboard

Before:

Terminal window
curl --cacert tls.cert \
--header "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 admin.macaroon)" \
https://your-node.voltage.cloud:8080/v1/balance/channels

After:

Terminal window
curl 'https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/payments' \
--header 'x-api-key: your-api-key'

This table maps every common LND operation to its Payments API equivalent.

LND MethodLND REST PathPayments API EquivalentNotes
AddInvoicePOST /v1/invoicesPOST /payments with payment_kindUse payment_kind: "bolt11" for Lightning. Response is 202; fetch with GET to retrieve the invoice string.
SendPaymentV2POST /v2/router/sendPOST /payments with type: "bolt11"No route hints or fee limits to manage beyond max_fee. Status via GET or webhooks instead of streaming.
DecodePayReqGET /v1/payreq/{pay_req}Not applicableThe Payments API handles invoice parsing internally. If you need to inspect an invoice before paying, decode it client-side or use a library.
NewAddressPOST /v1/newaddressPOST /payments with payment_kind: "onchain" (receive)Creates a receive payment that includes a Bitcoin address in the response.
SendCoinsPOST /v1/transactionsPOST /payments with type: "onchain"Amount in msats via the Amount object, not satoshis.
SubscribeInvoicesGET /v1/invoices/subscribeWebhooks + reconciliation loopSee “The Big Shift” below. Subscribe to receive.completed, receive.expired, etc.
LookupInvoiceGET /v1/invoice/{r_hash}GET /payments/{payment_id}Look up by your payment_id (UUID), not by payment hash.
ListPaymentsGET /v1/paymentsGET /payments?direction=sendSupports filtering by statuses[], wallet_id, pagination via offset/limit.
ListInvoicesGET /v1/invoicesGET /payments?direction=receiveSame endpoint, different direction filter.
WalletBalanceGET /v1/balance/blockchainGET /wallets/{wallet_id}Returns wallet balance. For node-backed setups you can still check on-chain balance via LND directly.
ChannelBalanceGET /v1/balance/channelsGET /wallets/{wallet_id}The Payments API exposes a single wallet balance. Channel-level detail is available through LND directly (node-backed only).
ListChannelsGET /v1/channelsNot applicableChannel management stays on LND (node-backed) or is not applicable (credit-backed).
OpenChannel / CloseChannelPOST /v1/channels / DELETE /v1/channels/{channel_point}Not applicableSame as above.
GetInfoGET /v1/getinfoNot applicableNode-level info (pubkey, alias, sync status) is an LND concern, not a Payments API concern.

The Big Shift: Streaming → Webhooks + Polling

Section titled “The Big Shift: Streaming → Webhooks + Polling”

This is the biggest architectural change in the migration. LND uses long-lived gRPC streams; the Payments API uses webhooks backed by a reconciliation loop.

How monitoring works today (direct LND):

You open a gRPC stream (SubscribeInvoices, SubscribeChannelEvents, etc.) and process events as they arrive. If the stream drops, you reconnect and may need to replay from an index.

How monitoring works with the Payments API:

  1. Webhooks (primary) — Voltage sends HTTP POST requests to your endpoint for each event (send.succeeded, receive.completed, etc.)
  2. Reconciliation loop (safety net) — A cron job periodically lists payments updated since your last checkpoint, catching anything your webhook handler missed
  3. Ad-hoc polling (optional) — GET /payments/{payment_id} for on-demand status checks

Recommended architecture:

  • A webhook handler that receives events and upserts payments into your database keyed on payment.id
  • A reconciliation cron (every 1–5 minutes) that lists recently updated payments and upserts them the same way

Both paths write to the same table using the same idempotent upsert logic. The webhook gives you real-time speed; the recon loop guarantees completeness.

Before (gRPC streaming)After (Webhooks + Recon)
Connection modelPersistent bidirectional streamStateless HTTP POSTs to your endpoint
Missed eventsMust track stream index and replay on reconnectReconciliation loop catches missed deliveries automatically
LatencySub-second (same connection)Sub-second (webhook) to minutes (reconciliation loop)
ReliabilityDepends on stream stability and your reconnect logicVoltage retries failed deliveries; recon loop as safety net
AuthMacaroon on the streamshared_secret for webhook signature verification; x-api-key for reconciliation polling
“
Event granularityPer-invoice or per-payment updatesPer-event types: send.succeeded, send.failed, receive.generated, receive.completed, receive.expired, receive.failed, etc.

For webhook setup details, signature verification, and reconciliation loop implementation, see the Webhooks guide.

The Payments API uses different status names than LND.

Send payments:

LND StatusPayments API StatusMeaning
IN_FLIGHTsendingPayment is being routed
SUCCEEDEDcompletedPayment delivered
FAILEDfailedRouting failed or timed out

Receive payments:

LND StatusPayments API StatusMeaning
(invoice created)generatingInvoice/address being created
OPEN / ACCEPTEDreceivingInvoice issued, waiting for payment
SETTLEDcompletedPayment received and credited
CANCELEDexpiredInvoice expired unpaid
(error)failedGeneration or receipt failed

LND works in satoshis (and sometimes millisatoshis as separate fields). The Payments API uses an Amount object everywhere:

{
"amount": 150000,
"currency": "btc",
"unit": "msats"
}
  • All BTC amounts use millisatoshis as the base unit
  • USD amounts use cents as the base unit ("currency": "usd", "unit": "cents")
  • Legacy fields amount_msats and amount_sats are accepted but deprecated — use the Amount object

Conversion reference: 1 sat = 1,000 msats. If your LND code sends 15,000 sats, the equivalent Payments API amount is 15000000 msats.

After migrating, you can delete the following from your codebase and infrastructure:

  • TLS certificate management — no cert files to load, rotate, or distribute
  • Macaroon loading and injection — no binary token files; just an API key string
  • gRPC / protobuf setup — no .proto files, code generation, or gRPC client libraries
  • Route finding and path optimization — the Payments API handles routing
  • Invoice decoding — the API parses invoices internally
  • Channel rebalancing logic — handled by Voltage (node-backed with Support Macaroon) or not applicable (credit-backed)
  • Fee estimation — max_fee defaults are sensible; no manual fee rate lookups needed
  • Stream reconnection logic — webhooks replace gRPC streams

Pick your path and follow the step-by-step guide: