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

# Error handling guide

> Handle API errors and implement robust error recovery

## How errors are returned

The Integrations API does not use a universal `error_code` field, and it does not return a `request_id`. Errors are signalled by the HTTP status code and one of four response body shapes. Always branch on `response.status_code` first, then read the body shape that matches.

<Tabs>
  <Tab title="Domain error">
    Most domain errors return a `detail` message. Some also include an optional `docs_url`.

    ```json theme={null}
    {
      "detail": "Asset already submitted",
      "docs_url": "https://docs.mediamagicverify.com//concepts/assets"
    }
    ```

    Authentication failures (401) use this shape, with messages such as `Missing X-API-Key header`, `Invalid API key`, `This API key has expired`, or `This API key has been revoked`.
  </Tab>

  <Tab title="Validation error (422)">
    Request validation failures return a structured envelope with a per-field breakdown.

    ```json theme={null}
    {
      "error": "validation_error",
      "message": "Request payload is invalid.",
      "fields": [
        { "field": "endpointUrl", "message": "URL must use HTTPS" }
      ]
    }
    ```
  </Tab>

  <Tab title="Rate limit (429)">
    Exceeding a rate-limit bucket returns a single `error` string and a `Retry-After` header.

    ```json theme={null}
    {
      "error": "Rate limit exceeded: too many submission writes"
    }
    ```

    See [Rate limiting](/reference/rate-limiting) for the bucket model.
  </Tab>

  <Tab title="Tier not authorized (402)">
    Returned when your plan or tier does not include access to the Integrations API.

    ```json theme={null}
    {
      "error_code": "tier_not_authorized_for_integrations_api",
      "message": "Your plan does not include access to the Integrations API.",
      "bucket": "submission_write"
    }
    ```

    This is the only response that uses an `error_code` field. The `bucket` names the gated capability.
  </Tab>
</Tabs>

## HTTP status codes

| Code | Meaning                                                   | Retryable?         |
| ---- | --------------------------------------------------------- | ------------------ |
| 200  | OK                                                        | No                 |
| 201  | Created (submission, webhook subscription)                | No                 |
| 202  | Accepted (asset retry)                                    | No                 |
| 204  | No content (delete subscription)                          | No                 |
| 400  | Bad request (e.g. `simulationDelaySeconds` on a live key) | No                 |
| 401  | Missing, invalid, expired, or revoked API key             | No                 |
| 402  | Tier not authorized for the Integrations API              | No                 |
| 403  | Sandbox-only endpoint with a live key; invalid blob path  | No                 |
| 404  | Resource not found in your workspace                      | No                 |
| 409  | Asset already submitted; asset not retryable              | No                 |
| 413  | Upload too large                                          | No                 |
| 415  | Unsupported or mismatched content type                    | No                 |
| 422  | Request validation failed                                 | No                 |
| 429  | Rate limit exceeded (includes `Retry-After`)              | Yes (with backoff) |
| 500  | Storage error or internal failure                         | Yes (with backoff) |
| 502  | Failed to load an issue or topic blob from storage        | Yes (with backoff) |

Only 429, 500, and 502 are safe to retry. Every other 4xx indicates a problem with the request, the resource, or your plan that a retry alone cannot resolve.

## Common situations

These are real conditions you will encounter, described by status code and the style of `detail` message returned. There are no error-code constants to match against.

### 401 Invalid API key

Missing or invalid API key, returned as a domain error.

```json theme={null}
{
  "detail": "Invalid API key"
}
```

Check that the `X-API-Key` header is present and correct, and that the key has not expired or been revoked.

### 403 Invalid blob path

A blob path failed validation, or a sandbox-only endpoint was called with a live key.

```json theme={null}
{
  "detail": "Invalid blob path"
}
```

### 404 Not found

A submission or asset does not exist in your workspace, or topics/issues were requested before the asset finished processing.

```json theme={null}
{
  "detail": "Submission not found"
}
```

### 409 Asset already submitted

The same asset was already submitted, or a retry is not allowed.

