retries, auto-disable, and the poll you still need
Delivery and reconciliation
How a delivery is attempted
- HTTPS on port 443 only. The hostname must resolve to public addresses only, checked again before every attempt. No redirects are followed: a
3xxis a failed attempt. - An attempt has 10 seconds, of which 3 to connect.
- A
2xxis delivered. Anything else is retried: a3xx,4xxor5xx, a timeout, a refused connection, or an address the check refuses. - Retries come at +1 min, +5 min, +30 min, +2 h and +8 h after the attempt that failed. If the sixth attempt fails too, about 10.6 hours after the first, the delivery is
failedand is not tried again. You can still redeliver it. - Each attempt is signed when it is sent, so its
tis that attempt's time. See verify. - Deliveries go out in parallel. One slow account or endpoint does not hold up the others, and an endpoint that times out is not tried again until the next round, about 15 seconds later. Do not depend on order: fetch the object and compare.
When a subscription is turned off
- By the server. When every attempt to a subscription has failed for three days, it is set
active: false. The clock starts at the first failure and runs while new attempts keep failing; any delivered attempt resets it. A single failed event alone stops after its six attempts, so it does not disable anything. - Telling you. A
webhook.disabledevent goes to every other active subscription of the account, whatever theirevents.data.idnames the one that was turned off. With no other subscription,GET /v1/mereports it inwebhooks.disabled, which counts only subscriptions the server turned off. Check it. - By you.
PATCHwithactive: falsepauses a subscription. Nothing new is queued for it while it is off, and a ping or redeliver on it answers409 conflict. - Turning it back on.
PATCHwithactive: true. What was still waiting when it went off is not replayed: those deliveries are closed asfailedwith the errornot sent: the subscription was off; redeliver it if you need it. Redeliver the ones you need. New events flow from then on.
The delivery log
GET /v1/webhooks/{id}/deliveries lists one subscription's deliveries, newest first. It keeps every delivery still waiting, plus the last 100 that finished (delivered or failed), for 30 days.
A row is one delivery of one event, not one attempt:
| field | what it holds |
|---|---|
id |
this delivery. A redelivery is a new row with a new id |
eventId |
the event's id, the same on a redelivery |
event |
the event type |
status |
pending (waiting or between retries), delivered or failed |
attempts |
how many attempts were made |
responseStatus |
the HTTP status of the last attempt, or null if none answered |
error |
why the last attempt failed, or why it was not sent |
createdAt |
when the delivery was queued |
deliveredAt |
when your endpoint accepted it |
nextAttemptAt |
when the next attempt is due, or null |
Only the last attempt is kept. To see the spacing of attempts, read your endpoint's own log.
POST /v1/webhooks/{id}/deliveries/{did}/redeliver queues the same event again, under the same event id.
At most 5,000 deliveries can wait for one subscription. Beyond that the oldest are closed as failed with the reason in error, and are still listed.
Limits
| call | limit |
|---|---|
POST /v1/webhooks |
20 created / hour / user. A refused URL does not use one |
checking a URL (POST, or PATCH with url) |
60 / hour / user, refused ones included |
POST /v1/webhooks/{id}/ping |
60 / hour / user |
POST …/redeliver |
60 / hour / user |
| subscriptions | 25 per account |
These windows slide: a slot frees one hour after it was used. Every /webhooks call also counts against your credential's request buckets and carries the RateLimit-* headers. See rate limits.
Inbound is best-effort
Inbound is best-effort and at-most-once. The change feed this API listens to makes one attempt per event and never retries, and its queue is not crash-safe. A dropped event is not detectable from either side.
Read that again before you design around it. It means webhooks are a latency optimisation, not a source of truth.
Reconcile on a timer
Keep a high-water mark and sweep with updated_since.
curl -s "https://api.inittasks.com/v1/todos?updated_since=2026-09-15T17:00:00Z" \
-H "Authorization: Bearer $INITTASKS_TOKEN"Poll every few minutes and treat webhooks as the thing that usually makes the poll find nothing. Any integration that assumes every event arrives will be quietly wrong sooner or later, and quietly is the hard part.
Advance the mark only after the sweep has been processed. Advancing it first loses everything in the window if your handler dies.
See pagination and filtering for walking the pages a sweep returns.
Handling duplicates
A retry can deliver an event you already processed. Key on the event id from X-InitTasks-Event-Id and skip one you have seen. The id is the same on a retry, on a redelivery, and on every subscription of yours that hears about the same change.
Your reconciliation sweep will also re-deliver work the webhook already did. Make the handler idempotent and both problems go away at once.
Next
- Rate limits for how often to sweep.
- Reference for /webhooks