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

# Realtime updates guide

> Subscribe to a submission's Ably channel for live status events instead of polling

Instead of [polling](/guides/poll-status), you can subscribe to a submission's
realtime channel and receive status events as they happen. Processing runs one
review per asset, and every event for a submission — per-asset progress and the
overall outcome — is published to a single channel: `submission:{submissionId}`.

Realtime uses [Ably](https://ably.com). You never hold an Ably key: you request
a short-lived, subscribe-only token from the API and hand it to the Ably client.

<Note>
  Realtime is a convenience layer on top of the REST API, not a replacement for
  it. Events are best-effort and are not replayed to late or reconnecting
  subscribers, so treat `GET /api/submissions/{submission_id}` as the source of
  truth and use it to reconcile state. See [Reliability](#reliability).
</Note>

## What you need

<Steps>
  <Step title="The channel id">
    `POST /api/submissions` returns `ablyChannel` (e.g.
    `submission:550e8400-...`). It is always `submission:{submissionId}`.
  </Step>

  <Step title="A subscription token">
    Call the token endpoint below to get an Ably token scoped to that one
    channel.
  </Step>

  <Step title="An Ably client">
    Use an Ably SDK, pointing its auth at the token endpoint so it can refresh
    automatically.
  </Step>
</Steps>

## Get a subscription token

`POST /api/submissions/{submission_id}/realtime-token`

Returns an Ably token request granting **`subscribe` only** on
`submission:{submissionId}` — nothing else. The submission must belong to your
API key's workspace.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST \
    -H "X-API-Key: your-api-key-here" \
    https://api.northell.io/nhl/prod/mediamagic-verify-integrations/api/submissions/550e8400-e29b-41d4-a716-446655440000/realtime-token
  ```

  ```python theme={null}
  import requests

  API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"
  submission_id = "550e8400-e29b-41d4-a716-446655440000"

  resp = requests.post(
      f"{API_BASE}/api/submissions/{submission_id}/realtime-token",
      headers={"X-API-Key": "your-api-key-here"},
  )
  token_request = resp.json()  # hand this straight to the Ably client
  ```
</CodeGroup>

The response is an Ably token request — relay it verbatim to the Ably client:

```json theme={null}
{
  "keyName": "aB1cD2.xYz789",
  "ttl": 3600000,
  "capability": "{\"submission:550e8400-...\":[\"subscribe\"]}",
  "clientId": "org:9a2e...",
  "timestamp": 1753795200000,
  "nonce": "8f3c1e5b9a2d4c7e",
  "mac": "hQ9k...=="
}
```

| Field                                  | Meaning                                                            |
| -------------------------------------- | ------------------------------------------------------------------ |
| `ttl`                                  | Token lifetime in milliseconds (default one hour)                  |
| `capability`                           | JSON string — grants `subscribe` on this submission's channel only |
| `clientId`                             | Identity stamped on the token                                      |
| `keyName`, `timestamp`, `nonce`, `mac` | Signature fields Ably verifies                                     |

A `404` is returned if the submission does not exist in your workspace. A `503`
is returned if realtime is not enabled for the deployment — fall back to
[polling](/guides/poll-status).

See [Create realtime token](/submissions/create-realtime-token) for the full
endpoint reference.

## Connect and subscribe

Point the Ably client's `authCallback` at the token endpoint. The SDK calls it
on connect and again whenever the token nears expiry, so refresh is automatic.

```javascript theme={null}
import * as Ably from 'ably';

const submissionId = '550e8400-e29b-41d4-a716-446655440000';

const realtime = new Ably.Realtime({
  authCallback: async (_params, callback) => {
    try {
      const res = await fetch(
        `https://api.northell.io/nhl/prod/mediamagic-verify-integrations/api/submissions/${submissionId}/realtime-token`,
        { method: 'POST', headers: { 'X-API-Key': 'your-api-key-here' } }
      );
      callback(null, await res.json());
    } catch (err) {
      callback(err, null);
    }
  },
});

const channel = realtime.channels.get(`submission:${submissionId}`);
channel.subscribe('workflow_update', (message) => {
  const { status, details } = message.data;
  console.log(status, details);
});
```

## Status events

Status updates arrive as the **`workflow_update`** event. Each message payload
has the same envelope:

```json theme={null}
{
  "workflow_id": "asset-review-...",
  "status": "ASSET_COMPLETED",
  "message": "Asset review completed",
  "details": { "asset_id": "a1b2...", "status": "compliant" }
}
```

<Warning>
  Realtime payload keys are `snake_case` (`asset_id`, `asset_count`), unlike the
  REST API, which is `camelCase`. Read fields off the event payload, not off the
  REST schema.
</Warning>

The `status` field tells you what happened:

| `status`          | When it fires                               | `details`                                              | Maps to submission status |
| ----------------- | ------------------------------------------- | ------------------------------------------------------ | ------------------------- |
| `PROCESSING`      | Once, when the submission starts processing | `{}`                                                   | `processing`              |
| `ASSET_COMPLETED` | Each time an asset finishes successfully    | `{ "asset_id", "status": "compliant" \| "in_review" }` | — (per-asset progress)    |
| `ASSET_FAILED`    | Each time an asset fails                    | `{ "asset_id", "status": "failed" }`                   | — (per-asset progress)    |
| `COMPLETED`       | All assets succeeded                        | `{ "asset_count" }`                                    | `complete`                |
| `PARTIAL`         | Some assets succeeded, some failed          | `{ "completed", "failed" }`                            | `partial`                 |
| `FAILED`          | Every asset failed                          | `{ "failed" }`                                         | `failed`                  |

`COMPLETED`, `PARTIAL`, and `FAILED` are terminal — after one of them, no
further status events fire (unless you [retry a failed
asset](/concepts/submissions#error-handling), which re-activates processing on
the same channel). The per-asset `status` on `ASSET_COMPLETED` is the asset's
compliance result: `compliant` (no issues) or `in_review` (issues found).

## Preview events

Alongside status, the channel also emits **`asset_preview_ready`** when an
asset's rendered preview becomes available:

```json theme={null}
{
  "status": "ASSET_PREVIEW_READY",
  "message": "Asset preview available",
  "details": { "asset_id": "a1b2...", "preview_mime_type": "application/pdf" }
}
```

This is best-effort and only useful if you display asset previews — otherwise
ignore it. Subscribe to it explicitly (`channel.subscribe('asset_preview_ready',
...)`) or subscribe without an event name to receive every event on the channel.

## Reliability

<Warning>
  Events are delivered best-effort and are **not** replayed. The subscribe-only
  token grants no message history, so an event published before you connect (or
  while you were disconnected) is missed.
</Warning>

* **Reconcile with the REST API.** After connecting, and on reconnect, call
  `GET /api/submissions/{submission_id}` to get the authoritative current state.
  A submission that reached a terminal state before you subscribed will not
  re-emit — read it from the status endpoint.
* **Tokens are short-lived.** The default lifetime is one hour. Using an
  `authCallback` (above) lets the Ably SDK refresh transparently; a token cannot
  be reused after it expires.
* **One channel per submission.** All per-asset workflows publish to the same
  `submission:{submissionId}` channel, so a single subscription covers the whole
  submission.

## See also

* [Poll status guide](/guides/poll-status) — the polling alternative and the
  fallback when realtime is unavailable.
* [Create realtime token](/submissions/create-realtime-token) — endpoint reference.
* [Submissions](/concepts/submissions) — the submission lifecycle and status model.
