Implementation Guide
Build a ledger-backed wallet
Hold a customer balance, reserve part of it for an operation, then consume or release the reservation.
The rule to remember: a balance has three numbers. availableBalance is what the
customer can spend now, reservedBalance is held for an operation that has not finished, and
totalBalance is both together. Reservations come from operations, and every reservation ends as
consumed or released.
The job
A marketplace gives a customer 50,000 COP of store credit after a return. Later the customer buys something for 120,000 COP and wants to use 20,000 of that credit. The credit has to be held while the customer pays the rest, spent if the payment succeeds, and given back if it does not. Support should be able to see each step.
The same shape fits a loyalty program or a seller balance. Orangepill records the position. It does not hold the money: funds stay with the banks, providers and licensed institutions you work with. Loyalty value recorded in a wallet is a balance in your program, not cash.
Before you start
- An API key for the sandbox with wallet permissions (
wallets:writeto create accounts,wallets:transactto credit them,wallets:read) andledger:read. Sandbox access is provided during evaluation. - A customer record. User wallet accounts belong to a customer.
- The currency enabled for your tenant. A wallet currency must be an active unit in your tenant's configuration. A loyalty program can use its own unit, set up with you during evaluation.
- A merchant and a pay-in route for hosted checkout, if you follow the checkout example.
Flow
- Available Spendable now
- Reserved Held by an operation
- Consumed Operation succeeded
- Released Operation failed, cancelled or expired
Implementation
1. Create the wallet account
One account per customer and currency. Store the returned id against your customer.
curl -X POST "$ORANGEPILL_API/v4/wallets/accounts" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountType": "user",
"currency": "COP",
"customerId": "<customer id>",
"metadata": { "program": "marketplace-credit" }
}' {
"account": {
"id": "5b0e7c2d-…",
"customerId": "<customer id>",
"accountType": "user",
"currency": "COP",
"status": "active",
"createdAt": "2026-10-11T09:00:02Z"
}
} 2. Credit it once
Wallet amounts are decimal strings in the currency's major unit. Send a referenceType and a
referenceId (a UUID from your own system) with every credit. If the same pair arrives again, for
example after a timeout, the existing transaction is returned and the balance does not change twice.
curl -X POST "$ORANGEPILL_API/v4/wallets/accounts/$WALLET_ID/deposit" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "COP",
"amount": "50000.00",
"referenceType": "store_credit",
"referenceId": "<uuid of the credit in your system>",
"description": "Credit for returned order 10233"
}' {
"transaction": {
"id": "e41a9d70-…",
"accountId": "5b0e7c2d-…",
"transactionType": "credit",
"amount": "50000.00",
"currency": "COP",
"balanceBefore": "0",
"balanceAfter": "50000.00",
"referenceType": "store_credit",
"referenceId": "<uuid of the credit in your system>"
}
} 3. Read the balance
curl "$ORANGEPILL_API/v4/wallets/accounts/$WALLET_ID/balances/COP" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" {
"balance": {
"accountId": "5b0e7c2d-…",
"currency": "COP",
"availableBalance": "50000.00",
"reservedBalance": "0.00",
"totalBalance": "50000.00",
"updatedAt": "2026-10-11T09:00:05Z"
}
}
Show availableBalance as the spendable amount. Do not compute it yourself from your own history.
4. Reserve part of it through an operation
There is no public "reserve" call. A reservation is created by the operation that needs the value, carries an expiry, and is released automatically by a job that runs every minute once that expiry passes. Two operations create reservations today:
- Hosted checkout:
POST /v4/checkout/sessions/{id}/apply-wallet. The reservation lasts 60 minutes. - Wallet-funded payout:
POST /v4/payoutswithsource.type: "wallet". The reservation lasts one hour. Today it is recorded against the wallet but does not moveavailableBalanceorreservedBalance, so treat the payout's own status as the source of truth. See Build your first payout.
For the purchase, create a checkout session for the full amount with the customer's id. The checkout session amount is also in major units.
curl -X POST "$ORANGEPILL_API/v4/checkout/sessions/" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchant_id": "<merchant id>",
"amount": "120000.00",
"currency": "COP",
"customer_id": "<customer id>",
"order_reference": "order-10251",
"success_url": "https://shop.example.com/orders/10251/done",
"cancel_url": "https://shop.example.com/orders/10251"
}' The wallet is applied with the session's client secret, which is how the checkout page the customer sees authenticates. The secret is delivered in the session's checkout URL when you create it.
curl -X POST "$ORANGEPILL_API/v4/checkout/sessions/$SESSION_ID/apply-wallet" \
-H "Authorization: CheckoutSession <session client secret>" \
-H "Content-Type: application/json" \
-d '{
"wallet_id": "<wallet account id>",
"amount": "20000"
}' {
"session_id": "9a7c51e2-…",
"reservation_id": "0f3d88b4-…",
"applied_amount": "20000",
"remaining_amount": "100000"
} Read the balance again. The money has moved from available to reserved. The total has not changed.
{
"balance": {
"availableBalance": "30000.00",
"reservedBalance": "20000.00",
"totalBalance": "50000.00"
}
} The customer pays the remaining 100,000 COP with a pay-in method on the session. Applying the wallet again with a different wallet or amount releases the first reservation before creating the new one.
5a. The payment succeeds: consumed
When the session completes, the reservation is consumed. The reserved amount leaves the total, and a debit transaction appears in the wallet's history.
{
"balance": {
"availableBalance": "30000.00",
"reservedBalance": "0.00",
"totalBalance": "30000.00"
}
} 5b. The payment fails, is cancelled or the session expires: released
If the payment fails or is cancelled, the reservation is released and the session goes back to open, so the customer can try again. If the session expires, it is released too. If nothing ends it, the expiry job releases it after 60 minutes.
{
"balance": {
"availableBalance": "50000.00",
"reservedBalance": "0.00",
"totalBalance": "50000.00"
}
} 6. Read the history and the ledger
The wallet's transactions are the customer-facing history: the credit from step 2 and, if the purchase succeeded, the debit. A reservation on its own does not add a transaction.
curl "$ORANGEPILL_API/v4/wallets/accounts/$WALLET_ID/transactions" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" The ledger shows each movement as a balanced journal. Creating and releasing a reservation are posted under the reservation id. Consuming it is posted under the checkout session id.
# Journals for creating and releasing the reservation
curl "$ORANGEPILL_API/v4/ledger/entries/by-reference?referenceType=wallet_reservation&referenceId=$RESERVATION_ID" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY"
# Journal for consuming it, which references the checkout session
curl "$ORANGEPILL_API/v4/ledger/entries/by-reference?referenceType=order_discount&referenceId=$SESSION_ID" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" {
"entries": [
{
"journalId": "c81f20aa-…",
"accountCode": "WL:5b0e7c2d-…",
"debitAmount": "20000",
"creditAmount": "0",
"currency": "COP",
"referenceType": "wallet_reservation",
"referenceId": "0f3d88b4-…"
},
{
"journalId": "c81f20aa-…",
"accountCode": "WLR:5b0e7c2d-…",
"debitAmount": "0",
"creditAmount": "20000",
"currency": "COP",
"referenceType": "wallet_reservation",
"referenceId": "0f3d88b4-…"
}
]
}
To see how a journal relates to the operation around it, pass its journalId to the financial
explain endpoint. It returns a graph of nodes, edges and facts.
curl "$ORANGEPILL_API/v4/financial/explain?subjectType=ledger_journal&subjectId=$JOURNAL_ID" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" What can go wrong
- The deposit request times out. Send it again with the same
referenceTypeandreferenceId. You get the existing transaction back. Without a reference pair, a retry is a second credit. - Not enough available balance. apply-wallet returns
422withINSUFFICIENT_BALANCE. Reserved value does not count as available. - Applying more than the session total. Rejected with
422(AMOUNT_EXCEEDS_SESSION_TOTAL). - The wallet belongs to someone else. Rejected with
403(WALLET_OWNERSHIP_MISMATCH). - The session expired. apply-wallet returns
410. Any reservation on it is released. - The operation outlives its reservation. Expiry is enforced whatever the operation's state. A
wallet-funded payout still
pendingafter one hour loses its hold. Alert on it well before that. - A wallet-funded payout fails with
RETRY_EXHAUSTED. Orangepill stopped checking the provider without a final answer. The reservation is released, but the money may have moved. Treat it as unknown: do not let the customer spend or withdraw that value again until the payout's outcome is confirmed with the provider.
What Orangepill guarantees today
- A reservation moves value from available to reserved, and ends as consumed or released.
- Reserving, consuming and releasing through checkout each post a balanced journal you can read by reference.
- A deposit with the same account,
referenceTypeandreferenceIdis applied once. - Expired reservations are released automatically by a job that runs every minute.
- Posted ledger amounts are not edited. Corrections are new entries.
What Orangepill does not guarantee
- A generic public endpoint to reserve, consume or release value. Reservations belong to operations.
- Holding a reservation for as long as an operation takes. Checkout reservations last 60 minutes and payout reservations one hour.
- Custody. Orangepill records positions. Funds stay with banks, providers and licensed institutions.
- That loyalty value is money. Points recorded in a wallet are a balance in your program, not cash or a regulated balance.
Production considerations
- Production uses its own API key, its own wallet accounts and its own currency configuration. Nothing carries over from sandbox.
- Persist the wallet account id per customer, the reference pair for every credit, and the reservation id returned by each operation.
- Monitor reservations against their expiry, and payouts still non-terminal as they approach one hour.
- Test in sandbox: a repeated deposit, insufficient balance, a failed payment after applying the wallet, and a session that expires.
- Your support team should be able to open a wallet and see available, reserved and total balance, its transactions and the ledger entries for each reservation.
- If you run a loyalty program, decide with your legal team how the program's value is described to customers. Orangepill records it as a balance.
LAQO uses Orangepill to power its Q points loyalty: see the case. The full list is in the production checklist.
Build this with us.
Sandbox access and API credentials are provided during evaluation.