# 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

```bash
curl "https://api.southbill.com/v1/events?type=invoice.paid&created_gte=1756000000&limit=100" \
  -H "Authorization: Bearer $SOUTHBILL_SECRET_KEY"
```

```node
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
}
```

```python
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
<?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:

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

## Replay an event

```bash
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" }'
```

```node
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
});
```

```python
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,
)
```

```json
{ "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](/docs/webhooks/signature-verification). |
| 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.

