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. |
| Mode | Test keys replay test events only. |
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.