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

# Error Handling

> Handle API errors with structured error responses and codes

## HTTP Status Codes

| Status Code | Meaning | Description |
| - | - | - |
| `200` | OK | Request succeeded |
| `201` | Created | Resource was successfully created |
| `202` | Accepted | Request accepted for async processing |
| `204` | No Content | Request succeeded with no response body |
| `400` | Bad Request | Invalid request data or parameters |
| `401` | Unauthorized | Missing API key, empty Bearer token, or a malformed or invalid API key |
| `403` | Forbidden | Insufficient permissions, or the API key lacks the scope the call needs |
| `404` | Not Found | Resource doesn't exist |
| `422` | Unprocessable Entity | The request was well-formed but could not be processed. Read `error_code` for the specific reason |
| `429` | Too Many Requests | Rate limit exceeded. Check the `Retry-After` header |
| `500` | Internal Server Error | Unexpected server error |
| `502` | Bad Gateway | Upstream service error |
| `503` | Service Unavailable | Service temporarily unavailable |
| `504` | Gateway Timeout | Backend did not respond in time |

## Error Response Format

Every error response follows one JSON structure:

```json theme={null}
{
  "error": "Brief, human-readable error description",
  "detail": "Detailed explanation of what went wrong",
  "error_code": "MACHINE_READABLE_ERROR_CODE",
  "retryable": false,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

| Field | Description |
| - | - |
| `error` | Short, human-readable summary |
| `detail` | Detailed explanation for debugging. A validation error sends an array here |
| `error_code` | Machine-readable code for programmatic handling |
| `retryable` | Whether the client should retry this request |
| `request_id` | Unique request identifier for debugging and support |

## Error Codes Reference

The status column is the registry default; a route may return a different status for the same code via a per-call override.

| Error Code | Default HTTP Status | Retryable | Description |
| - | - | - | - |
| `ACCESS_DENIED` | 403 | No | You do not have access to this resource. |
| `AGENTIC_MULTIFILE_UNSUPPORTED` | 409 | No | Agentic extraction cannot process a multi-file document. |
| `ASSISTANT_SPEND_LIMIT_REACHED` | 429 | No | The organization has reached its daily spend limit for the assistant. |
| `ATTACHMENT_LIMIT_EXCEEDED` | 422 | No | This would put too many attached files on the conversation. |
| `AUTH_FAILED` | 401 | No | Authentication failed. |
| `CONCURRENT_EDIT` | 409 | Yes | Someone else changed this at the same time. Reload it and try again. |
| `DATASET_DOCUMENT_REJECTED` | 422 | No | A file in the dataset document was rejected. |
| `DATASET_GROUND_TRUTH_SAVE_FAILED` | 422 | No | The ground truth could not be saved. |
| `DATASET_INVALID_REQUEST` | 422 | No | The dataset registration request is not valid. |
| `EMPTY_EVAL` | 422 | No | The eval has no rows to run. |
| `ENDPOINT_RETIRED` | 410 | No | This endpoint has been retired. Use the successor named in the Link header. |
| `EXPORT_EXPIRED` | 410 | No | This export has expired. Run it again to get a fresh archive. |
| `EXTRACT_MODE_FORBIDDEN` | 403 | No | This organization cannot use that extract mode. |
| `EXTRACT_NODE_UNRESOLVED` | 422 | No | The request does not say which extract node to use. |
| `FILENAME_CONFLICT` | 409 | No | A file with that name already exists. |
| `FILE_NOT_IN_DATASET` | 409 | No | The ground truth cites a file the dataset does not hold. |
| `FILE_TOO_LARGE` | 413 | No | The file is larger than the maximum allowed size. |
| `GATEWAY_TIMEOUT` | 504 | Yes | The upstream service did not answer in time. |
| `GRAPH_READBACK_FAILED` | 502 | No | The workflow was saved and its graph could not be read back. |
| `GT_INVALID` | 422 | No | The ground truth is not valid. |
| `IDEMPOTENCY_KEY_REUSED` | 422 | No | The idempotency key was already used with a different request. |
| `IMMUTABLE_WORKFLOW` | 409 | No | This is a system workflow and it cannot change. |
| `INSUFFICIENT_CREDIT` | 402 | No | The organization has no extraction credit left. |
| `INTEGRATION_NOT_CONNECTED` | 404 | No | The organization has no active integration. |
| `INTERNAL_ERROR` | 500 | Yes | An internal error stopped the request. |
| `INTERNAL_INCONSISTENCY` | 400 | No | The server found an internal inconsistency. |
| `INVALID_API_KEY` | 401 | No | The API key is not valid. |
| `INVALID_JSON` | 400 | No | The request body is not valid JSON. |
| `INVALID_LOOKUP_FILE_ENCODING` | 400 | No | The lookup file uses an encoding the server cannot read. |
| `INVALID_STATE` | 400 | No | The state value is not valid or it expired. |
| `INVALID_VALUE` | 400 | No | A value in the request is not valid. |
| `INVALID_WORKFLOW_CONFIG` | 400 | No | The workflow holds a node configuration that is not valid. |
| `JOIN_REQUEST_CONFLICT` | 409 | No | A join request for this organization already exists. |
| `KNOWLEDGE_NOT_ENABLED` | 409 | No | The knowledge base is not enabled. |
| `KNOWLEDGE_NOT_READY` | 409 | Yes | The knowledge base is not ready yet. |
| `KNOWLEDGE_THREAD_MISMATCH` | 409 | No | The thread belongs to another workflow. |
| `KNOWLEDGE_UNSUPPORTED_SNAPSHOT_FORMAT` | 409 | No | The knowledge base's snapshot format is not supported. |
| `LOOKUP_FIELD_WITHOUT_FILE` | 400 | No | A lookup field has no lookup file. |
| `MEMBERSHIP_CONFLICT` | 409 | No | The user is already a member of this organization. |
| `METHOD_NOT_ALLOWED` | 405 | No | The HTTP method is not allowed for this resource. |
| `MISSING_API_KEY` | 401 | No | The request carries no API key. |
| `NOT_ACCEPTABLE` | 406 | No | No representation matches the request's Accept header. |
| `NOT_ASSIGNED_VERIFIER` | 403 | No | Another user is the assigned verifier for this file. |
| `NOT_FOUND` | 404 | No | The resource does not exist. |
| `NOT_IMPLEMENTED` | 501 | No | This endpoint is not implemented. |
| `NOT_IN_DATASET` | 409 | No | The document is not in the workflow's dataset. |
| `NO_ACTIVE_ORGANIZATION` | 400 | No | No active organization selected. |
| `OBJECT_STORAGE_UNAVAILABLE` | 502 | Yes | Object storage did not complete the request. |
| `OPERATOR_DEPRECATED` | 400 | No | This workflow version uses a deprecated operator and cannot run. |
| `OPERATOR_NOT_AVAILABLE` | 400 | No | This workflow uses an operator that is no longer available. |
| `OPTIMIZATION_ALREADY_RUNNING` | 409 | Yes | An optimization is already running for this workflow. |
| `PARSE_MODE_FORBIDDEN` | 403 | No | This organization cannot use that parse mode. |
| `PASSWORD_VALIDATION_FAILED` | 400 | No | The password does not meet the requirements. |
| `PAYMENT_GATEWAY_ERROR` | 502 | Yes | The payment gateway did not complete the request. |
| `PAYMENT_REQUIRED` | 402 | No | Payment is required to continue. |
| `PERMANENT_REQUIRES_ANYFORMAT_OWNER` | 400 | No | Permanent debug is only available to an organization an AnyFormat user owns. |
| `PRECONDITION_FAILED` | 412 | Yes | The results are not available yet. |
| `RATE_LIMITED` | 429 | Yes | Too many requests. |
| `REQUEST_TOO_LARGE` | 400 | No | The request body is larger than the maximum allowed size. |
| `RUN_IN_PROGRESS` | 409 | Yes | A run is still in progress. Wait for it to finish, then delete again. |
| `RUN_NOT_FINISHED` | 409 | Yes | The run has not finished yet. |
| `SLACK_CHANNELS_FAILED` | 502 | Yes | Slack did not return the channels. |
| `SLACK_CHANNEL_INACCESSIBLE` | 502 | No | The Slack channel is not accessible. |
| `SLACK_EXCHANGE_FAILED` | 400 | No | The Slack token exchange failed. |
| `SLACK_OAUTH_UNCONFIGURED` | 503 | No | Slack is not configured on this deployment. |
| `SLACK_TEST_DELIVERY_FAILED` | 502 | Yes | Slack did not accept the test message. |
| `SPLIT_HAS_NO_PAGES` | 409 | No | The split holds no pages. |
| `SPLIT_SOURCE_NOT_PDF` | 409 | No | The split's source document is not a PDF. |
| `SPLIT_WORKFLOW_DATASET_UNSUPPORTED` | 409 | No | Split workflows do not support datasets yet. |
| `SUBSCRIPTION_ALREADY_ACTIVE` | 409 | No | The organization already has an active subscription. |
| `TAG_NAME_CONFLICT` | 409 | No | A tag with that name already exists. |
| `TOPOLOGY_INVALID` | 400 | No | The workflow topology is not valid. |
| `UNKNOWN_FILTER_PARAM` | 422 | No | The request has an unknown filter parameter. |
| `UNSUPPORTED_FILE_TYPE` | 400 | No | The file type is not supported. |
| `UNSUPPORTED_LOOKUP_FILE` | 400 | No | The lookup file type is not supported. |
| `UNSUPPORTED_MEDIA_TYPE` | 415 | No | The request media type is not supported. |
| `UPLOAD_SNIFF_BUDGET_EXCEEDED` | 503 | Yes | The server could not check the uploaded files in time. |
| `URL_UNREACHABLE` | 422 | No | The URL's hostname could not be resolved. |
| `VALIDATION_ERROR` | 400 | No | The request failed validation. |
| `WEBHOOK_SUBSCRIPTION_LIMIT` | 429 | No | The organization has reached its webhook subscription limit. |
| `WORKFLOW_CONCURRENT_EDIT` | 503 | No | Another request is editing this workflow. |
| `WORKFLOW_NAME_CONFLICT` | 409 | No | A workflow with that name already exists. |
| `ZERO_DATA_RETENTION_REFUSES_LANGFUSE_DEBUG` | 400 | No | This organization keeps no data, so debug capture cannot start. |
| `deprecated_operator_ack_required` | 409 | No | This workflow uses a deprecated operator, so the save needs an acknowledgement. |
| `insufficient_api_key_scope` | 403 | No | The API key does not carry the scope this call needs. |
| `stripe_customer_not_provisioned` | 409 | No | The organization has no billing account yet. |
| `terms_acceptance_required` | 403 | No | You must accept the terms of service to continue. |
| `too_many_in_request` | 409 | No | The request names more items than one call accepts. |
| `voucher_already_redeemed_by_user` | 400 | No | You already redeemed this voucher. |
| `voucher_expired` | 400 | No | This voucher expired or it reached its usage limit. |
| `voucher_no_membership` | 403 | No | You are not a member of this organization. |
| `voucher_not_found` | 400 | No | That voucher code does not exist. |
| `voucher_only_for_new_orgs` | 400 | No | This voucher only applies to a new organization. |

### Authentication Errors

**AUTH\_FAILED** (401). The API key is invalid.

```json theme={null}
{
  "error": "Authentication failed",
  "detail": "Invalid or missing authentication credentials.",
  "error_code": "AUTH_FAILED",
  "retryable": false,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

**MISSING\_API\_KEY** (401). The request has no `Authorization` header, or its Bearer token is empty.

```json theme={null}
{
  "error": "The request carries no API key.",
  "detail": "No API key was provided. Send it as 'Authorization: Bearer <your-api-key>'. Get a key at https://app.anyformat.ai/api-key.",
  "error_code": "MISSING_API_KEY",
  "retryable": false,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

**ACCESS\_DENIED** (403). The API key is valid, but it has no access to this resource.

```json theme={null}
{
  "error": "You do not have access to this resource.",
  "detail": "You do not have permission to perform this action.",
  "error_code": "ACCESS_DENIED",
  "retryable": false,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

### Validation Errors

**VALIDATION\_ERROR** (400). The `detail` field holds an array of validation error objects.

```json theme={null}
{
  "error": "Validation failed",
  "detail": [
    {"type": "missing", "loc": ["body", "name"], "msg": "Field required"},
    {"type": "missing", "loc": ["body", "fields"], "msg": "Field required"}
  ],
  "error_code": "VALIDATION_ERROR",
  "retryable": false,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

**INVALID\_JSON** (400)

```json theme={null}
{
  "error": "Invalid JSON format",
  "detail": "The request body contains invalid JSON. Please check your JSON syntax.",
  "error_code": "INVALID_JSON",
  "retryable": false,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

### Resource Errors

**NOT\_FOUND** (404)

```json theme={null}
{
  "error": "Resource not found",
  "detail": "The requested resource could not be found.",
  "error_code": "NOT_FOUND",
  "retryable": false,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

### Rate Limiting

**RATE\_LIMITED** (429). The response carries a `Retry-After` header.

```json theme={null}
{
  "error": "Rate limited",
  "detail": "Rate limit exceeded. Try again in 12s.",
  "error_code": "RATE_LIMITED",
  "retryable": true,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

`Retry-After` gives the number of seconds to wait before retrying, and appears only on a 429 response. Every successful response carries `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` for the tier that applies to that endpoint, extraction or general.

### Server Errors

**INTERNAL\_ERROR** (500, or 502/503 when the gateway passes an unstructured upstream failure through with its status)

```json theme={null}
{
  "error": "Internal server error",
  "detail": "An unexpected error occurred while processing your request. Please try again or contact support.",
  "error_code": "INTERNAL_ERROR",
  "retryable": true,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

**GATEWAY\_TIMEOUT** (504)

```json theme={null}
{
  "error": "Gateway timeout",
  "detail": "The backend did not respond in time. Please try again.",
  "error_code": "GATEWAY_TIMEOUT",
  "retryable": true,
  "request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```

Quote the `request_id` when you contact support; it is the sole correlation handle for an error.

## Error Handling Best Practices

### 1. Check HTTP Status Codes

```python theme={null}
import requests

response = requests.get(
    "https://api.anyformat.ai/v3/workflows/",
    headers={"Authorization": "Bearer your-api-key"}
)

if response.status_code == 200:
    workflows = response.json()
elif response.status_code == 401:
    print("Authentication failed. Check that you send a valid API key.")
elif response.status_code == 403:
    print("Access denied. The API key has no access to this resource.")
elif response.status_code == 404:
    print("Resource not found.")
elif response.status_code == 429:
    retry_after = response.headers.get("Retry-After", "5")
    print(f"Rate limited. Retry after {retry_after}s.")
elif response.status_code >= 500:
    print("Server error. Retry with backoff.")
else:
    error = response.json()
    print(f"Error: {error.get('detail')}")
```

### 2. Use Error Codes for Logic

```python theme={null}
def handle_api_error(response):
    if response.status_code >= 400:
        error_data = response.json()
        error_code = error_data.get('error_code')

        if error_code in ('MISSING_API_KEY', 'INVALID_API_KEY', 'AUTH_FAILED'):
            print("Missing or invalid API key.")
        elif error_code == 'ACCESS_DENIED':
            print("The API key has no access to this resource.")
        elif error_code == 'VALIDATION_ERROR':
            show_validation_error(error_data.get('detail'))
        elif error_code == 'NOT_FOUND':
            handle_missing_resource()
        elif error_code == 'RATE_LIMITED':
            retry_after = int(response.headers.get('Retry-After', 5))
            time.sleep(retry_after)
            return retry_request()
        elif error_code == 'INTERNAL_ERROR':
            retry_with_backoff()
        elif error_code == 'GATEWAY_TIMEOUT':
            retry_with_backoff()
        else:
            log_unexpected_error(error_data)
```

### 3. Implement Retry Logic

Use exponential backoff for the retryable errors: 5xx and 429.

```python theme={null}
import time
import random

def api_request_with_retry(url, headers, max_retries=3):
    for attempt in range(max_retries + 1):
        try:
            response = requests.get(url, headers=headers)

            # Don't retry non-retryable client errors
            if 400 <= response.status_code < 500 and response.status_code != 429:
                return response

            # Handle rate limiting
            if response.status_code == 429:
                delay = int(response.headers.get('Retry-After', 5))
                time.sleep(delay)
                continue

            # Retry server errors (5xx)
            if response.status_code >= 500 and attempt < max_retries:
                delay = (2 ** attempt) + random.uniform(0, 1)
                time.sleep(delay)
                continue

            return response

        except requests.exceptions.RequestException as e:
            if attempt < max_retries:
                delay = (2 ** attempt) + random.uniform(0, 1)
                time.sleep(delay)
                continue
            raise e
```

### 4. Validate Before Sending

```python theme={null}
def create_workflow(name, description, nodes, edges):
    # Validate required fields locally first
    if not name:
        raise ValueError("Workflow name is required")
    if not nodes:
        raise ValueError("At least one node is required")

    node_ids = {node.get("id") for node in nodes}
    for edge in edges:
        if edge.get("source") not in node_ids or edge.get("target") not in node_ids:
            raise ValueError("Every edge must connect two declared nodes")

    # Make API request
    response = requests.post(
        "https://api.anyformat.ai/v3/workflows/",
        headers={"Authorization": "Bearer your-api-key", "Content-Type": "application/json"},
        json={"name": name, "description": description, "nodes": nodes, "edges": edges}
    )

    return handle_response(response)
```

### 5. Log Errors for Debugging

```python theme={null}
import logging

def log_api_error(response, context=""):
    # Log codes and ids only: `detail` and the raw body can echo document content.
    try:
        error_data = response.json()
        error_code = error_data.get('error_code', 'UNKNOWN')
        request_id = error_data.get('request_id', 'N/A')
    except ValueError:
        error_code, request_id = 'NON_JSON_RESPONSE', 'N/A'

    logging.error(
        f"API Error {response.status_code}: {error_code} "
        f"[Context: {context}] [Request: {request_id}]"
    )
```

## Testing Error Scenarios

When building your integration, test these common scenarios:

| Scenario | How to Test |
| - | - |
| Missing API key | Omit the `Authorization` header |
| Invalid API key | Use a fake or expired key |
| Missing required fields | Create a workflow without `name` or `nodes` |
| Invalid JSON | Send malformed JSON in request body |
| Nonexistent resource | Request a workflow or document packet ID that does not exist |
| Run not finished | Poll `GET /v3/runs/{run_id}/` while processing runs, and expect `200` with a non-terminal `status` |
| Rate limit exceeded | Send rapid requests to trigger 429 with a `Retry-After` header |
| Server errors | Test the retry logic against mocked 500 responses |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.