Engineering
Prepaid wallet billing for a reseller platform: holds, captures and a double-entry ledger
A reseller wallet should reserve funds with a hold, capture only what is delivered, release the rest, and record every step in a double-entry ledger.
The short answer
Wholesale platforms take money up front because every order they accept creates an immediate cost upstream. The safe way to spend that balance is to hold the order total, capture only what is delivered and release the remainder, with each step written as a balanced, append-only double-entry journal line. Without that structure, retries charge twice and concurrent orders push a balance below zero.
A reseller platform should take payment up front into a wallet, reserve the cost of each order with a hold, charge only for what is actually delivered, and return the rest. Every one of those steps belongs in an append-only double-entry ledger, because that is what stops a retried request charging twice and two simultaneous orders spending the same dollar.
Why wholesale platforms are prepaid
Wholesale platforms are prepaid because each order they accept becomes a cost they owe upstream straight away, and a prepaid balance means they never lend that cost to a reseller. A domain, a rented phone number or a licence seat is bought upstream the moment it is provisioned. If the reseller paid 30 days later, the platform would be financing every order and carrying the bad debt.
The domain wholesale market shows how normal this is. The terms below come from the vendors' own pages.
| Platform | Funding model | What the page says |
|---|---|---|
| OpenSRS | Prepaid deposit | "The Reseller must deposit funds into their reseller account" before registering; card and PayPal deposits carry a 3% processing fee; a one-time $95 activation fee applies (OpenSRS payment terms, as of October 2026) |
| Openprovider | Prepaid balance with auto top-up | Its example tops up €200 each time the balance drops below €500 (Openprovider prepaid system, published June 2024) |
| ResellerClub | Prepaid funding | "Once you fund your account, you can start registering domains at wholesale immediately" (ResellerClub domain reseller, as archived on 28 September 2026). A minimum deposit was not found on a ResellerClub page. |
| Namecheap | Account balance | Production API access needs one of: 20 domains, a $50 balance, or $50 spent in the last two years (Namecheap API FAQ, as of October 2026) |
| Google Workspace resale | Invoiced, postpaid | Fees are "due net 30 days from the invoice date", with interest on overdue amounts at 1.5% per month (Google reseller agreement, as of October 2026) |

OpenSRS asks resellers to deposit funds before they register anything. Screenshot of opensrs.com, taken on 7 October 2026.
Each model has a real strength. A deposit with a published fee schedule, like OpenSRS, is predictable. Auto top-up, as Openprovider offers, removes the most common failure of prepaid, which is running dry before a renewal. Google's net-30 terms are kinder to cash flow, but they come with a contract and a credit relationship.
That last row is the mismatch a multi-product platform absorbs: some suppliers want money first and some invoice later, yet the reseller sees one balance. For the wider picture, see what a white-label reseller platform is and the domain reseller API comparison.
Two cautions. No data was found on what share of reseller platforms are prepaid, so this is an observation from the pages above, not a statistic. And a stored balance can raise tax and stored-value questions that depend on jurisdiction, which this article does not cover.
How hold, capture and release work
A hold reserves money without spending it, a capture turns the reserved amount into a charge, and a release returns whatever was not captured. The pattern comes from card payments, where Stripe describes it as placing "a hold on an eligible payment method to reserve funds that you can capture later" (Stripe Docs, as of October 2026).

