Event catalog
Everything we can send you.
These are the exact events spuke delivers to app webhook endpoints today. Anything not listed here is never sent — subscribe only to what exists.
How to think about events
| Concept | Explanation |
|---|---|
| Event = past-tense fact | payment_intent.succeeded means it already happened. You react, you never approve. |
| One action can fire several events | A completed checkout fires payment_intent.succeeded and checkout.session.completed. Use the checkout event as your fulfilment trigger. |
| At-least-once delivery | The same event can arrive twice. Deduplicate on the event id. |
| No ordering guarantee | Events can arrive out of order. If order matters, re-fetch the object from the API. |
| Unknown types are safe to ignore | We add events without a breaking change. Never crash on an unfamiliar type. |
| Subscription is per endpoint | Unsubscribed events are not delivered and not stored. * and checkout.* wildcards are supported. |
The envelope
Every delivery has the same outer shape; only type and data.object differ.
{
"id": "evt_9f1c2ab34d5e6f70",
"object": "event",
"type": "checkout.session.completed",
"environment": "live",
"installation": "3f0c…-uuid",
"created": 1755423672,
"data": { "object": { "…": "resource payload" } }
}
| Field | What it is | Why it matters |
|---|---|---|
id |
Unique event ID, prefix evt_ |
Your idempotency key. Store it and skip duplicates. |
object |
Always "event" |
Lets you store raw payloads generically. |
type |
What happened | The only field you should branch on. |
environment |
test or live |
Never let a sandbox event touch production data. |
installation |
The installation this event belongs to | Routes the event to the right tenant in your app. |
created |
Unix timestamp in seconds (not RFC 3339) | Use it to resolve out-of-order arrivals. |
data.object |
The resource payload at the time of the event | Shape depends on type — see Event payloads. |
Delivery headers: Spuke-Signature, Spuke-Event-Id, Spuke-Event-Type, Spuke-Environment, Spuke-Delivery-Attempt.
Checkout
The checkout session is the object your app should treat as the order.
| Event | What it means | What you should do |
|---|---|---|
checkout.session.completed |
The shopper paid and the session flipped to complete |
Fulfil the order here. Fired exactly once per session. |
checkout.session.expired |
The session timed out unpaid | Release reserved stock, optionally send a recovery mail. |
checkout.session.canceled |
The shopper or the merchant cancelled before payment | Close the order; cancellation_reason may be set. |
checkout.session.async_payment_pending |
A delayed method (e.g. bank debit) is processing | Show "payment pending"; do not ship yet. |
checkout.session.async_payment_failed |
The delayed payment failed | failure_code / failure_message explain why. |
checkout.session.refunded |
The related charge was refunded | amount_refunded and fully_refunded tell you how much. |
Payments (payment intents)
Lower-level than checkout — use these only if you need the payment view.
| Event | What it means | What you should do |
|---|---|---|
payment_intent.succeeded |
Funds captured | Mark paid. Prefer checkout.session.completed for order fulfilment. |
payment_intent.payment_failed |
Declined; failure_code, decline_code, failure_message explain it |
Offer a retry; never retry silently. |
payment_intent.requires_action |
Waiting on the shopper (3-D Secure, app confirmation) | Nothing to do server-side; the shopper continues in checkout. |
payment_intent.canceled |
The payment was cancelled or abandoned | Free reserved inventory. |
Billing
| Event | What it means | What you should do |
|---|---|---|
invoice.payment_failed |
A subscription invoice could not be collected; dunning continues | Warn the customer — don't cut access on the first failure. |
customer.subscription.deleted |
A subscription ended | Revoke entitlements. |
There are currently no invoice.created / invoice.paid / subscription.created app events. Poll /v1/invoices and /v1/subscriptions if you need that state.
Not available yet
Standalone refund.*, dispute.*, payout.* and customer.* events are not delivered to apps. Read that data through the App API instead:
| You want | Use |
|---|---|
| Refund state | checkout.session.refunded event + GET /v1/refunds |
| Dispute state | GET /v1/disputes (no dispute events are pushed to apps) |
| Payouts | GET /v1/payouts (restricted scope) |
| Customers | fields on the payment/invoice objects |
Minimal handler
export async function POST(req) {
const raw = await req.text();
if (!verifySignature(raw, req.headers.get("spuke-signature"), SECRET)) {
return new Response("bad signature", { status: 400 });
}
const event = JSON.parse(raw);
if (await seen(event.id)) return new Response("ok"); // dedupe
await store(event.id);
switch (event.type) {
case "checkout.session.completed": await fulfil(event.data.object); break;
case "checkout.session.refunded": await refund(event.data.object); break;
default: break; // ignore unknown types
}
return new Response("ok"); // 2xx within 10s
}
See Signatures & retries.