Branding guide
Build a reliable Lightning ledger, with payment reconciliation and balanced journal examples.
Payments12 minutes10/15/2026

Lightning Payment Ledger: Balances, Fees, and Reconciliation

Liam West

Build a Lightning payment ledger with balanced entries, reserved funds, precise fees, and reconciliation. Follow worked examples for deposits, withdrawals, and internal transfers.

A Lightning payment can complete before your application records it. A notification can arrive twice. A fee can differ from your estimate.

Your ledger must handle each case without recording the same financial effect twice. It must also show what happened to the money.

This guide explains how to record Lightning payments and check those records against your payment provider.

Consider a payment business that lets customers hold BTC balances and make Lightning deposits, withdrawals, and internal transfers. The examples use a prefunded BTC model. They describe an operational ledger; your finance team must map that ledger to your actual custody and accounting arrangements.

Give each system a clear role

The payment provider, customer ledger, and finance system answer different questions.

SystemQuestion it answersResponsibility
Payment providerWhat happened to the payment?Submit payments and report their results
Customer ledgerWhat does the business owe each customer?Record customer balances, fees, and transfers
Finance systemWhat does the business own, owe, earn, and spend?Record the business's financial position and results

In this example, Voltage provides the payment API. The business maintains the customer ledger.

Voltage's payment records do not replace the business's records of customer obligations. The business must connect each external payment to the correct customer and business transaction.

The finance system then receives records or summaries from the customer ledger. Those summaries must retain enough detail to trace a total back to its original transactions.

Voltage supports different funding models. Choose the correct model before defining the accounts. A credit balance and a balance backed by BTC are not the same asset. See the Voltage implementation models.

Track payment, posting, and reconciliation separately

A completed payment does not prove that your application recorded it. A posted ledger transaction does not prove that you checked it against external records.

Track three things separately:

RecordWhat it tells you
Payment stateThe latest confirmed result from the provider
Posting stateWhether your application recorded the required financial effect
Reconciliation stateWhether that financial effect agrees with the supporting records

Keep posting state on the business event or posting task. An event can be unposted before its ledger transaction exists.

Keep reconciliation results in a separate record. You can update an investigation without changing posted financial entries.

Distinguish provider states from application states

Voltage uses payment states such as sending, receiving, completed, and failed. Use the documented states for the relevant payment flow. If your application uses a label such as settled, define its mapping explicitly. See the Voltage developer guide.

Your application can also record an unknown outcome. This means that it lacks enough evidence to determine what happened.

A timeout does not prove failure. A failed status lookup does not undo a previously confirmed completion.

When an outgoing payment has an unknown outcome:

  • Keep the payment ID and original request.
  • Keep the customer's funds reserved.
  • Query the existing payment.
  • Escalate unresolved cases after your operating deadline.
  • Do not send a replacement payment while the original remains unresolved.

Internal transfers, reservations, and accounting corrections have their own rules. They do not require a completed Lightning payment.

Build transactions that balance

Double-entry bookkeeping requires equal total debits and credits for each transaction in a currency.

A debit does not always mean "money out." A credit does not always mean "money in." Their effects depend on the account type. See these debit and credit rules.

Account typeA debitA credit
AssetIncreases the assetDecreases the asset
LiabilityDecreases the liabilityIncreases the liability
RevenueDecreases revenueIncreases revenue
ExpenseIncreases the expenseDecreases the expense

For example, a customer deposit increases both an asset and a liability. The business holds more BTC and owes the customer more BTC.

Define the accounts

The business uses these accounts for the examples:

AccountTypePurpose
business:btcAssetBTC held within the payment system described here
customer:{id}LiabilityBTC owed to a customer
business:feesRevenueService fees charged to customers
business:routingExpenseLightning routing costs paid by the business

This small account set illustrates the postings. A production system needs accounts that reflect its funding arrangements, treasury movements, and other costs.

Preserve the smallest supported unit

Store money as integers. Keep the currency and unit explicit.

Voltage's BTC amount objects use millisatoshis. One satoshi equals 1,000 millisatoshis. Preserve that precision in your ledger or retain it through an explicit rounding policy. See the Voltage API contract.

For this design, the business stores BTC amounts as integer millisatoshis. The tables display sats to make the examples easier to read.

Group entries into one transaction

A journal transaction groups all the debit and credit entries for one financial event.

Store these identifiers:

FieldPurpose
journal_idIdentifies the complete journal transaction
entry_idIdentifies one debit or credit entry
business_transaction_idIdentifies the deposit, withdrawal, or internal transfer
provider_payment_idLinks an external payment, when applicable
posting_keyPrevents the same financial effect from being posted twice
reverses_journal_idLinks a correction to the transaction it reverses

