SUQODocs
User GuideAPI ReferenceSDKs

Webhook events

The event catalogue and wire format. To set an endpoint up see Receive webhook events; to check a delivery is genuine see Verify a webhook signature.

Events

EventSent when
checkout.succeededA checkout payment on your account was finalized successfully
checkout.failedA checkout payment failed terminally (no further retry by SUQO)
subscription.status_changedA subscription moved from one status to another

The two checkout events fire once per checkout session, whatever the item count. Payments taken through the legacy Create a subscription endpoint report through the same payload.

checkout.succeeded

{
  "event": "checkout.succeeded",
  "session_id": "cks_3n9k2xQ7m",
  "amount": "5000.00",
  "status": "succeeded",
  "items": [
    { "pbp_id": "pbp_3n9k2x" },
    {
      "name": "Onboarding setup",
      "amount": "2500.00",
      "interval_type": "one_time",
      "interval_count": 0
    }
  ]
}

status is always "succeeded". Sent once, after the payment is committed on our side.

Fields

FieldNotes
session_idThe cks_… returned as public_id when the session was opened. This is how a delivery is matched back to the session you created.
amountThe session total, as a decimal string.
itemsWhat the session carries, echoing the shapes that were sent: pbp_id for a catalogue item, or name / amount / interval_type / interval_count for a partner-priced one.
status"succeeded".

checkout.failed

{
  "event": "checkout.failed",
  "session_id": "cks_3n9k2xQ7m",
  "amount": "5000.00",
  "status": "failed",
  "items": [
    { "pbp_id": "pbp_3n9k2x" }
  ]
}

status is always "failed". The fields are the ones listed above. We intentionally do not expose the specific cause (gateway decline, verification error, timeout) — that detail stays in SUQO's logs.

Pending or transient failures do not produce a webhook. We keep retrying the payment internally and only emit checkout.failed once the outcome is final.

Parse every amount as a decimal. The session total and each item's amount are sent as strings to avoid float rounding — do not read them into a float.

subscription.status_changed

{
  "event": "subscription.status_changed",
  "subscription_id": "c5d91b6d-8948-4e35-bc55-da60b411ff51",
  "previous_status": "inactive",
  "current_status": "active",
  "changed_at": "2026-08-17T10:24:03.381452+00:00"
}
FieldNotes
subscription_idUUID of the subscription that moved.
previous_status / current_statusBoth drawn from the subscription status set — see Subscription statuses.
changed_atISO 8601, UTC, microsecond precision, explicit offset.

Each delivery describes one transition, so a single subscription produces several of these across its life — activation after the first payment, active → due when a renewal comes round, → cancelled when it ends. Branch on the pair, not on the event name alone: inactive → active is a reactivation, pending_checkout → active is a first activation, and the two usually deserve different handling on your side.

Two things this event is not:

  • It is not a payment signal. There is no amount and no payment outcome in the body. A subscription reaching active does not by itself mean money arrived — checkout.succeeded is the event that says that.
  • It is not ordered. Deliveries are retried independently, so a later transition can land before an earlier one. Use changed_at to decide which is newer — see Retries.

Request format

Every delivery is a POST with:

HeaderValue
Content-Typeapplication/json
X-SUQO-Signaturesha256=<hex HMAC-SHA256>
X-SUQO-TimestampUnix timestamp in seconds
User-AgentSUQO-Webhooks/1.0

Example:

POST /hooks/suqo HTTP/1.1
Host: your-app.example.com
Content-Type: application/json
X-SUQO-Signature: sha256=4f2a...c91b
X-SUQO-Timestamp: 1785312724
User-Agent: SUQO-Webhooks/1.0

{"event":"checkout.succeeded","subscription_id":"8f3c...","amount":"1500.00"}

On this page