Why an event archive?
Webhooks are the primary way to find out when a submission's ATO status changes. If your handler is down during a delivery attempt we retry with exponential backoff (see Webhooks → Retries), but there are scenarios where even the retry window isn't enough:
- Your endpoint was auto-paused after 10 consecutive failures
- A handler bug quietly returned
200 OKbut dropped events before they were processed - You want to backfill events before your webhook was configured
The Events API is the recovery path for all three. Every delivery we attempt is persisted for 90 days and can be listed, inspected, or re-delivered.
Browsing events
GET /api/v1/events
Scopes: webhooks:read
Cursor-paginated. Newest events first.
Query parameters:
| Param | Default | Notes |
|---|---|---|
limit |
50 | 1–100 |
starting_after |
— | Pass the last event ID from the previous page |
event_type |
— | Filter to a single event type (e.g. submission.accepted) |
Response:
{
"data": [
{
"id": "evt_4f1c2b...",
"event_type": "submission.accepted",
"status": "succeeded",
"attempts": 1,
"created_at": "2026-04-16T02:11:04Z",
"delivered_at": "2026-04-16T02:11:05Z",
"webhook_endpoint_id": 42
}
],
"has_more": true,
"next_cursor": "evt_4f1c2b..."
}
Walking history
cursor=""
while true; do
resp=$(curl -s "https://stp.beeswaxapp.com/api/v1/events?limit=100&starting_after=$cursor" \
-H "Authorization: Bearer stp_live_...")
echo "$resp" | jq -r '.data[] | .id'
more=$(echo "$resp" | jq -r '.has_more')
cursor=$(echo "$resp" | jq -r '.next_cursor')
[ "$more" = "false" ] && break
done
Inspecting a single event
GET /api/v1/events/evt_xxx
Returns the full webhook payload plus delivery diagnostics:
{
"id": "evt_4f1c2b...",
"event_type": "submission.rejected",
"status": "succeeded",
"attempts": 1,
"created_at": "2026-04-16T02:11:04Z",
"delivered_at": "2026-04-16T02:11:05Z",
"webhook_endpoint_id": 42,
"payload": {
"submission": { "id": 501, "status": "rejected", "..." },
"pay_event": { "..." },
"employer": { "..." }
},
"response_code": 200,
"response_body": "ok",
"last_error": null,
"replayed_from_id": null,
"next_retry_at": null
}
Resending an event
POST /api/v1/events/:id/resend
Scopes: webhooks:write
Idempotent: yes — set Idempotency-Key to safely retry
Queues a fresh delivery of the exact same payload. The replay:
- Gets its own evt_... ID
- Carries replayed_from_id pointing back at the original
- Counts against your webhook endpoint's rate limit like any other delivery
Your handler receives the original payload byte-for-byte. That means data.submission.id and every other field is the same — your idempotency key should be event.id, not a hash of the payload, if you want to distinguish replays.
curl -X POST https://stp.beeswaxapp.com/api/v1/events/evt_4f1c2b.../resend \
-H "Authorization: Bearer stp_live_..." \
-H "Idempotency-Key: replay-evt_4f1c2b-2026-04-16"
Bulk replay after an outage
# Re-deliver every rejected/error event from the last 24 hours
curl "https://stp.beeswaxapp.com/api/v1/events?event_type=submission.rejected&limit=100" \
-H "Authorization: Bearer stp_live_..." \
| jq -r '.data[] | select(.created_at > "'"$(date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)"'") | .id' \
| while read id; do
curl -s -X POST "https://stp.beeswaxapp.com/api/v1/events/$id/resend" \
-H "Authorization: Bearer stp_live_..."
done
Retention
Events and their delivery history are retained for 90 days. After that they're purged. If you need longer retention, ingest the webhook payloads into your own datastore — that's the intended architecture for long-term event history.
Rate and quota
Listing and inspecting events count against your normal rate limits. Replays also consume webhook endpoint delivery quota and will themselves fire retries if your handler fails on replay.