checkout.failed is off by default. It will not be delivered until you enable it in Dashboard → Developers. See Receiving Webhooks for instructions.
One Event Per Attempt, Not Per Checkout
Despite its name, checkout.failed does not mean the checkout failed. It reports that one payment attempt — one charge, identified by data.charge.id — failed. What happens to the checkout depends on charge.from_status:
from_status: "created" — the attempt was declined while the customer was still in the checkout (a synchronous card decline, for example). The checkout stays open, the customer returns to the payment form, and their next submission is a new charge against the same checkout. See One Checkout, Several Charges.
from_status: "pending" or "succeeded" — the charge had already been accepted, and the customer had already left the checkout on a pending or success screen. The checkout is not reopened and the customer cannot retry inside it. If you want them to pay again, create a new checkout.
So within a single checkout you can receive checkout.failed for one charge and then checkout.succeeded for another. A customer whose first card is declined and who then pays with a different card or wallet produces exactly that pair, and it is expected.
A successful charge is reported by checkout.succeeded; the checkout-level terminal outcomes are checkout.terminally_failed and checkout.expired:
checkout.failed is not one of them. Treat it as attempt-level history and never let it mark a checkout as failed.
- Key every booking on
data.charge.id. A checkout.failed for one charge must never undo a checkout.succeeded for a different charge in the same checkout, even if it arrives later — delivery is at-least-once and events can arrive out of order.
- Dedupe on
event_id; retries of the same event reuse it. See Retries, Timeouts and Idempotency.
- To confirm any charge’s current state, read it back with
GET /api/v1/charges/{id}.
Payload Structure
Example Response
Updating Your Internal Ledger
This event is triggered when one deposit or withdrawal attempt fails. For deposits, and for withdrawals you have not already debited, your ledger is usually unaffected — the customer may still complete the same checkout with a later attempt.
For withdrawals it can be the event that releases a hold, and missing it strands customer funds. Use charge.from_status to tell the two cases apart:
from_status: "pending" — the payout was accepted for processing and then failed. Because the charge was pending rather than held immediately beforehand, Soap sends this event instead of checkout.release_hold. If you debited the customer on checkout.hold, you must credit them back here. See the withdrawal hold lifecycle.
from_status: "succeeded" — a settled charge was reversed after the fact. Track the charge.id state to reconcile these correctly.
This event is off by default. For withdrawals that means an accepted-then-failed payout produces no webhook at all unless you enable it in Dashboard → Developers, which is the most common way held funds go missing.