Skip to content

Developer Guide

Migrating to Node Backed Payments

Keep your Voltage LND node. Route payments through the Voltage Payments API instead of calling LND directly.

  • An existing Voltage LND node (running and synced)
  • An Enterprise plan (node-backed setup requires Enterprise)
  • You’ve read Migration Guide (auth changes, concept mapping, status mapping, amount handling)
  1. Set up Payments environment and wallet
  2. Confirm node operations ownership
  3. Set up webhooks
  4. Migrate sending
  5. Migrate receiving
  6. Implement reconciliation loop
  7. Parallel run
  8. Cut over

Step 1: Set Up Payments Environment and Wallet

Section titled “Step 1: Set Up Payments Environment and Wallet”

Follow the Node-backed Setup guide to:

  1. Create a Payments environment in the Voltage dashboard
  2. Create a wallet linked to your existing node (check “I want to use a team infrastructure node” and select your node)
  3. Generate an API key for the environment

After this step you’ll have three values you need for every API call:

  • organization_id
  • environment_id
  • wallet_id

And one credential:

  • Your x-api-key

Before migration, confirm who owns liquidity operations, channel changes, credential custody, and escalation for the node.

  1. Review the current node access model → operating owner → approved workflow
  2. Click the least-privilege credential approved for the workflow
  3. Record the credential owner, scope, storage location, and removal plan.

Limit access to the actions approved for the operating owner — review and remove the access when it is no longer needed.

For details on macaroon types and security, see the Node Security security guide.

Replace your gRPC stream listeners with webhooks.

1. Create a publicly accessible HTTPS endpoint on your server to receive webhook POST requests.

2. Register the webhook:

Terminal window
curl "https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/webhooks" \
--request POST \
--header "Content-Type: application/json" \
--header "x-api-key: your-api-key" \
--data '{
"id": "b0fc9829-f139-4035-bb14-4a4b6cd58f0e",
"organization_id": "{organization_id}",
"environment_id": "{environment_id}",
"url": "https://your-domain.com/webhook",
"name": "Migration Webhook",
"events": [
{ "send": "succeeded" },
{ "send": "failed" },
{ "receive": "generated" },
{ "receive": "completed" },
{ "receive": "expired" },
{ "receive": "failed" }
]
}'

3. Save the shared_secret from the response — it’s only returned once.

4. Implement signature verification in your webhook handler:

const crypto = require("crypto");
function verifyWebhookSignature(payload, signature, timestamp, sharedSecret) {
const message = `${payload}.${timestamp}`;
const hmac = crypto.createHmac("sha256", sharedSecret);
hmac.update(message);
const expectedSignature = hmac.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}

Read x-voltage-signature, x-voltage-timestamp, and x-voltage-event from the request headers.

5. Test the webhook:

Terminal window
curl "https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/webhooks/{webhook_id}/test" \
--request POST \
--header "Content-Type: application/json" \
--header "x-api-key: your-api-key" \
--data '{
"delivery_id": "123e4567-e89b-12d3-a456-426614174002",
"payload": {
"type": "test",
"detail": {
"event": "created",
"data": "test"
}
}
}'

For full webhook documentation, see the Webhooks guide.

Before (LND REST):

Terminal window
curl --cacert tls.cert \
--header "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 admin.macaroon)" \
https://your-node.voltage.cloud:8080/v2/router/send \
--request POST \
--data '{
"payment_request": "lntbs1500n1p...",
"timeout_seconds": 60,
"fee_limit_msat": 1000,
"no_inflight_updates": true
}'

After (Payments API):

Terminal window
curl 'https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/payments' \
--request POST \
--header 'x-api-key: your-api-key' \
--header 'Content-Type: application/json' \
--data '{
"id": "68d00852-8dd8-4c71-94d2-91c84695da78",
"wallet_id": "{wallet_id}",
"currency": "btc",
"type": "bolt11",
"data": {
"payment_request": "lntbs1500n1p...",
"amount": {
"currency": "btc",
"amount": 150000,
"unit": "msats"
},
"max_fee": {
"currency": "btc",
"amount": 1000,
"unit": "msats"
}
}
}'

Key differences:

  • You generate the id (UUID) — this is your idempotency key
  • Amount uses the Amount object (msats), not a bare integer
  • max_fee replaces fee_limit_msat — same concept, structured format
  • No timeout_seconds — the API manages routing timeouts
  • Response is 202; monitor status via GET /payments/{payment_id} or webhooks

Before (LND REST):

Terminal window
curl --cacert tls.cert \
--header "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 admin.macaroon)" \
https://your-node.voltage.cloud:8080/v1/transactions \
--request POST \
--data '{
"addr": "tb1pzkhtj4ld86g9c49du5yagnncfrm0s489t76vmrwmt2ecxfnf7spsvjte49",
"amount": 15000,
"sat_per_vbyte": 10
}'

After (Payments API):

