Events & replay

Every webhook Southbill emits is stored. Query the log and re-deliver anything you missed.

Events & replay

Southbill persists every event it emits for your account — even when no webhook endpoint existed at the time. That makes recovery after an outage a query instead of a support ticket.

Base URL https://api.southbill.com · Auth Authorization: Bearer sk_live_… · Scopes events:read, events:write.

Endpoints

Method Path Scope Purpose
GET /v1/events events:read List events — ?type=, ?created_gte=, ?limit=, ?starting_after=.
GET /v1/events/{id} events:read Retrieve one event.
POST /v1/events/{id}/replay events:write Re-deliver the event to your webhook endpoints.

Catch up after downtime

curl "https://api.southbill.com/v1/events?type=invoice.paid&created_gte=1756000000&limit=100" \
  -H "Authorization: Bearer $SOUTHBILL_SECRET_KEY"
const since = Math.floor(Date.now() / 1000) - 24 * 60 * 60;
const events = await fetch(
  `https://api.southbill.com/v1/events?created_gte=${since}&limit=100`,
  { headers: { Authorization: `Bearer ${process.env.SOUTHBILL_SECRET_KEY}` } },
).then((r) => r.json());

for (const event of events.data) {
  await handle(event); // your own dispatcher, keyed on event.id
}
import os, time, requests

since = int(time.time()) - 24 * 60 * 60
events = requests.get(
    "https://api.southbill.com/v1/events",
    headers={"Authorization": f"Bearer {os.environ['SOUTHBILL_SECRET_KEY']}"},
    params={"created_gte": since, "limit": 100},
    timeout=30,
).json()
<?php
$since = time() - 86400;
$ch = curl_init("https://api.southbill.com/v1/events?created_gte={$since}&limit=100");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SOUTHBILL_SECRET_KEY")],
]);
$events = json_decode(curl_exec($ch), true);

The event object mirrors the webhook body:

{
  "id": "evt_01J…",
  "object": "event",
  "livemode": true,
  "type": "invoice.paid",
  "data": { "object": { "id": "inv_01J…", "status": "paid" } },
  "created": 1756000000
}

Replay an event

curl -X POST https://api.southbill.com/v1/events/evt_01J/replay \
  -H "Authorization: Bearer $SOUTHBILL_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: replay_evt_01J" \
  -d '{ "endpoint": "we_01J" }'
await fetch(`https://api.southbill.com/v1/events/${eventId}/replay`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SOUTHBILL_SECRET_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `replay_${eventId}`,
  },
  body: JSON.stringify({}), // omit "endpoint" to hit every subscribed endpoint
});
requests.post(
    f"https://api.southbill.com/v1/events/{event_id}/replay",
    headers={
        "Authorization": f"Bearer {os.environ['SOUTHBILL_SECRET_KEY']}",
        "Idempotency-Key": f"replay_{event_id}",
    },
    json={},
    timeout=30,
)
{ "id": "evt_01J…", "object": "event", "replayed": true, "endpoints": 2 }
Rule Behaviour
Target endpoint is optional — without it, every enabled endpoint subscribed to that event type receives the delivery.
No match 400 no_endpoint when no enabled endpoint subscribes to the type.
Signature Replays are signed exactly like the original delivery — see Verify signatures.
Idempotency Send an Idempotency-Key so a retried replay request does not double-deliver.

Handlers must stay idempotent: deduplicate on event.id, because a replay reuses the original id.

Rate limits: 300 reads/min, 60 replays/min per key.