Each entry also needs an account, direction, currency, integer amount, and posting timestamp.

Internal transfers have a business transaction ID but no Voltage payment ID.

Commit all entries together. Either every entry is saved, or none is saved. Update any stored balance totals in that same database transaction.

Prevent duplicate postings in the database

Checking whether a payment already exists is insufficient. Two workers can both check before either writes.

Use a database uniqueness constraint on the posting key. Include the relevant tenant, environment, source transaction, and financial effect.

A settlement and a correction need different posting keys. Multiple entries within one settlement share a journal transaction.

When a repeated posting request arrives, compare it with the existing transaction. Return the existing result if the details match. Investigate if the amounts, currency, or accounts differ.

Reserve funds before a withdrawal

A customer must not spend the same balance while a withdrawal is pending.

The business distinguishes two balances:

  • Posted balance: the amount currently owed to the customer.
  • Available balance: the posted balance minus active reservations and other applicable restrictions.

In the simplified examples:

Plain text
Available balance = posted balance - active reservations

A reservation reduces what the customer can spend. It does not yet record a completed external payment.

Before dispatch, the business checks the available balance and saves the withdrawal, reservation, payment ID, and durable dispatch task together. Concurrent requests must not reserve the same funds.

On confirmed completion, the business posts the settlement and consumes the reservation in one database transaction.

On confirmed failure, the business releases the reservation when it decides to stop that withdrawal. An application timeout alone must not release the funds.

The existing reliable payout guide explains payment submission, ambiguous responses, and retry handling.

Follow the money through three examples

These examples form one continuous sequence.

Alice starts with 500,000 sats. Bob starts with 0 sats. The opening ledger records 500,000 sats in BTC assets and an equal liability to Alice.

The fee values are illustrative. The examples exclude additional provider processing fees. Record those separately when they apply.

Example 1: Alice deposits 100,000 sats

The business confirms the completed inbound payment and matches it to Alice's deposit request.

It posts:

AccountDebit, satsCredit, sats
business:btc100,000—
customer:alice—100,000
Total100,000100,000

The debit increases the business's BTC asset. The credit increases the amount it owes Alice.

Alice's posted and available balances are now 600,000 sats.

Creating an invoice does not credit Alice. The completed deposit does.

Example 2: Alice withdraws 50,000 sats

The business charges Alice a fixed service fee of 500 sats. It pays the routing cost, which is 200 sats.

First, the business reserves 50,500 sats from Alice's balance.

StagePosted balance, satsReserved, satsAvailable balance, sats
Before withdrawal600,0000600,000
While withdrawal is pending600,00050,500549,500
After successful settlement549,5000549,500

The business must also have enough payment liquidity for the outgoing amount and applicable provider costs.

After it confirms completion, it posts:

AccountDebit, satsCredit, sats
customer:alice50,500—
business:routing200—
business:btc—50,200
business:fees—500
Total50,70050,700

The entries record four effects:

  • The business owes Alice 50,500 sats less.
  • It records a 200-sat routing expense.
  • Its BTC assets decrease by 50,200 sats.
  • It earns 500 sats in service-fee revenue.

The net fee earnings are 300 sats: 500 sats of revenue minus 200 sats of expense.

Because Alice pays a fixed service fee, a different routing cost changes the business's margin. It does not automatically change Alice's charge.

If your business passes routing costs to the customer, define that policy separately. Reserve an approved allowance and settle against the actual charge.

Example 3: Alice transfers 20,000 sats to Bob

The business checks Alice's available balance and authorises the internal transfer.

It posts both entries together:

AccountDebit, satsCredit, sats
customer:alice20,000—
customer:bob—20,000
Total20,00020,000

Alice now has 529,500 sats. Bob has 20,000 sats.

No external payment occurs. The BTC asset balance does not change. The total owed to customers remains 549,500 sats.

This transfer must have its own unique posting key and an atomic balance check, just like an external withdrawal.

Use one posting process for every recovery path

Webhooks, scheduled checks, and manual recovery can discover the same completed payment.

Send each discovery through the same posting function. That function validates the transaction, checks the posting key, and writes the balanced entries.

For Voltage webhooks, verify the signature and timestamp before trusting the payload. Save the incoming work durably before acknowledging it. The documented envelope uses type and detail.event; a successful send event uses succeeded. Keep application-defined event names separate from these API fields. See the Voltage webhook contract.

The business then retrieves the payment, validates its ownership and amounts, and applies the appropriate posting rule.

