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

# Error recovery

> Handle failed jobs, transient errors, and submission failures

This page covers how to handle errors at each stage of a moderation request — submission, processing, and result retrieval.

## Error types

| Stage            | What can go wrong           | Recovery                                                    |
| ---------------- | --------------------------- | ----------------------------------------------------------- |
| Submission       | `4xx` validation error      | Fix the request and resubmit                                |
| Submission       | `5xx` server error          | Retry with exponential backoff                              |
| Processing       | Job reaches `failed` status | Resubmit the original request                               |
| Result retrieval | `404 JOB_NOT_FOUND`         | Check the job ID; the job may belong to a different account |
| Result retrieval | `401 UNAUTHORIZED`          | Refresh or verify your API key                              |

## Transient vs permanent errors

**Transient errors** are temporary and safe to retry:

* `429 RATE_LIMITED` — wait and retry (see [rate limiting](/platform/rate-limiting))
* `500 INTERNAL_ERROR` — server-side failure; retry with backoff
* `503` — service degraded; retry with backoff

**Permanent errors** should not be retried without changing something:

* `400 INVALID_REQUEST` — fix the request (missing fields, invalid file format, file too large)
* `401 UNAUTHORIZED` — provide a valid API key
* `403 ACCOUNT_TERMINATED` — contact support; the account cannot be recovered programmatically

## Retrying submissions

For `5xx` errors, use exponential backoff to avoid overwhelming the API during an outage.

```javascript theme={null}
async function submitWithRetry(formData, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await fetch('https://api.omnifence.ai/api/v1/moderate/image', {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.OMNIFENCE_API_KEY}` },
      body: formData,
    });

    if (response.ok) return response.json();

    const error = await response.json();

    // Don't retry permanent errors
    if (response.status < 500) throw new Error(error.message);

    // Exponential backoff for server errors
    if (attempt < maxAttempts) {
      await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt));
    }
  }

  throw new Error('Max retry attempts reached');
}
```

## Handling failed jobs

A job with `status: "failed"` means processing encountered an unrecoverable error after the job was accepted. `is_prohibited` will be `null`.

```json theme={null}
{
  "is_prohibited": null,
  "categories": {},
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "image",
  "status": "failed",
  "created_at": "2026-03-24T12:00:00.000Z",
  "completed_at": "2026-03-24T12:00:10.000Z"
}
```

Failed jobs should be resubmitted as new requests. The original `job_id` cannot be retried.

## Rate limit recovery

When you receive a `429 RATE_LIMITED` response, the error body includes details:

```json theme={null}
{
  "error": "RATE_LIMITED",
  "message": "Rate limit exceeded",
  "statusCode": 429
}
```

Wait before retrying. The standard rate limit window is 60 seconds. See [rate limiting](/platform/rate-limiting) for limits by tier.

## Checking job status after a timeout

If your application restarts or a webhook is missed, poll the job status endpoint to recover in-flight jobs:

```javascript theme={null}
async function pollUntilComplete(jobId, timeoutMs = 300_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    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') return job;

    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
  throw new Error(`Job ${jobId} did not complete within timeout`);
}
```
