What counts as success
Any2xx. Custral reads the status code and nothing else: the response body is
ignored, and a redirect is not followed.
Anything else, including a redirect, a timeout or a connection failure, is a
failed attempt and is retried.
Redirects are not followed
A delivery is a signedPOST. Following a redirect would send that signed body
to whatever address the Location header names, so Custral stops at the 3xx,
records it, and retries the URL you registered. It never requests the
Location.
The common causes are an http:// URL on a server that forces https://, a
trailing slash your framework adds or strips, and an apex domain that redirects
to www. Register the URL your endpoint redirects to.
The retry schedule
Custral makes up to 8 attempts over about 24 hours: the first as soon as the event fires, then 7 retries with a longer wait before each one. The short waits cover a dropped connection or a process restart. The long ones cover a deploy or an outage that runs for hours.
Each wait starts when the previous attempt fails, so an attempt that hits the 10
second timeout adds those 10 seconds to the times above. After an outage, a
delivery can arrive hours after your endpoint is back online.
Two failures skip the retries entirely, because retrying cannot help:
- The endpoint’s URL, or an address its hostname resolves to, does not pass the safety check.
- The subscription was deleted or set inactive after the event was queued.
Your handler must be idempotent
Assume every event can arrive more than once. A retry after a timeout is the common case: your server did the work, the response was slow, Custral never saw it, and the same delivery lands again. Deduplicate on the envelope’sid, which is stable across every attempt of one
delivery:
createdAt is not stable across retries. It is stamped per attempt, so it
is useful for measuring delivery latency and useless as a deduplication key.Order is not guaranteed
Deliveries are processed concurrently, so two events about the same record can arrive out of order. Do not infer sequence from arrival. If order matters, treat the payload as a notification that something changed and read the current state back:Fan-out
One event delivers once per matching subscription. Three endpoints subscribed torecord.created produce three deliveries with three separate ids,
each retried independently. An endpoint that is down does not hold up the others.
The delivery log
Every attempt is recorded against the subscription before anything is sent, so a delivery survives a worker restart. Open an endpoint under Settings → Developers → Webhooks to read the last 100, newest first.
A
failed row is not final while attempts is below 8. Another attempt is
still coming, unless error is webhook_inactive_or_missing or starts with
unsafe_url:, which are never retried.
The log holds one row per delivery, not per attempt.
responseStatus and
error describe the most recent try, so you can see that a delivery failed
three times and what the last failure was, but not the individual history of
each attempt.Error codes
error holds a code, never an error message. A failed connection’s message can
include a network address, and everyone in the workspace can read the delivery
log. Transport failures carry the underlying system code after a colon when
there is one.
Limits worth knowing
What’s next
Troubleshooting
Nothing arriving, or arriving and failing.
Managing subscriptions
Create, rotate, and delete endpoints.