Delivery & retries
Curviate delivers every event at least once with automatic retries and transparent endpoint health tracking.
At-least-once delivery
Every event is delivered at least once, so the same event can arrive twice. The
top-level id (wdl_<ulid>) is unique per delivery attempt and changes on every
retry. Deduplicate on event + data.account_id + data.occurred_at instead.
Retry schedule
A non-2xx response or a timeout schedules a retry with exponential backoff, up to
5 attempts in total:
| Attempt | Delay after previous failure | Total elapsed from first attempt |
|---|---|---|
| 1 | Immediate | 0 s |
| 2 | 30 seconds | ~30 s |
| 3 | 5 minutes | ~5 m 30 s |
| 4 | 30 minutes | ~36 m |
| 5 | 2 hours | ~2 h 36 m |
Each attempt has a 10-second HTTP timeout; no 2xx within it counts as a failure.
Verify the signature, return 200, then process asynchronously.
Slow work inside the request risks the 10-second timeout and a needless retry.
Endpoint health
GET /v1/webhooks is the only endpoint that returns health:
curl "https://api.curviate.com/v1/webhooks" \
-H "Authorization: Bearer cvt_live_YOUR_API_KEY"Each item carries health, last_delivery_at, and delivery_success_rate_7d.
GET /v1/webhooks/{id} omits all three.
| Status | Meaning |
|---|---|
ok | The most recent delivery attempt succeeded. |
degraded | All 5 attempts for the last delivery failed. The webhook keeps receiving new events. Rarely, it means the webhook's signing secret could not be used: the delivery is recorded as failed without being sent or retried, and no later delivery can succeed. Delete the webhook and create a new one to get a new secret. |
degraded is a signal, not a gate. Nothing pauses, and there is no re-enable
control. When a webhook is degraded:
- If it turned
degradedright after an event, rather than after the retry window, and your endpoint never received a request, the webhook's signing secret could not be used and no delivery can succeed: delete the webhook and create a new one. Nothing on your endpoint will fix it. - Otherwise, fix your endpoint (firewall, TLS certificate, response codes). The next
successful delivery returns
healthtook. - Deliveries that exhausted their 5 attempts are not replayed. Backfill from the API if you need them.
- To stop deliveries, set
enabledtofalsewithPATCH /v1/webhooks/{id}or delete the webhook. Both apply immediately, queued retries included. Re-enabling does not replay what was dropped; new events flow from that moment.
Delivery history
delivery_success_rate_7d is the percentage (0-100) of the last 7 days' attempts
that succeeded. Each retry counts as its own attempt, so a delivery that succeeded on
its third try adds two failures and one success. It is null, not 100, when there
were no attempts. last_delivery_at is the most recent attempt, successful or not:
0 with a timestamp means failing, null and null means never used.
Per-attempt records are not in the API; only the dashboard lists them.
Errors you may hit
Delivery failures never return an API error; they show as degraded health and a
falling delivery_success_rate_7d. These come from the management endpoints.
| Code | HTTP | Cause | Fix |
|---|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API key. | Send Authorization: Bearer cvt_live_<key>. |
RESOURCE_NOT_FOUND | 404 | The webhook id is well-formed but not yours. | Confirm it with GET /v1/webhooks. |
INVALID_REQUEST | 400 | A malformed webhook id, or a PATCH body field that is not accepted. | Ids are wh_ plus 26 base32 characters. |
Full envelope shapes are in the error reference.
Next steps
- Verifying signatures: why a rejected delivery costs you four retries.
- Webhooks overview: creating, listing, and deleting a webhook.
- Event reference: what each event carries.