Terminal window
curl 'https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/payments' \
--request POST \
--header 'x-api-key: your-api-key' \
--header 'Content-Type: application/json' \
--data '{
"id": "2ec1e783-19b4-4c10-8181-66336a6232bd",
"wallet_id": "{wallet_id}",
"currency": "btc",
"type": "onchain",
"data": {
"address": "tb1pzkhtj4ld86g9c49du5yagnncfrm0s489t76vmrwmt2ecxfnf7spsvjte49",
"amount": {
"currency": "btc",
"amount": 15000000,
"unit": "msats"
},
"max_fee": {
"currency": "btc",
"amount": 1000000,
"unit": "msats"
},
"description": "test payment"
}
}'

Key differences:

  • addr → data.address
  • amount in sats → data.amount in msats (15,000 sats = 15,000,000 msats)
  • sat_per_vbyte → data.max_fee (max fee budget, not fee rate)

Before (LND REST):

Terminal window
curl --cacert tls.cert \
--header "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 admin.macaroon)" \
https://your-node.voltage.cloud:8080/v1/invoices \
--request POST \
--data '{
"value_msat": 150000,
"memo": "test payment",
"expiry": 3600
}'

After (Payments API) — Step 1: Create the receive payment:

Terminal window
curl 'https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/payments' \
--request POST \
--header 'x-api-key: your-api-key' \
--header 'Content-Type: application/json' \
--data '{
"id": "11ca843c-bdaa-44b6-965a-39ac550fcef7",
"wallet_id": "{wallet_id}",
"currency": "btc",
"payment_kind": "bolt11",
"amount": {
"amount": 150000,
"currency": "btc",
"unit": "msats"
},
"description": "test payment",
"expiration": 3600
}'

After — Step 2: Fetch the payment to get the invoice string:

Terminal window
curl 'https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/payments/11ca843c-bdaa-44b6-965a-39ac550fcef7' \
--header 'x-api-key: your-api-key'

The data.payment_request field in the response contains the BOLT11 invoice string to give to the payer.

Key differences:

  • Receiving is a two-step flow: create (202) → fetch (200 with invoice)
  • Send uses type; receive uses payment_kind — same values (bolt11, onchain, bip21)
  • value_msat → amount object
  • memo → description in the request (returned as memo in the response)
  • LND returns the invoice immediately; Payments API returns 202 and you fetch separately

Before (LND REST):

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

After (Payments API):

Terminal window
curl 'https://voltageapi.com/v1/organizations/{organization_id}/environments/{environment_id}/payments' \
--request POST \
--header 'x-api-key: your-api-key' \
--header 'Content-Type: application/json' \
--data '{
"id": "11ca843c-bdaa-44b6-965a-39ac550fcef8",
"wallet_id": "{wallet_id}",
"currency": "btc",
"payment_kind": "onchain",
"amount": {
"amount": 150000000,
"currency": "btc",
"unit": "msats"
},
"description": "on-chain deposit"
}'

Fetch the payment to get data.address. The API tracks confirmations via data.receipts.

Set up a cron job (every 1–5 minutes) as a safety net alongside your webhook handler. The full implementation is documented in the Webhooks guide under “Reconciliation Loop.”

In short:

  1. Store a last_reconciled_at timestamp
  2. Query GET /payments?start_date={last_reconciled_at - 10 min}&end_date={now}&sort_key=updated_at&sort_order=ASC
  3. Page through all results
  4. Upsert each payment into your database keyed on payment.id
  5. Advance last_reconciled_at

The overlap window and idempotent upserts make this safe to run as frequently as you want.

Run both your existing LND integration and the new Payments API integration side by side. Verify that the new path works correctly before cutting over.

Verification checklist:

  • Send a Lightning payment via the Payments API → status reaches completed
  • Receive a Lightning payment via the Payments API → invoice generated, payment completed
  • Send an on-chain payment via the Payments API → status reaches completed
  • Receive an on-chain payment via the Payments API → address generated, payment completed
  • Webhooks fire for all send and receive events
  • Reconciliation loop catches all payments (cross-reference with webhook-delivered payments)
  • Wallet balance in Payments API matches expected values

Once your parallel run is verified:

  1. Disable your old LND integration — stop calling LND REST/gRPC endpoints for payments
  2. Remove old code — TLS cert loading, macaroon injection, gRPC client setup, stream reconnection logic (see “What You Can Remove” in the Migration Guide)
  3. Keep node management access — you still need ThunderHub and/or lncli for:
    • Channel opens and closes
    • Liquidity and treasury management
    • Node unlocking after restarts or updates
    • Seed phrase and SCB backup access

Even after migrating to the Payments API, the following remain your responsibility on a node-backed setup:

  • Node uptime — your node must stay running and synced
  • Channels — open, close, and rebalance as needed (Voltage assists via the approved operating workflow)
  • Liquidity / treasury — keep capacity above minimum thresholds per your treasury management strategy with Voltage
  • Seed phrase + SCB backups — maintain secure offline copies
  • On-chain deposits — fund your node when Voltage notifies you that capacity is below the minimum
  • Node unlocking — unlock with your password after every restart or update