Skip to main content

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.
Soap does not store your customers’ balances and has no endpoint to read one. You own the ledger. Soap only ever sees the single balance_amount_cents figure you supply when you create the checkout, plus the charges it processes itself — it cannot reconstruct a balance you credit or debit for reasons it never sees (bonuses, promotions, corrections, gameplay). See You Own the Ledger below before you wire up withdrawals.
Every Soap API request is authenticated with a bearer token in the Authorization header:
Your API key is in the Soap Dashboard under the Developers section. It looks like 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

  1. You create a checkout session with type: "withdrawal" and include balance_amount_cents (the customer’s available balance).
  2. The customer is redirected to the Soap-hosted checkout page.
  3. The customer enters the amount they’d like to withdraw (up to their balance) and selects a payout method.
  4. 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 the balance_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:
  1. Read the customer’s current available balance from your ledger.
  2. Subtract any amount you’re already holding for withdrawals currently in flight (i.e. checkouts where you’ve received checkout.hold but not yet a terminal event) — otherwise the same funds could be authorized for withdrawal twice.
  3. 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.
  1. You create the withdrawal checkout with balance_amount_cents.
  2. The customer picks an amount and a payout method.
  3. 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.hold is delivered synchronously, is never retried, and times out after 10 seconds, so this check must be fast.
  4. 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.
  5. Soap later sends checkout.succeeded for the same checkout — treat it as a no-op for withdrawals you already debited on hold.
  6. 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.
  7. Every event carries a stable event_id. Use it as an idempotency key so retries never double-apply a debit or credit.
The double-debit trap: debiting the customer on checkout.hold and again on checkout.succeeded for the same withdrawal. If you debit on hold (recommended), checkout.succeeded for that checkout must be a no-op.

Every way a hold ends

Once you have debited on checkout.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.failed is off by default and it is the one most likely to strand real money. When a payout is accepted for processing and then fails, the charge moves from held to pending to failed. Because it was pending rather than held immediately before failing, Soap sends checkout.failednot checkout.release_hold. This is the normal failure shape for asynchronous payout rails, so if you debit on hold and leave checkout.failed disabled, you will debit customers for withdrawals that never arrive and never hear about it.Enable checkout.failed in Dashboard → Developers before you take withdrawals live.
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.
For signature verification and the general webhook endpoint setup, see Receiving Webhooks. For the full payload shape of each event, see checkout.hold and checkout.release_hold.

← Back to Create Checkout

View the full API reference for creating checkouts.