---
title: Migrating to Node Backed Payments
slug: migrating-to-node-backed-payments
docTags: 
createdAt: 2026-03-06T17:48:45.705Z
---

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

## Prerequisites

- An existing **Voltage LND node** (running and synced)
- An **Enterprise plan** (node-backed setup requires Enterprise)
- You've read [Migration Guide](docId:7szaYmJG8lKjCmIWjkuB-) (auth changes, concept mapping, status mapping, amount handling)

## Migration Steps

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

Follow the [Node-backed Setup](docId\:SmmI0_UkGfGdcHtSZtlU5) 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`

## Step 2: Confirm Node Operations Ownership

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](docId\:rLDa4beEj5rlBld3JM2ql) security guide.

## Step 3: Set Up Webhooks

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:**

```bash
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&#x20;**`shared_secret` from the response — it's only returned once.

**4. Implement signature verification** in your webhook handler:

```javascript
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:**

```bash
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](docId\:PdeH_4ET9AqAWca5q91nA) guide.

## Step 4: Migrate Sending

### Lightning Payment

**Before (LND REST):**

```bash
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):**

```bash
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

### On-chain Payment

**Before (LND REST):**

```bash
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):**

```bash
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)

## Step 5: Migrate Receiving

### Lightning Invoice

**Before (LND REST):**

```bash
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:**

```bash
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:**

```bash
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

### On-chain Receive

**Before (LND REST):**

```bash
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):**

```bash
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`.

## Step 6: Implement Reconciliation Loop

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](docId\:PdeH_4ET9AqAWca5q91nA) 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.

## Step 7: Parallel Run

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

## Step 8: Cut Over

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](docId:7szaYmJG8lKjCmIWjkuB-))
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

## What You Still Manage

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