Stripe documents authorising a payment first and capturing the funds later. Screenshot of docs.stripe.com, taken on 7 October 2026.
A wallet needs the same steps because provisioning is neither instant nor certain. An upstream provider can be out of stock, time out, or fill two of three items. Charging first and refunding on failure fills the history with charge and refund pairs for orders that never happened. Holding first means a failed order never appears as a charge at all.
The lifecycle of one order looks like this:
- Hold. The order total moves from the available balance to a held balance. If available funds are too low, the order is refused and nothing is written.
- Capture. For each item the provider delivers, that item's price moves from held to the platform.
- Release. Anything still held when the order finishes moves back to available.
- Refund. If a captured item is later cancelled, the charge is reversed with a new entry. The original is never edited.
One difference from cards matters. A card authorisation expires; Stripe says the amount is typically on hold for 7 days for online card payments, depending on the transaction type and card network, and that a partial capture "automatically releases the remaining amount" (Stripe Docs, as of October 2026). A wallet has no card network behind it, so nothing expires a hold for you. The platform has to decide how an unanswered hold is settled.
Why the ledger should be double-entry
A double-entry ledger records every movement as a debit to one account and an equal credit to another, so money can move between accounts but cannot appear or disappear. Modern Treasury puts the rule as: "every transaction should record both where the money came from and what the money was used for" (Modern Treasury, updated August 2025).
The alternative is a single balance column that code increments and decrements. It is fast to build and impossible to audit: when a reseller asks why the balance is lower than expected, a mutable number has no answer. Modern Treasury's guidance is that "it's more accurate to store immutable transactions and always compute balances from those transactions" (same article).
The large payment companies reached the same design. Stripe's Ledger "is based on double-entry bookkeeping", its published transactions "cannot be deleted or modified", and it handled five billion events a day as of February 2024 (Stripe engineering blog). Square built its Books service after its settlement systems "occasionally observed inconsistencies in the data, resulting in customer inquiries and delayed deposits"; in Books every journal entry "must balance to 0, so each cent lost is matched with a cent gained" (Square engineering blog, October 2019).
For a wallet, a small set of accounts is enough. Modern Treasury's wallet tutorial uses a cash account that is debit normal, one wallet account per user that is credit normal, and a revenue account (Modern Treasury digital wallet tutorial, as of October 2026). The wallet is credit normal because, from the platform's side, a customer's prepaid balance is money it owes. Adding a second per-reseller account for held funds gives the full model:
| Account | Type | What it means |
|---|---|---|
| Bank | Asset | Cash the platform has received |
| Wallet available | Liability | What the reseller can spend now |
| Wallet held | Liability | What is reserved for orders in progress |
| Revenue | Revenue | What the platform has charged |
A worked example in ledger entries
One order with a partial fill touches every entry type, so it makes a good test of the model. The amounts below are invented for illustration and are not anyone's prices. A reseller tops up $100.00, then orders three items at $15.00 each. The provider delivers two. Later, one delivered item is cancelled and refunded.
| # | Event | Debit | Credit | Amount |
|---|---|---|---|---|
| 1 | Top-up | Bank | Wallet available | $100.00 |
| 2 | Hold for 3 items | Wallet available | Wallet held | $45.00 |
| 3 | Capture 2 delivered items | Wallet held | Revenue | $30.00 |
| 4 | Release 1 undelivered item | Wallet held | Wallet available | $15.00 |
| 5 | Refund 1 cancelled item | Revenue | Wallet available | $15.00 |
Balances after each entry, computed from the journal and stored nowhere else as the source of truth:
| After entry | Bank | Wallet available | Wallet held | Revenue |
|---|---|---|---|---|
| 1 | $100.00 | $100.00 | $0.00 | $0.00 |
| 2 | $100.00 | $55.00 | $45.00 | $0.00 |
| 3 | $100.00 | $55.00 | $15.00 | $30.00 |
| 4 | $100.00 | $70.00 | $0.00 | $30.00 |
| 5 | $100.00 | $85.00 | $0.00 | $15.00 |
In every row, Bank equals the sum of the other three columns. That is the check double entry gives for free: if it ever fails, an entry is missing or duplicated, and the journal says which order it belongs to. The held balance also returns to zero once the order is finished; a hold account that does not points at an order that was never settled.
The table is simplified. Real books would also record the cost paid upstream, and may recognise rental revenue over time. Neither changes the wallet mechanics.
An equivalent design keeps one account per wallet and gives each transaction a status. In Modern Treasury's model a hold is a pending transaction that is later posted or archived, and the available balance of a credit-normal account is posted credits minus pending debits, where the pending figure counts both pending and posted entries (Modern Treasury ledger balances, transaction statuses, both as of October 2026). Both designs give the reseller the same two numbers: available and held.

