When to show a deposit as pending, when it becomes available, and how to credit it exactly once from payin webhooks. With ledger entries and a test plan.
A neobank turns an ACH or wire deposit into an available balance in two steps: show it as pending when the deposit arrives, and make it available only when it has finished settling. With BlindPay, each deposit to a virtual account creates a payin. Credit pending on payin.new and available on a completed payin.complete, keyed by payin ID.
The previous guide gave each user US account details. This one covers what happens in your ledger when money lands on them. For the full product map, see how to build a neobank.
Key takeaways
svix-id, then a unique constraint on the payin ID.on_hold, failed, and refunded.Money moves through four hands before your user sees it as available. Your ledger mirrors each step.
processing and fires payin.new.completed and fires payin.complete. The stablecoins are in the user's wallet.Your app shows dollars throughout. The wallet balance and your ledger should agree at the end of every day; virtual account reconciliation covers that check.
Pending from payin.new, available from a completed payin.complete. Everything in between is a hold, not a credit.
| Payin status or event | Ledger action | What the user sees |
|---|---|---|
payin.new, status processing | Write a pending entry for the deposit | "Deposit received, arriving soon" |
payin.update, status on_hold | Keep pending, flag for review | "We're reviewing this deposit" |
payin.complete, status completed | Release pending, credit receiver_amount as available, book the fee | "Added to your balance" |
payin.complete, status failed | Reverse the pending entry | "This deposit didn't go through" |
payin.complete, status refunded | Reverse the pending entry | "Returned to the sender" |
Don't let users spend pending money. A deposit that sits in review and then fails leaves you with a negative balance to chase.
They differ in speed and in how they settle between banks, which changes what you tell users while a deposit is pending.
| ACH | Wire (Fedwire) | |
|---|---|---|
| How it moves | Batches of credit and debit transfers between banks | Real-time gross settlement, one transfer at a time |
| Finality at the bank | Batch settlement | Immediate, final, and irrevocable once processed |
| Fastest option | Same Day ACH, within hours on the same business day | Same day |
| Typical payer | Payroll, platforms, recurring payments | Larger, time-critical amounts |
| BlindPay arrival window | Up to 5 business days | Up to 5 business days |
The BlindPay window is the honest number for your "arriving by" copy. It covers the slow cases: a Friday send, a bank holiday, a payer's bank that batches late.
With two keys and one rule. Webhooks can arrive twice and out of order, so your handler must be safe either way.
svix-id, svix-timestamp, and svix-signature. Compute the HMAC-SHA256 over the ID, timestamp, and the raw body, as Svix describes. Re-serialized JSON breaks the signature. Reject old timestamps outside your tolerance.svix-id. It stays the same across redelivery attempts. Seen it? Return 200 and stop.processing event arrives after completed, ignore it.Stablecoin API webhooks goes deeper on ordering, and idempotency keys covers the API side.
Illustrative example. The fee below is invented for the arithmetic. Read real amounts from the payin, which carries them in minor units (cents).
A client sends a user $1,000.00 by ACH. Because the deposit is $100.00 or more, BlindPay charges its fee at the time of the transaction, so transaction_fee_amount is set and receiver_amount is the deposit minus the fee.
| Step | Event | Entry | Pending | Available |
|---|---|---|---|---|
| 1 | payin.new | Pending credit, deposit $1,000.00 | $1,000.00 | $0.00 |
| 2 | payin.complete, completed | Release pending | $0.00 | $0.00 |
| 3 | same event | Credit receiver_amount $995.00 | $0.00 | $995.00 |
| 4 | same event | Fee line, transaction_fee_amount $5.00 |
Steps 2 to 4 are one database transaction. If any line fails, none land.
Now a $40.00 deposit. It's below $100.00, so the fee goes to your invoice as billing_fee_amount and the user receives nearly the full $40.00. Your ledger books that fee as your cost, not the user's, unless your own pricing charges them. The neobank roadmap puts that pricing call in the money-in step.
Treat them as normal outcomes. Each gets a ledger action and a message.
on_hold. Compliance or risk review. Keep the pending entry and tell the user a review is in progress. Don't guess a time.failed. The payin did not go through. Reverse pending, notify, and link to support.refunded. The deposit went back to the sender. Reverse pending and tell the user to check with the payer.When a user asks where a deposit is, start from virtual account deposit not received. For quote-based payins, why on-ramp deposits fail lists the usual causes.
On a BlindPay development instance, payins complete on their own about 30 seconds after creation, using the USDB test token. Then work through this list:
request_amount 66600: ends failed. Your pending entry must reverse.request_amount 77700: ends refunded. Same check.payin.new delivered after payin.complete: no change to the available balance.payin.new.processing event overwrite a completed one.BlindPay gives each user a US virtual account and turns every deposit to it into a payin, with no checkout or payment instructions to show. The payin carries everything your ledger needs: the customer it belongs to, the account it landed on, sender_amount, receiver_amount, and separate fee fields in minor units, plus a status that moves through processing, on_hold, completed, failed, or refunded.
BlindPay converts the dollars and delivers USDC or USDT to the wallet linked to the account, then tells you through Svix-signed webhooks: payin.new, payin.update, and payin.complete. KYC, sanctions screening, and transaction review run before money moves, so an on_hold is BlindPay's review doing its job.
On a development instance, payins complete in about 30 seconds and sentinel amounts force failed and refunded outcomes, so you can test every branch of your ledger before a real dollar arrives.
Write the five-row status table above into your ledger service as code, then run the six tests on a development instance. The payins docs list every status, and webhook verification has the signature steps.
Always cheaper? No intermediaries? Recipient needs a wallet? 10 common claims about cross-border stablecoin payments, checked against the Fed and the BIS.
Seven places blockchain payments beat bank rails: contractor payroll, remittances, B2B suppliers, marketplaces, 24/7 treasury, bill pay, and PSP payouts.
The dates that turned stablecoins from a trading tool into cross-border payment rails: USDC, Pix, MiCA, the GENIUS Act, ISO 20022, and Visa settlement.