```json theme={null}
{
  "detail": "Asset with blobPath '<workspace-id>/campaign-video.mp4' already submitted"
}
```

To process the file again, check `/api/submissions/{submission_id}/versions` for existing versions, or upload a new file.

### 413 Upload too large

The uploaded blob exceeds the per-category size limit for its type. Check the file against the per-category maximums before submitting.

```json theme={null}
{
  "detail": "File too large; use the presigned upload URL"
}
```

### 415 Unsupported content type

The content type is not in the allowlist, or it does not match the file.

```json theme={null}
{
  "detail": "Unsupported content type"
}
```

### 422 Validation error

The request body failed validation. Read the `fields` array — see the validation envelope above.

### 402 Tier not authorized

Your plan does not include Integrations API access — see the 402 envelope above.

### 429 Rate limit exceeded

A rate-limit bucket was exceeded. Wait for the `Retry-After` header — see the 429 envelope above.

## Retry strategies

Retry only on 429, 500, and 502. Use exponential backoff with jitter, and respect the `Retry-After` header on 429 responses.

### Exponential backoff with jitter

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

import requests

API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"
RETRYABLE = {429, 500, 502}


def request_with_retries(
    method: str,
    url: str,
    max_retries: int = 4,
    base_delay: float = 1.0,
    **kwargs,
) -> requests.Response:
    """Make a request, retrying only 429/500/502 with backoff and jitter."""
    for attempt in range(max_retries):
        response = requests.request(method, url, timeout=10, **kwargs)

        if response.status_code not in RETRYABLE or attempt == max_retries - 1:
            return response

        # Honour Retry-After (seconds) on 429, otherwise back off exponentially.
        retry_after = response.headers.get("Retry-After")
        if response.status_code == 429 and retry_after:
            delay = float(retry_after)
        else:
            delay = base_delay * (2 ** attempt)
            delay += random.uniform(0, delay * 0.1)  # jitter

        print(f"HTTP {response.status_code}, retry {attempt + 1}/{max_retries} in {delay:.1f}s")
        time.sleep(delay)

    raise RuntimeError(f"Failed after {max_retries} attempts")


response = request_with_retries(
    "POST",
    f"{API_BASE}/api/submissions",
    headers={"X-API-Key": "your-api-key-here"},
    json={
        "assets": [{"blobPath": "<workspace-id>/campaign-video.mp4"}],
        "sidekickIds": ["<sidekick-id>"],
    },
)
```

For 429 responses, the `Retry-After` header gives the number of seconds to wait. See [Rate limiting](/reference/rate-limiting) for the bucket model.

## A note on duplicate submissions

Submission creation is not idempotent, and the API does not support an `Idempotency-Key` header. If you retry a submission request whose response you did not receive, you may create a duplicate. Guard against this client-side — for example, only retry submission creation on a network error or a retryable status (429/500/502), and track which assets you have already submitted. A repeated submission of the same asset returns `409` (asset already submitted).

## Validation before upload

Validate files client-side before uploading to fail fast. Maximum sizes are per category (video up to 5 GB, audio 500 MB, image 50 MB, document 100 MB). The example below enforces per-category limits.

```python theme={null}
import mimetypes
import os

ALLOWED_TYPES = {
    "video/mp4",
    "video/quicktime",
    "video/x-msvideo",
    "video/x-matroska",
    "audio/mpeg",
    "audio/wav",
    "audio/x-wav",
    "audio/aac",
    "audio/ogg",
    "image/jpeg",
    "image/png",
    "image/gif",
    "image/webp",
    "image/svg+xml",
    "application/pdf",
    "application/msword",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "text/plain",
}

# Maximum file size per category, in bytes.
MAX_SIZES = {
    "video": 5 * 1024**3,
    "audio": 500 * 1024**2,
    "image": 50 * 1024**2,
    "application": 100 * 1024**2,
    "text": 100 * 1024**2,
}