FailureRequired response
Duplicate notificationUse the posting key to return the existing result without posting again.
Missing notificationFind the payment through reconciliation and use the normal posting function.
Crash before ledger commitRetry the posting. The database must contain either the complete transaction or none of it.
Crash after ledger commitFind the existing journal transaction and return its result.
Unknown outgoing payment resultKeep the reservation. Investigate the existing payment before any replacement attempt.
Actual fee differs from estimateApply the agreed fee policy using the actual amount. Release any unused customer reservation.
Older event arrives after completionPreserve the confirmed result. Investigate conflicting evidence before changing the financial treatment.
Duplicate financial transaction was postedReverse the erroneous duplicate transaction. Retain the legitimate original.

Correct entries without removing history

Do not edit or delete posted financial entries.

To replace an incorrect journal transaction:

  1. Create a new transaction that reverses all its entries.
  2. Link the reversal to the original journal ID.
  3. Create the correct transaction.
  4. Record the reason and approval evidence.

Use new IDs for the reversal and replacement.

Reconcile transactions, balances, and finance totals

Reconciliation compares records and explains differences.

Use frequent operational checks for unresolved payments. Schedule broader checks and financial close procedures according to your volume and operating requirements.

Check external payments

Voltage documents a complete reconciliation sweep using unfiltered cursor pagination, ordered by created_at. Follow every page and repeat the sweep on a schedule. Its date filters select payment creation time, so a "today only" query can miss an older payment that completed today. See the Voltage reconciliation procedure.

Also check unresolved payment IDs directly.

Match external payments to complete journal transactions. Compare the principal and each fee separately. A customer's debit can include a service fee, so it need not equal the amount sent.

Do not require every journal transaction to have a provider payment. Internal transfers and accounting corrections have different supporting evidence.

Likewise, a failed or unfinished provider payment does not require a completed-payment posting.

Check balances with the correct equation

The original customer liability and BTC asset totals can diverge for valid reasons.

After the three examples:

BalanceSats
BTC assets549,800
Liability to Alice529,500
Liability to Bob20,000
Total customer liabilities549,500
Net fee earnings retained in the system300

For this simplified example:

Plain text
549,800 sats of assets
= 549,500 sats owed to customers
+ 300 sats of retained net fee earnings

The 300-sat difference is explained. It is not a posting error.

A production reconciliation must also account for company funding, treasury transfers, other liabilities, expenses, and timing differences.

Check that recorded assets agree with the relevant external balances. Check that the customer accounts agree with the customer-liability control total. Do not substitute one check for the other.

Connect the customer ledger to finance

Reconcile the customer ledger's closing totals with the finance system.

Include opening balances, deposits, withdrawals, fees, corrections, and closing balances. Record the reporting cutoff and account for late postings explicitly.

Keep currencies separate. A balanced BTC ledger does not establish a correct USD valuation.

Assign unresolved differences

Give each unresolved difference an owner, next action, and review deadline.

The investigation record should include:

  • Business transaction and provider payment IDs.
  • Relevant payment evidence.
  • Journal transactions and reservations.
  • Expected and actual amounts, currencies, and fees.
  • Event time, posting time, and reconciliation time.
  • The reason for any correction.

A reconciliation run can finish with documented exceptions. Those exceptions must remain visible until someone resolves them.

Treat USD and credit-backed flows separately

The worked examples use prefunded BTC. Do not extend their asset entries unchanged to another funding model.

For USD conversion, retain both currency amounts, the quote reference, the applied rate, and the relevant fees. Record balanced entries within each currency and connect the conversion records.

For credit-backed payments, distinguish customer liabilities from the amount owed to the credit provider. Record credit use and repayment as separate events.

Repayment reduces the asset used to repay the debt. A USD bank repayment should not reduce a BTC asset account.

Use the relevant Voltage funding model documentation and agree the account mapping with finance before implementation.

Test the ledger before launch

Test the financial results as well as the API calls.

Verify that:

  • Two concurrent withdrawals cannot reserve the same customer funds.
  • A repeated event cannot create another settlement transaction.
  • A failed database transaction cannot leave half a journal transaction.
  • An unknown payment retains its reservation.
  • A missed webhook is recovered through reconciliation.
  • A fractional-satoshi fee retains its full precision.
  • An internal transfer creates no false "missing provider payment" exception.
  • A correction preserves the original entries and an explicit audit trail.

For the withdrawal submission and retry procedure, use How to Design a Reliable Lightning Payout System. For integration fields and payment behaviour, use the Voltage developer documentation.

Talk to our team

Let's plan your business's Lightning Network integration.

Schedule a strategy session