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 OK but 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.