def validate_file(file_path: str) -> str:
    """Validate a file before upload. Returns the detected MIME type."""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"File not found: {file_path}")
    if not os.access(file_path, os.R_OK):
        raise PermissionError(f"File not readable: {file_path}")

    mime_type, _ = mimetypes.guess_type(file_path)
    if mime_type not in ALLOWED_TYPES:
        raise ValueError(f"Unsupported content type: {mime_type}")

    category = mime_type.split("/", 1)[0]
    max_size = MAX_SIZES.get(category)
    size = os.path.getsize(file_path)
    if max_size and size > max_size:
        raise ValueError(f"File too large: {size} bytes (max {max_size} for {category})")

    return mime_type


try:
    validate_file("campaign-video.mp4")
except (FileNotFoundError, PermissionError, ValueError) as exc:
    print(f"Validation failed: {exc}")
```

## Logging errors

Log the status code and the `detail` (or validation `fields`) for context. Do not log `error_code` or `request_id` — neither exists on most responses.

```python theme={null}
import logging

import requests

logger = logging.getLogger(__name__)


def log_api_error(response: requests.Response) -> None:
    """Log an API error with the status code and message."""
    try:
        body = response.json()
    except ValueError:
        body = {}

    detail = body.get("detail") or body.get("message") or body.get("error")

    logger.error(
        "Integrations API error",
        extra={
            "status_code": response.status_code,
            "detail": detail,
            "fields": body.get("fields"),
            "url": response.url,
            "method": response.request.method,
        },
    )


response = requests.post("...")
if response.status_code >= 400:
    log_api_error(response)
```

## Handling specific asset errors

Per-asset failures are reported by `GET /api/submissions/{submission_id}` (there is no `/status` suffix). Each asset carries flat camelCase failure fields: `errorMessage`, `errorType`, `isRetryable`, and `retryCount`. There is no nested `error` object. An asset is failed when its `status` is `failed`; terminal asset states are `complete` and `failed`.

```python theme={null}
import requests

API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"


def get_failed_assets(submission_id: str, api_key: str) -> list[dict]:
    """Return the assets that failed processing."""
    response = requests.get(
        f"{API_BASE}/api/submissions/{submission_id}",
        headers={"X-API-Key": api_key},
        timeout=10,
    )
    response.raise_for_status()
    submission = response.json()

    failed = [
        asset for asset in submission["assets"]
        if asset.get("status") == "failed"
    ]

    for asset in failed:
        message = asset.get("errorMessage")
        error_type = asset.get("errorType")
        retryable = asset.get("isRetryable", False)
        print(f"{asset['assetId']}: {error_type} - {message} (retryable={retryable})")

    return failed
```

If `isRetryable` is `true`, you can re-run the asset with `POST /api/submissions/{submission_id}/assets/{asset_id}/retry`.

## Circuit breaker pattern

For high-volume production systems, wrap API calls in a circuit breaker so repeated failures fail fast instead of piling up.

```python theme={null}
import requests
from circuitbreaker import circuit

API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"


@circuit(failure_threshold=5, recovery_timeout=60)
def call_integrations_api(endpoint: str, **kwargs) -> dict:
    """Call the API with a circuit breaker."""
    response = requests.get(f"{API_BASE}{endpoint}", timeout=10, **kwargs)
    response.raise_for_status()
    return response.json()


submission_id = "550e8400-e29b-41d4-a716-446655440000"
try:
    submission = call_integrations_api(
        f"/api/submissions/{submission_id}",
        headers={"X-API-Key": "your-api-key-here"},
    )
    print(f"Status: {submission['status']} ({submission['submissionId']})")
except Exception as exc:
    print(f"Request failed or circuit open: {exc}")
```

## Error handling checklist

<Tip>
  * Branch on `response.status_code`, then read `detail` (or `fields` for 422 validation errors)
  * Retry only 429, 500, and 502 — never other 4xx
  * Respect the `Retry-After` header on 429 responses
  * Use exponential backoff with jitter for retries
  * Validate files client-side before uploading
  * Log the status code and message with full context
  * Guard against duplicate submissions client-side (there is no idempotency key)
  * Implement a circuit breaker for high-volume systems
</Tip>
