Skip to main content
Soap notifies your backend of checkout activity by sending an HTTP POST request with a JSON body to the webhook URL you configure in your dashboard. New to receiving webhooks? Start with Receiving Webhooks for setup steps and a minimal handler. Before writing a handler, read Checkouts, Charges and Reviews. Most events below report a transition of a charge, not of the checkout session, and one checkout can produce several charges — which determines what you should key your ledger on.

The Envelope

Every webhook delivery is a JSON object with three top-level keys. The wire key order is always event_id, data, type — note that data comes before type, even though it reads more naturally last.
data.type (inside the payload) is the checkout type"deposit" or "withdrawal" — not the event type. It’s easy to confuse with the top-level type, which is the event name.
Here’s a complete, realistic checkout.succeeded delivery for a deposit with a populated line item:
charge.failure_code and charge.failure_message are present on every charge-based event and are null when there’s no failure. subscription is present on every one of the 12 events — it’s null for non-subscription checkouts, otherwise an object describing the recurring plan.

Events

Soap sends 12 event types. Four are always sent and cannot be disabled. The other eight are off by default and will never fire until you enable them. Not every charge status has an event: a charge that becomes cancelled or refunded is not notified. See the charge status lifecycle for the full status-to-event mapping.
The 8 events marked off by default are silently never delivered until you enable them in Dashboard → Developers. If you write a handler for one of these (e.g. checkout.failed) and it never fires, this is why. See Receiving Webhooks for how to enable them.

Verifying Webhook Signatures

To prevent malicious actors from sending you fake events, every delivery includes a signature. Soap sends the identical signature value in two headers: There is no separate timestamp header and no event-id header — the timestamp is embedded in the signature value, and event_id is in the body. Both headers use the same format:
  • t is the Unix timestamp when the signature was generated.
  • v1 is the signature itself, an HMAC-SHA256 hex digest.
The signed message is "<timestamp>.<raw_request_body>" — the timestamp, a literal period, then the exact raw bytes of the request body. Recompute this using your webhook signing secret (wss_..., from Dashboard → Developers) and compare it to the received signature.
The signature covers Soap’s exact serialized bytes, not your parsed-and-re-serialized JSON. If your framework parses the JSON body before your handler runs, and you then call JSON.stringify/.to_json to recompute the signature, verification will fail — key order and whitespace differ from what Soap actually sent.In Express, use express.raw({ type: 'application/json' }) on the webhook route (instead of the global express.json() middleware) so req.body is the untouched Buffer of raw bytes:
Every framework has an equivalent need: read the body as raw bytes (or a raw string) for signature verification before any middleware parses and reconstructs it as an object.
If the signatures don’t match you should reject the webhook and not process it.

Updating Your Internal Ledger

It depends on how you implement your accounting system. In this example, we only use credits and debits and update the customer’s balance or internal ledger accordingly.

Retries, Timeouts and Idempotency

  • Each delivery attempt has a 10 second timeout.
  • Soap retries up to 5 total attempts (the initial attempt plus 4 retries) for events that fail or time out.
  • checkout.hold is delivered synchronously and is never retried — see checkout.hold for why your response status matters.
  • Each delivery attempt generates a new timestamp and therefore a new signature — even retries of the same logical event will have a different SOAP-WEBHOOK-SIGNATURE value.
  • event_id is stable for a given logical event across all of its delivery attempts. Use it as your idempotency key so you don’t reprocess the same event twice.