Skip to main content
Soap models a payment with three separate entities. Most integration bugs come from treating them as one, so it is worth reading this page before you write a webhook handler or design your ledger. A checkout has one or more charges — one per payment attempt. A checkout has at most one review.
Despite the naming, most checkout.* webhooks are charge events: they report a transition of the underlying charge, and their payload carries a charge object. Only checkout.expired and checkout.terminally_failed describe the checkout itself, and neither carries a charge.So checkout.succeeded means the charge succeeded, not the session reached a successful end state.

Charge Status Lifecycle

A charge starts at created and moves through the transitions below. failed, returned, cancelled and refunded are terminal — no further transition is possible. Note that succeeded is not terminal. A settled charge can later be reversed by the processor, which is why you can receive checkout.failed with from_status: "succeeded" for a payment you already credited.

Which transition emits which webhook

Not every status has a corresponding event. Three have none at all.
cancelled and refunded are not notified. cancelled applies to bank-transfer payments cancelled while still pending. refunded is set when Soap refunds a settled charge out-of-band — through a payment partner rather than through the API — and reconciles it afterwards; nothing produces it automatically, so it does not follow from anything you do through the API.Soap sends no event for either. If you need to observe them, read the charge with GET /api/v1/charges/{id}.
Two details about failed that matter when reconciling withdrawals:
  • The same status produces two different events depending on where the charge came from. A withdrawal that fails after a hold was placed emits checkout.release_hold; a withdrawal that fails before reaching held emits checkout.failed, which is off by default. Both mean you should release the hold.
  • checkout.hold is delivered before the charge moves to held. If your endpoint does not return success, Soap fails the charge while it is still created — so you receive checkout.failed, not checkout.release_hold. See checkout.hold.

One Checkout, Several Charges

Each payment attempt creates a new charge with a new ch_ id. When an attempt fails, the customer returns to the payment form; their next submission is a new attempt against the same checkout. A customer whose card is declined and who then pays with a different method leaves behind two charges: one failed, one succeeded.
Key your ledger on data.charge.id, not on the checkout id. A checkout id is not unique per payment, so using it as your idempotency or reconciliation key will mis-book retries.
Soap enforces at most one charge per attempt. It does not limit how many attempts a checkout can accumulate, so do not assume a fixed number of charges per checkout. Two consequences worth designing around:
  • The charge returned when you read a checkout is the latest attempt’s charge. Earlier attempts are not listed on it.
  • Because checkout.failed is off by default, a merchant who has not enabled it receives no notification of the earlier failed attempts and has no charge id for them. If you need attempt-level history, enable checkout.failed in Dashboard → Developers.

Checkout Outcomes

A checkout is in progress from creation until the customer finishes it or it expires, which is 30 minutes after creation by default. There is no checkout status field, and no endpoint to poll a checkout’s state after you create it — you observe outcomes through webhooks.
There is no terminal-success event for the checkout itself. Success is signalled by the charge events, which is why the list above mixes charge-level and checkout-level events.Both checkout.expired and checkout.terminally_failed are off by default, so a merchant who has enabled nothing extra receives no webhook at all for an abandoned or blocked checkout.

Reviews

Some withdrawals are routed to manual review before any payment is attempted. Review is disabled by default and enabled per account, and the criteria that select a withdrawal for review are configured per account — so some withdrawals enter review and others do not. Today every checkout.review.* event you receive will carry "type": "withdrawal". A review is created before the charge exists. Soap creates the review and pauses the checkout instead of submitting the payment, which means:
  • There is no charge during the review window, so no charge status can change and you will receive no charge-based webhook for that checkout while the review is open.
  • On approval, Soap creates the charge and places the hold at that point — checkout.hold arrives then, followed by the normal withdrawal events.
  • On decline, no charge is ever created. Neither checkout.failed nor checkout.release_hold can fire, because there was never a charge to fail.
A declined withdrawal produces no charge-failure webhook. If you reconcile solely on checkout.failed or checkout.release_hold, declined withdrawals will be invisible to you. Subscribe to checkout.review.declined to observe them — it is off by default, as are all three review events.
Reviews are resolved by a person in the Soap dashboard. There is no timeout, no auto-approve and no auto-decline, and there is no public endpoint to approve or decline a review.