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

# Asset lifecycle

From queued to ready (or failed) — every state an asset can be in.

# Asset lifecycle

Every Motioness asset goes through the same finite-state machine, regardless of mode or model.

```text
queued ──→ vision_started ──→ vision_completed
              │
              ▼
        stage_started ──→ stage_completed
              │   (loop mode: stage1 → stage2 → merge)
              ▼
       r2_write_completed
              │
              ▼
            ready ──┬──→ webhook_dispatched ──→ webhook_delivered
                    └──→ (cached at edge forever)

  Any stage can emit *_failed → asset.status = failed → asset.failed event
```

## States

| Status | Meaning |
|---|---|
| `pending` | Generation in flight. Worker may still be in vision / stage1 / stage2 / merge / R2-write. |
| `ready` | mp4 exists in R2, accessible via the proxy URL. Terminal success. |
| `failed` | Pipeline gave up. Inspect `error_code` in the `asset.failed` event payload. |

## Event types

The full event log is emitted to D1 (`asset_events`), surfaced via SSE, and dispatched to webhooks. The closed enum:

| Event | When |
|---|---|
| `queued` | Worker accepted the request, asset row inserted |
| `vision_started` | Claude vision call dispatched |
| `vision_completed` | Vision returned a motion prompt |
| `vision_failed` | Vision rejected the source (NSFW, oversize, broken URL) |
| `stage_started` | Replicate prediction submitted (`stage` ∈ `stage1`, `stage2`, `merge`, `single`) |
| `stage_completed` | Prediction returned an mp4 |
| `stage_failed` | Prediction errored or timed out |
| `retry_scheduled` | Transient error, retry queued (observational in v1.1) |
| `r2_write_completed` | mp4 stored in R2 |
| `webhook_dispatched` | Outbound delivery enqueued |
| `webhook_delivered` | Customer endpoint responded 2xx |
| `webhook_failed` | Customer endpoint errored or timed out |
| `webhook_dead_letter` | 5 failed attempts, won't retry automatically |
| `ready` | Terminal success — proxy URL is now hot |
| `failed` | Terminal failure — `error_code` field tells you why |
| `cancelled` | Manual cancel via dashboard |

Each event carries `ts`, `attempt`, `stage` (when applicable), `latency_ms` (on `*_completed`), `error_code` + `error_msg` (on `*_failed`), `request_id`.

## Watching events

Three ways to observe asset state:

**Public SSE (per asset)**

Browser-friendly, no auth required (uses project key + asset id):

    ```bash
    curl -N "https://motioness.com/v1/pk_abc/events/asset_xyz"
    ```

    Each event arrives as:

    ```text
    id: 1714801234567
    event: stage_completed
    data: {"stage":"stage1","latency_ms":4820,"asset_id":"a25b…","ts":1714801234567}
    ```

    Reconnect resumes from `Last-Event-ID`.

  **Auth-gated SSE**

For dashboards. Requires session cookie:

    ```bash
    # Per-asset
    curl -N -b "better-auth.session_token=…" \
      "https://motioness.com/api/assets/$ASSET_ID/events"

    # Per-project (all activity)
    curl -N -b "better-auth.session_token=…" \
      "https://motioness.com/api/projects/$PROJECT_ID/activity"
    ```

  **Webhooks**

POST to your endpoint when subscribed events fire. See [webhooks guide](/guides/webhooks).

    ```json
    {
      "id": "evt_01HXY…",
      "type": "asset.completed",
      "ts": 1714801234567,
      "asset_id": "a25b…",
      "project_id": "proj_…",
      "data": { "mp4_url": "https://motioness.com/r2/assets/…mp4", "bytes": 845321 }
    }
    ```

## Stale-pending recovery

If a Replicate webhook never lands (network blip, signature mismatch, etc.), two safety nets kick in:

1. **Opportunistic reconcile** — `GET /api/assets/:id` triggers a Replicate state query if the asset has been pending for >25 seconds.
2. **Cron reconciler** — runs every minute, finalizes stuck assets after 5 minutes, marks them `failed` with `pipeline_stalled` after 10 minutes.

You don't have to do anything; just don't assume `pending` is permanent. Poll the asset endpoint or listen on SSE.

## Idempotency

Asset IDs are content-addressed, so calling the proxy twice for the same `(src, motion, mode, dur, prompt, seed, idem, aspect)` returns the same `asset_id` — no duplicate generation.

To force a distinct asset from the same source, vary `idem`:

```text
?idem=variant-a   → asset_id A
?idem=variant-b   → asset_id B
```

## Cancellation

There's no public cancel endpoint. The dashboard exposes a **Cancel** button on pending assets that emits `cancelled` and stops billing — generations that have already submitted to Replicate will run to completion but won't count as `ready`.