Delivery & retries
Curviate delivers every event at least once with automatic retries and transparent endpoint health tracking.
At-least-once delivery
Curviate guarantees that every event is delivered at least once. Under normal conditions each event produces a single POST to your endpoint. In rare cases, such as network blips or a retry that succeeds after an earlier attempt appeared to time out, you may receive the same event more than once.
Every delivery payload includes an id field (format: wdl_<ulid>) that is unique
per delivery attempt. Use the combination of event + data.account_id +
data.occurred_at as a natural idempotency key on your side to detect and safely
discard duplicates.
Retry schedule
When your endpoint returns a non-2xx response or the connection times out,
Curviate schedules a retry with exponential backoff. Up to 5 attempts are made
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. If your server does not return a
2xx within 10 seconds, the attempt is recorded as failed and the next retry is
scheduled.
Acknowledge the delivery immediately with a 200 after signature
verification, then process the event asynchronously. Handler logic that runs
synchronously inside the webhook request risks hitting the 10-second timeout and
triggering an unnecessary retry.
Endpoint health
Curviate tracks the health of each registered webhook. Read it from the API with
GET /v1/webhooks, which is the only endpoint that returns it:
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} deliberately omits all three, so a single-webhook read will
never answer "is this endpoint healthy".
| Status | Meaning |
|---|---|
ok | The most recent delivery attempt succeeded. |
degraded | All 5 attempts for the last delivery failed. Retries for that delivery are exhausted; the webhook itself keeps receiving new events. |
degraded is an observability signal, not a gate. Nothing in the delivery path
filters on health: fan-out selects on enabled and deletion only, so a degraded
webhook continues to receive every new event it is subscribed to. There is no
re-enable control to find, and nothing is paused waiting for you.
When a webhook becomes degraded:
- Fix the issue on your endpoint (check firewall rules, TLS certificate, response codes).
- New events continue to arrive; the next successful delivery returns
healthtook. - Deliveries that already exhausted their 5 attempts are not replayed automatically, so anything that failed during the outage is lost. Backfill from the API if you need it.
- If you genuinely want deliveries to stop, either set
enabledtofalsewithPATCH /v1/webhooks/{id}or delete the webhook. Both take effect immediately, including for retries that were already queued. This is a stop, not a pause: attempts dropped while a webhook was disabled are abandoned, and re-enabling it does not replay them. You receive new events from the moment you re-enable, not a burst of stale ones.
Delivery history
delivery_success_rate_7d on GET /v1/webhooks is the percentage (0-100) of the last
7 days' attempts that succeeded, computed from individual attempt records; each retry
counts as its own attempt, so a delivery that succeeded on its third try contributes
two failures and one success. It is null, not 100, when there were no attempts in
the window: no data is not a perfect score. Pair it with last_delivery_at, which
carries the most recent attempt whether or not it succeeded, to tell a webhook that is
failing (0 with a timestamp) from one that has never been used (null and null).
Per-attempt records themselves are not exposed through the API. GET /v1/webhooks is
the API-side view of delivery health; the dashboard is the only surface that lists
individual attempts.
Idempotency recap
Because delivery is at-least-once, your handler may receive the same logical
event more than once across retries. Deduplicate on the combination of
event, data.account_id, and
data.occurred_at in your own datastore. The id
field (wdl_...) uniquely identifies a delivery attempt,
not a logical event; two attempts for the same event will have different
id values.
Errors you may hit
Delivery failures never surface as an API error to you; they surface as health
turning degraded and as a falling delivery_success_rate_7d. The errors below come
from the management endpoints on this page.
| 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.