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

# Errors and retries

Every failure mode the proxy can produce and what to do about each.

# Errors and retries

Motioness fails loud — every error has a stable `error_code` you can branch on, and a `request_id` for support tickets.

## HTTP status codes

| Status | When | Body |
|---|---|---|
| `202` | Generation in flight | `{ status: 'pending', asset_id, eta_seconds }` |
| `400` | Malformed proxy URL or body validation failed | `{ error: '...' }` |
| `403` | Unknown project key, origin not allowlisted, or invalid signature | `{ error: '...' }` |
| `404` | Asset not found / not owned | `{ error: 'not found' }` |
| `413` | Source > 10 MB | `{ error: 'file exceeds 10 MB limit' }` |
| `415` | Source not png/jpg/webp | `{ error: 'unsupported type: ...' }` |
| `429` | Monthly quota exhausted | `{ error: 'monthly limit reached', upgrade_url }` |
| `502` | Last generation failed terminally | `{ error: 'last generation failed', asset_id, hint: 'POST /api/v1/regenerate/...' }` |

## Error codes (in `asset.failed` events)

`error_code` is the stable identifier; `error_msg` is human-readable.

| Code | Meaning | Action |
|---|---|---|
| `vision_blocked` | Source rejected by vision model (NSFW, copyright) | Use a different source |
| `vision_timeout` | Vision call exceeded 30s | Retry; if persistent, simplify the source |
| `source_unfetchable` | 4xx/5xx fetching the source URL | Check the URL is publicly reachable |
| `source_too_large` | Source > 10 MB | Resize or upload via `/api/v1/upload` |
| `source_unsupported` | Source is svg / mp4 / unknown content-type | Re-encode to png/jpg/webp |
| `i2v_failed` | Image-to-video model errored | Retry with regenerate; if persistent, lower `motion` |
| `i2v_timeout` | i2v exceeded 5 minutes | Retry; lower `dur_seconds` |
| `merge_failed` | Loop stitching failed | Try `mode=animate-in` instead |
| `r2_write_failed` | R2 write errored | Auto-retried; if persistent, contact support |
| `quota_exceeded` | Monthly limit hit mid-flight | Upgrade or wait for reset |
| `pipeline_stalled` | Cron reconciler gave up after 10 min | Regenerate; if persistent, contact support |

## Built-in retry mechanisms

You don't need to handle transient errors yourself. Three independent mechanisms recover automatically:

### 1. Per-stage retry envelope

Transient errors (Replicate 5xx/429, R2 5xx, timeouts) retry up to **3 attempts per stage** with `[0s, 1s, 4s]` backoff. Permanent errors (Replicate 4xx, content-type issues, quota) fail loud.

You'll see `retry_scheduled` events on SSE and `asset.retry_scheduled` webhooks when this fires.

### 2. Cron reconciler

A cron job runs every minute:

- Reconcile assets stuck `pending` for 5–10 minutes by querying Replicate directly.
- Mark assets stuck `pending` for >10 minutes as `failed` with `error_code: pipeline_stalled`.

If a Replicate webhook never lands (network blip, signature mismatch on Motioness's side, etc.), the cron is the safety net.

### 3. Webhook delivery retries

Outbound webhooks retry on `30s, 2m, 10m, 30m, 2h`. After 5 failed attempts the delivery is `dead`; redeliver from the dashboard or via [`POST /api/projects/:id/webhooks/deliveries/:deliveryId/redeliver`](/api/webhooks/redeliver).

## What to do on each status

**202 Accepted**

Normal first-hit response. The `Retry-After` header tells you how many seconds the server estimates for completion. The `X-Motioness-Eta-Ms` header gives a more precise estimate.

    Either:
    - Poll the same URL again after `Retry-After`.
    - Subscribe via SSE: `GET /v1/{key}/events/{asset_id}`.
    - Use `?wait_ms=8000` to long-poll up to 8 seconds.

  **403 — origin not allowed**

Add your origin to `allowed_origins` (Project → Settings → Proxy), or switch to [signed mode](/concepts/allowlist-vs-signed).

    Subdomain matching is automatic: `https://acme.com` accepts `*.acme.com`.

  **403 — invalid signature**

Signing-mode-only. Common causes:
    - You're signing with the wrong tuple. Must be `${projectKey}:${srcUrl}:${exp}` exactly.
    - `exp` already passed. Stick to TTLs >= 60s.
    - You used base64 instead of base64url for the source. Check for `+` / `/` characters.
    - The signing secret was rotated and the URL is signed with the old one.

  **429 — monthly limit reached**

Free tier: 5 generations/month. Pro: 100. Team: 500.

    The error response includes `upgrade_url` pointing to the pricing page. After upgrade, generations resume immediately.

    If you're on Pro+ and seeing this with `error: "overage disabled"`, enable overage in Project → Settings → Billing.

  **502 — last generation failed**

The asset's last generation hit a permanent error (vision-blocked, i2v failure, etc.). The `hint` in the response tells you to call regenerate. Inspect `asset.failed` event to see `error_code`, then either:

    - Adjust the source (most `vision_*` and `source_*` errors require this).
    - Retry with different opts: `POST /api/assets/:id/regenerate` with new motion / prompt / seed.

## Debugging with `X-Request-Id`

Every response carries `X-Request-Id`. Echo it back to support tickets — we can pull the full event log with one query.

To set your own, send `X-Request-Id: req_my_correlation_id` on the request. The server echoes it if it matches `^[\w-]{4,64}$`, otherwise mints `req_<8-hex>`.