Webhook delivery is retried for about five hours before it is abandoned. This page covers how to build a reliable receiver that handles failures gracefully.
Delivery guarantees
- Each delivery attempt has a 10-second timeout.
- If your server does not respond within 10 seconds or returns a non-
2xx status, the delivery is retried with exponential backoff: 30 seconds, then 1, 2, 4, 8, 16 and 32 minutes, then hourly.
- There are 12 attempts in total, so the last one lands roughly five hours after the job completed.
- After the final attempt, delivery is abandoned. The result is always available via the job status endpoint, regardless of webhook delivery outcome.
An outage on your side therefore delays results rather than losing them. If your receiver is down for an hour and comes back, the queued deliveries arrive on their next scheduled attempt. You do not need to ask us to resend them.
Two cases are not retried, because no amount of waiting would fix them. Delivery stops immediately if your webhook URL is malformed, does not use HTTPS, or resolves to a private or internal IP address. Re-register a valid URL with POST /webhook/register.
Do not rely on webhooks as your only mechanism for receiving results. Always fall back to polling
the job status endpoint for critical workloads.
Handling delivery failures
The retry window covers an outage of a few hours, not an outage of a few days. Build a recovery mechanism into your integration for anything longer:
- Persist job IDs on submission. Store the
job_id returned from any of the modality endpoints (POST /moderate/text, POST /moderate/image, POST /moderate/video).
- Poll on startup or recover from failures. On application restart or after a webhook outage, query
GET /job/{id} for any job IDs in a non-terminal state (queued or processing).
- Set a polling timeout. If a job has not completed after a reasonable period (for example, 5 minutes for prompt or image moderation, 15 minutes for video moderation), treat it as failed and investigate.
Idempotency
Your webhook handler may occasionally receive a duplicate delivery. For example, if your server processed a request but responded after the 10-second timeout, the delivery is retried. Every attempt for the same result carries the same id in three places: the delivery_id field in the body, the X-Omnifence-Delivery-Id header, and the webhook-id signature header. Deduplicate on that value.
The body is byte-identical to the original, but the signature is not: each attempt is signed as it is sent, so webhook-timestamp and webhook-signature are fresh and a retry verifies against the default five-minute tolerance. See Verifying a webhook.
There is exactly one webhook per job, so delivery_id and job_id map one to one today. Key your deduplication on delivery_id / webhook-id anyway. That is the value guaranteed to stay unique per delivery.
Responding quickly
Webhook payloads are small, but any processing you do synchronously (database writes, downstream API calls) can push your response time past the 10-second timeout. Acknowledge receipt immediately and process asynchronously.
Security considerations
Every webhook is signed with your account’s signing secret, following the
Standard Webhooks
specification. Verify the signature before you act on a payload — see
Verifying a webhook for the headers,
the signed string and a worked example.
Without that check, anyone who learns your webhook URL can POST a forged
completed result. That matters most for the fields you act on: a spoofed
is_prohibited: false waves prohibited content through your pipeline.
Alongside the signature:
- Use HTTPS for your webhook URL.
- Validate the
job_id against your records before trusting the payload.
- Reject a payload whose signature does not verify; do not fall back to trusting it.
Hybrid approach
For maximum reliability, use webhooks for speed and polling as a fallback:
- Register a webhook to receive results immediately when a job completes.
- Store all submitted
job_id values with a pending state in your database.
- Run a background job every few minutes to poll the job status endpoint for any IDs that remain
pending longer than expected.
Because retries now span hours, a result can arrive by webhook after your polling job has already recovered it. This is another reason to deduplicate on delivery_id.