Modern Treasury gives a ledger transaction one of three statuses: pending, posted or archived. Screenshot of docs.moderntreasury.com, taken on 7 October 2026.
What goes wrong without it
The two classic wallet bugs are the same order charged twice and a balance driven below zero by concurrent orders. Both come from treating the balance as a number to edit instead of the result of a history.
Double charges from retries
A reseller's server posts an order. The platform debits the wallet and buys the item, but the response is lost in a timeout. The client retries, and a naive server runs the whole thing again.
| Step | What happens | Balance column |
|---|---|---|
| 1 | Request A: debit $15.00, buy item | $85.00 |
| 2 | Response to A is lost | $85.00 |
| 3 | Client retries: debit $15.00, buy a second item | $70.00 |
The reseller wanted one item and paid for two. The fix is an idempotency key: the client sends a unique value with the order, the server stores the first outcome against it, and a retry gets that stored response back. Stripe saves "the resulting status code and body of the first request" and lets keys be pruned once they are at least 24 hours old (Stripe API reference, as of October 2026); Adyen keeps keys valid for 7 to 14 days (Adyen Docs, as of October 2026). The header is a convention, not a standard: the IETF draft that describes it expired on 18 April 2026 and has not become an RFC (IETF Datatracker, as of October 2026).
The ledger adds a second guard. If each journal entry carries a unique reference, such as the order id plus the entry kind, the database refuses a second hold for the same order. The full retry contract is covered in idempotency keys and webhooks for reseller APIs.
Negative balances from concurrent orders
A wallet holds $20.00. Two orders for $15.00 each arrive at the same moment, and each does a read followed by a write.
| Step | Order A | Order B | Stored balance |
|---|---|---|---|
| 1 | Reads $20.00, enough | $20.00 | |
| 2 | Reads $20.00, enough | $20.00 | |
| 3 | Writes $20.00 minus $15.00 | $5.00 | |
| 4 | Subtracts $15.00 | negative $10.00 |
If order B instead writes the value it computed from its stale read, the balance shows $5.00 and one charge is silently lost. Either way the platform has delivered $30.00 of product against $20.00 of funds.
The fix is to make the check and the write one atomic operation. Modern Treasury documents this as a balance filter on the entry, for example requiring the available balance to stay at or above zero, and says that if the transaction "would cause the Ledger Account's current balance to go outside of the specified range, the request will fail with a 422"; it presents the feature as a way to prevent double spends (Modern Treasury Docs, as of October 2026). Here, order A's hold succeeds and order B is refused before anything is bought.
Holds that never settle
A third failure is quieter. The provider times out and the platform does not know whether the item was provisioned. Releasing the hold risks giving the item away; capturing risks charging for nothing. The correct state is neither: keep the money held, mark the order for reconciliation, and let a background job ask the provider what happened.
Design checklist
These rules summarise the sections above.
- Store amounts as integers in the smallest currency unit, never floating point.
- Make journal entries append-only. Correct mistakes with a new reversing entry.
- Derive balances from entries, so any cached balance can be rebuilt.
- Make the hold a conditional write that fails when available funds would go below zero.
- Require an idempotency key on every route that spends money, and publish how long keys are kept.
- Give every entry a unique reference tied to the order.
- Define what happens to a hold when the provider gives no clear answer.
- Check on a schedule that debits equal credits and finished orders hold nothing.
- Decide what a renewal does when the wallet can't pay: retry, wait, or let it lapse. See how renewals and grace periods work for resellers for how that plays out across numbers, licences and domains.
Where ResellerHQ fits
ResellerHQ is a white-label reseller API whose wallet follows the model in this article, and it is at an early stage. The wallet is prepaid in US dollars with amounts as integer cents. The balance endpoint returns available and held, and the history lists topup, hold, capture, release and refund movements recorded as balanced double-entry lines. An order the wallet cannot cover is refused with 402 insufficient_funds, a number order with no clear provider answer stays reserved under needs_reconciliation, and idempotency keys are stored for 24 hours. The details are in the docs.
The limits are real. Domains and mailbox licences run in the sandbox only for now, and their live routes answer 422 not_available. Live phone numbers depend on an upstream provider, and live ordering is opening account by account. Live funds are added by invoice, because card payments are not connected yet. ResellerHQ publishes no price list. If you need a production domain wallet today, the established registrars in the table above are the place to look. To watch hold, capture and release behave against an API with test funds, the sandbox is open at /signup.
Questions
- An order for a domain, a number or a licence creates a cost upstream the moment it is filled, so the platform would be lending money to every reseller it invoiced later. A prepaid balance removes that credit risk and lets an API order be approved in one request with no credit check.
- A hold moves money from the available balance to a reserved balance without spending it. A capture turns some or all of the reserved amount into a real charge, and anything not captured is released back to the available balance.
- Every movement is recorded as a debit to one account and an equal credit to another, so money can only move, never appear or vanish. Balances are computed from an append-only history, which means any balance can be explained line by line and a missing or duplicated entry shows up as an imbalance.
- Make the hold a conditional write that succeeds only if the available balance stays at or above zero, and reject the order otherwise. Reading the balance first and writing second is not enough, because two concurrent orders can both pass the read.
- Accept an idempotency key on every route that spends money and store the first outcome against it, so a retry returns the stored response instead of creating a second hold. Give each ledger entry a unique reference as a second line of defence.
- The money should stay reserved, neither charged nor returned, until a reconciliation job learns what the provider actually did. Releasing early risks giving away a product for free, and capturing early risks charging for nothing.
Sources
- Stripe: Ledger, Stripe's system for tracking and validating money movement stripe.dev
- Stripe Docs: Place a hold on a payment method docs.stripe.com
- Stripe API reference: Idempotent requests docs.stripe.com
- Modern Treasury: Accounting for developers, part I moderntreasury.com
- Modern Treasury Docs: Transaction status and balances docs.moderntreasury.com
- Modern Treasury Reference: Ledger balances object docs.moderntreasury.com
- Modern Treasury Docs: Lock on account balance or version docs.moderntreasury.com
- Modern Treasury Docs: Digital wallet tutorial docs.moderntreasury.com
- Square: Books, an immutable double-entry accounting database service developer.squareup.com
- OpenSRS: Payment terms opensrs.com
- Openprovider: Prepaid system openprovider.com
- ResellerClub: Domain reseller resellerclub.com
- Namecheap: API FAQ namecheap.com
- Google Workspace: Reseller agreement, North America workspace.google.com
- Adyen Docs: API idempotency docs.adyen.com
- IETF Datatracker: The Idempotency-Key HTTP Header Field datatracker.ietf.org