> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnifence.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook reliability

> Handle delivery failures, duplicate events, and ensure reliable webhook processing

Webhooks are delivered once with no automatic retries. This page covers how to build a reliable receiver that handles failures gracefully.

## Delivery guarantees

* Webhooks are delivered **once** with a **10-second timeout**.
* If your server does not respond within 10 seconds or returns a non-`2xx` status, the delivery is **not retried**.
* The result is always available via the [job status endpoint](/api-reference/endpoint/get-job), regardless of webhook delivery outcome.

<Warning>
  Do not rely on webhooks as your only mechanism for receiving results. Always fall back to polling
  the job status endpoint for critical workloads.
</Warning>

## Handling delivery failures

Because webhooks are not retried, build a recovery mechanism into your integration:

1. **Persist job IDs on submission.** Store the `job_id` returned from any of the modality endpoints (`POST /moderate/prompt`, `POST /moderate/image`, `POST /moderate/video`).
2. **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`).
3. **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.

```javascript theme={null}
async function fetchMissingResults(pendingJobIds) {
  for (const jobId of pendingJobIds) {
    const response = await fetch(`https://api.omnifence.ai/api/v1/job/${jobId}`, {
      headers: { Authorization: `Bearer ${process.env.OMNIFENCE_API_KEY}` },
    });
    const job = await response.json();
    if (job.status === 'completed' || job.status === 'failed') {
      await processResult(job);
    }
  }
}
```

## Idempotency

Your webhook handler may occasionally receive a duplicate delivery (for example, if your server responded slowly). Make your handler idempotent by checking whether you have already processed a given `job_id` before acting on it.

```javascript theme={null}
app.post('/webhooks/omnifence', async (req, res) => {
  const { job_id, status, is_prohibited } = req.body;

  // Return early if already processed
  if (await db.isProcessed(job_id)) {
    return res.status(200).send({ ok: true });
  }

  await processResult({ job_id, status, is_prohibited });
  await db.markProcessed(job_id);

  res.status(200).send({ ok: true });
});
```

## 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.

```javascript theme={null}
app.post('/webhooks/omnifence', async (req, res) => {
  // Acknowledge immediately
  res.status(200).send({ ok: true });

  // Process asynchronously (don't await)
  processResult(req.body).catch((err) => logger.error(err));
});
```

## Security considerations

The webhook request does not include a signature. To prevent spoofed payloads:

* Use HTTPS for your webhook URL.
* Validate the `job_id` against your records before trusting the payload.
* Optionally, verify the result by calling `GET /job/{id}` before acting on a webhook.

## Hybrid approach

For maximum reliability, use webhooks for speed and polling as a fallback:

1. Register a webhook to receive results immediately when a job completes.
2. Store all submitted `job_id` values with a `pending` state in your database.
3. Run a background job every few minutes to poll the job status endpoint for any IDs that remain `pending` longer than expected.
