Skip to main content
Start at the delivery log. Open the endpoint under Settings → Developers → Webhooks: it records every delivery before anything is sent, so the difference between Custral never tried and your server refused is visible there. The error column says which. Every code is listed under Error codes.

Nothing is arriving

The catalog is the complete list of the nineteen events Custral delivers. An event that is not on it never fires, so there is nothing to wait for.
Imports, job runs and AI property fills are suppressed so one import does not become ten thousand deliveries. The record really did change and no webhook was sent. See Bulk writes.
Matching is exact, prefix.*, or *. record.update does not match record.updated, and records.* does not match anything. Read the event list back from the endpoint’s row rather than from memory.
An inactive endpoint records its delivery as failed with webhook_inactive_or_missing and does not retry.
The log shows unsafe_url: and a reason. The URL is re-checked on every attempt, not only when it was created, so a URL edited to something internal stops working immediately. The reasons are listed under What Custral will and will not call.unsafe_url:resolves_to_private_ip means the hostname looks public but its DNS answers with a private address, such as an internal load balancer or a record left over from a staging setup. Every address the hostname resolves to has to be public, so one private answer among several is enough to refuse it.
The log shows redirect_not_followed, with the 3xx in responseStatus. Custral does not follow redirects, so the delivery never reaches the address your server redirects to. 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. A redirect is retried like any other failure, so a deploy that briefly answers with one recovers on its own. See Redirects are not followed.
Deliveries that failed during the outage are retried on the retry schedule: up to 8 attempts over about 24 hours, with waits that grow from 5 seconds to 18 hours. So an event can arrive hours after your endpoint is back online. A failed row with attempts below 8 is still waiting for its next try, unless its error is webhook_inactive_or_missing or starts with unsafe_url:.If all 8 attempts fail, the delivery is not tried again and there is no way to replay it by hand. Reconcile from the API after an outage that lasted longer than about a day.

Signature verification keeps failing

The most common cause by a distance. Verification needs the exact bytes that were signed, and re-serializing a parsed object does not reproduce them. Mount a raw body parser on the webhook route only:
The SDK throws invalid_payload with this exact advice when it is handed an object.
Each endpoint has its own secret. Two endpoints pointing at one URL need the right secret per subscription, or verification fails for one of them.
Rotation is immediate and has no overlap window. Deliveries signed with the new secret fail against the old one from the next event onward.
The default tolerance is 300 seconds either side. A container with a drifted clock rejects perfectly good deliveries. Compare your host’s time against the t value in the header.
Fix the timing leak while you are there, but note this does not usually cause a false negative. If a constant-time compare fails, the bytes really do differ.

The same event arrives twice

Expected, and your handler has to tolerate it. The usual cause is a timeout: your server did the work, the response took more than 10 seconds, Custral never saw it and retried. Deduplicate on the envelope’s id, which is stable across retries of one delivery. createdAt is not: it is stamped per attempt. See Your handler must be idempotent.

Events arrive out of order

Also expected. Deliveries are processed concurrently. If order matters, treat the payload as a nudge and read current state back from the API.

A value looks like gibberish

["opt_3Kd8sM"] is a select’s stored value, not a bug. Select, status, relation and user fields carry ids. Resolve them from the object’s schema, as shown in Payload format.

Still stuck

Quote the delivery id (whd_…) and the endpoint id (hook_…) when you write in. Both are on the delivery log row, and together they identify one specific attempt end to end.

Get support