Event catalog
Everything we can send you.
These are the exact events Southbill 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: Southbill-Signature, Southbill-Event-Id, Southbill-Event-Type, Southbill-Environment, Southbill-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.created |
An invoice was created | Mirror it; nothing is collected yet. |
invoice.sent |
The invoice was sent to the customer | Track dunning state in your app. |
invoice.finalized |
The invoice is locked and collectible | Safe to render as an official document. |
invoice.paid / invoice.payment_succeeded |
The invoice was collected | Grant entitlements. |
invoice.payment_failed |
Collection failed; dunning continues | Warn the customer — don't cut access on the first failure. |
invoice.payment_action_required |
The customer must authenticate (3-D Secure) | Ask the customer to finish the payment. |
invoice.upcoming |
A renewal invoice is coming up | Hook for pre-billing notices. |
invoice.marked_uncollectible / invoice.voided |
Written off or voided | Stop dunning. |
subscription.created / .updated |
Subscription started or changed | Sync plan, quantity, period end. |
customer.subscription.trial_will_end |
Trial ends soon | Nudge for a payment method. |
customer.subscription.paused / .resumed |
Billing paused or resumed | Suspend/restore entitlements. |
customer.subscription.deleted |
A subscription ended | Revoke entitlements. |
customer.created / .updated / .deleted |
Customer record changed | Keep your mirror in sync. |
Refunds, disputes and payouts
These arrive as standalone events — no polling required.
| Event | What it means |
|---|---|
charge.refunded |
The refunded total of a charge changed (full or partial). |
refund.created / .updated / .failed |
Lifecycle of a single refund. |
charge.dispute.created |
A chargeback was opened; evidence_due_by is the deadline. |
charge.dispute.updated |
Status or evidence state changed. |
charge.dispute.closed |
Final outcome (won / lost). |
charge.dispute.funds_withdrawn / .funds_reinstated |
Disputed funds debited or returned. |
checkout.session.dispute.created / .closed |
The same chargeback, expressed on the checkout session. |
payout.created / .updated / .paid |
Payout lifecycle to the merchant's bank account. |
payout.failed / .canceled |
The payout did not arrive — see failure_code. |
payout.reconciliation_completed |
The payout is fully reconciled and safe to book. |
Payout events require the restricted payouts:read scope.
Southbill Store (orders & catalogue)
Store events are separate from checkout.session.* on purpose: checkout.session.completed means "money arrived", order.paid means "a Southbill Store order exists and must be fulfilled". Invoice money never appears here — it uses invoice.*.
| Event | What it means | What you should do |
|---|---|---|
order.created |
A store order row was created (also via the Store API) | Mirror the order; it may still be unpaid. |
order.paid |
The store order was paid | Fulfil here. product, variant, order_number are set. |
order.updated |
Status or tracking changed (e.g. shipped) | Sync status, tracking_carrier / tracking_number / tracking_url. |
order.refunded |
The order was refunded | amount_refunded, fully_refunded. |
order.canceled |
The order was cancelled | Release stock, close your record. |
product.created / product.updated |
Catalogue changed (dashboard or Store API) | Re-fetch the product incl. variants and prices. |
The order payload is object: "store.order" with source: "store"; id and payment both hold the checkout session ID. Requires the store:read scope.
Lifecycle
| Event | What it means |
|---|---|
app.installed |
A merchant installed your app (first OAuth grant). |
app.reinstalled |
An existing installation was re-authorized. |
app.uninstalled |
The merchant removed your app — tokens are already revoked. Delete their data within 30 days. |
Minimal handler
export async function POST(req) {
const raw = await req.text();
if (!verifySignature(raw, req.headers.get("southbill-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.