Receive real-time events with signed HTTP requests.
How Southbill delivers webhooks, retries, and how to configure endpoints.
How webhooks work
Webhooks let Southbill notify your server whenever something happens on your account — a payment succeeds, an invoice is paid, a dispute is opened.
Configure endpoints
Dashboard → Developers → Webhooks → Add endpoint
url — must be HTTPS.
description — free-form.
enabled_events — pick the events you want. Use * for everything.
mode — always live. Southbill issues live keys only.
On save Southbill returns a signing secret (whsec_…) — copy it now, it's shown only once.
Delivery guarantees
- HTTP
POST with Content-Type: application/json.
- 2xx = success. Any non-2xx (or timeout > 20 s) triggers exponential backoff retries for up to 72 hours (roughly 15 attempts).
- Delivery attempts are visible per-event under the endpoint.
- Events can arrive out of order. Use
data.id + type for idempotency.
Envelope
{
"id": "evt_01H…",
"object": "event",
"type": "checkout.session.completed",
"created": 1735689600,
"livemode": true,
"data": {
"object": { /* the resource — checkout session, invoice, charge, dispute, payout, … */ }
}
}
The resource always lives at data.object. See Event reference for the exact shape per event type.
Testing
- Southbill has no test mode. Validate with a small real-amount payment you refund afterwards.
- The Developers → Webhooks → Test button sends a synthetic
ping.test event.
HMAC-SHA256 signing scheme — verify before you trust an event.
Verify signatures
Every webhook is signed with your endpoint's whsec_… secret. Verify the signature before parsing the body.
Header
Southbill-Signature: t=1735689600,v1=6a76…f3
t — Unix timestamp of when Southbill generated the signature.
v1 — HMAC-SHA256 of "{t}.{raw_body}" using your whsec_… secret, hex-encoded.
Steps
- Extract
t and v1 from the header.
- Reject if
|now - t| > 300 seconds (replay protection).
- Compute
expected = HMAC_SHA256(secret, t + "." + rawBody).
- Compare
expected with v1 in constant time.
Node.js example
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const t = Number(parts.t);
if (Math.abs(Date.now()/1000 - t) > 300) throw new Error("expired");
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex")
);
if (!ok) throw new Error("bad_signature");
}
PHP example
[$t, $v1] = [null, null];
foreach (explode(',', $_SERVER['HTTP_SOUTHBILL_SIGNATURE']) as $p) {
[$k, $v] = explode('=', $p, 2);
if ($k === 't') $t = (int)$v;
if ($k === 'v1') $v1 = $v;
}
if (abs(time() - $t) > 300) http_response_code(400);
$expected = hash_hmac('sha256', $t.'.'.$raw, $secret);
if (!hash_equals($expected, $v1)) http_response_code(400);
Always use the raw request body — parsing to JSON first will change whitespace and invalidate the signature.
Every event Southbill can send, with the payload shape.
Event reference
These are the exact event types Southbill delivers to Merchant API webhook endpoints. Use "*" to subscribe to all of them. Test endpoints only receive livemode: false events; live endpoints only receive live events.
Checkout
checkout.session.completed
checkout.session.async_payment_pending
checkout.session.async_payment_failed
checkout.session.canceled
checkout.session.refunded
checkout.session.expired
Payments
payment_intent.succeeded
payment_intent.payment_failed
payment_intent.requires_action
payment_intent.canceled
A declined sandbox card produces a failed payment with failure_code and failure_message, plus payment_intent.payment_failed. The Checkout Session remains retryable with another card.
Invoices
invoice.created
invoice.sent
invoice.paid
invoice.partially_paid
invoice.payment_succeeded
invoice.payment_failed
invoice.voided
invoice.installment.paid
invoice.installment.due
invoice.installment.overdue
Store
order.paid
order.updated
order.refunded
Customers
customer.created
customer.updated
customer.deleted
Subscriptions
subscription.created — emitted immediately, including while first-payment confirmation is incomplete.
subscription.updated
subscription.canceled
customer.subscription.deleted
Payload
Every event has id, object: "event", type, created, livemode and a resource snapshot under data.object. Deduplicate deliveries by event.id. Events not listed above are not available in the Merchant API endpoint picker. Payouts remain dashboard-managed and are not part of this Merchant API webhook catalog.