Overview
A balance withdrawal presents the customer with an input form where they can choose how much to withdraw, up to their available balance. This is the standard withdrawal experience. Every Soap API request is authenticated with a bearer token in theAuthorization header:
key_....
Requests with a missing or invalid API key return 422 — never 401 — with a JSON error field, usually {"error": "API key not found."}. Branch on the status code rather than the message: GET /api/v1/device_pings/latest_geo_check does not yet return the same wording.
Never expose your API key in client-side code — it is server-side only.
How It Works
- You create a checkout session with
type: "withdrawal"and includebalance_amount_cents(the customer’s available balance). - The customer is redirected to the Soap-hosted checkout page.
- The customer enters the amount they’d like to withdraw (up to their balance) and selects a payout method.
- Upon completion, a webhook is fired and the customer is redirected back to your app.
API Request
Key Parameters
When to Use
- Letting the customer choose their withdrawal amount
- Standard cashout flows where the customer has a balance
- Any scenario where the withdrawal amount is not predetermined
You Own the Ledger
Soap is a payments layer, not a ledger. There is no endpoint anywhere in the public API to look up a customer’s balance — the full API surface is customers, checkouts, charges, products, KYC, device pings, and geo checks. Nothing else. Soap never tracks a running balance for your customers; the only balance-related figure it ever holds is thebalance_amount_cents you send it, scoped to that single checkout.
This means balance_amount_cents is not something Soap validates against a stored balance — it’s a snapshot you assert at checkout-creation time. Soap stores it on the checkout and enforces it as a ceiling: the amount the customer ultimately withdraws must be less than or equal to what you sent. Soap does not re-check it against anything else, because it has nothing else to check it against.
Computing balance_amount_cents
Since you own the source of truth, compute this figure from your own ledger when you create the checkout:
- Read the customer’s current available balance from your ledger.
- Subtract any amount you’re already holding for withdrawals currently in flight (i.e. checkouts where you’ve received
checkout.holdbut not yet a terminal event) — otherwise the same funds could be authorized for withdrawal twice. - Pass the result as
balance_amount_cents.
fixed_amount_cents is the alternative to balance_amount_cents for withdrawals with a preset amount instead of a customer-chosen one. See Preset Amount Withdrawal. Which fields your account accepts for a given checkout is governed by your account’s checkout flow configuration — if you send a combination it isn’t configured for, you’ll get a 422 whose hint tells you to contact Soap.The Hold Lifecycle
Withdrawals use a hold handshake so a customer can’t spend money that’s already being cashed out. Getting this wrong is the single easiest way to double-debit a real customer, so read this before you go live.- You create the withdrawal checkout with
balance_amount_cents. - The customer picks an amount and a payout method.
- Soap sends
checkout.hold— a question: does this customer have the funds? Respond 2xx and the withdrawal proceeds; respond non-2xx and it doesn’t.checkout.holdis delivered synchronously, is never retried, and times out after 10 seconds, so this check must be fast. - Debit the customer immediately when you respond 2xx to
checkout.hold. This is what prevents the customer from spending the same funds elsewhere while the withdrawal is in flight. - Soap later sends
checkout.succeededfor the same checkout — treat it as a no-op for withdrawals you already debited on hold. - If the withdrawal does not complete, credit the customer back. Which event tells you depends on how it failed — see Every way a hold ends below. Getting this set wrong is how held funds go missing.
- Every event carries a stable
event_id. Use it as an idempotency key so retries never double-apply a debit or credit.
Every way a hold ends
Once you have debited oncheckout.hold, you have taken on an obligation to credit the money back if the withdrawal doesn’t complete. A held charge can end in exactly three ways, and they do not all send the same event:
checkout.release_hold and checkout.succeeded cannot be disabled, so those two always reach you. checkout.expired is also off by default, and unlike the others its payload contains no charge object at all — only id, expired_at, type, customer.id and subscription. That is why the handler below records the amount it debited rather than reading it back off the event.
A charge can also move from succeeded to failed or returned well after the fact, when a payout bounces. Those arrive as
checkout.failed (with charge.from_status set to "succeeded") and checkout.returned. Keep your record of what you debited until your reconciliation window closes so you can credit these back too, rather than deleting it the moment a withdrawal succeeds.Example: a single handler for the full lifecycle
creditBackHold is deliberately keyed on your own debit record rather than on
the event type, so the four terminal events can share one branch and a
withdrawal you never debited is ignored safely. Because it deletes the record
as it credits, two different terminal events for the same checkout can never
credit the customer twice.checkout.hold and checkout.release_hold.
← Back to Create Checkout
View the full API reference for creating checkouts.

