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

# Billing & Limits (Beta)

> The exact billing formula, the $0-reject guarantees, the full error catalog, and every operational limit of the Video API.

<Note>
  **Beta.** The Video API is in beta. Rates below are the current beta rate card and may be adjusted during the beta [with notice](/video/versioning); the *mechanics* (integer seconds, no multipliers, \$0 rejects) are the contract.
</Note>

## The formula

```text theme={"dark"}
billed_seconds = min( max( ceil(execution_wall_seconds), 5 ), 3600 )
delivery_gb    = max( output_gb - 0.2, 0 )
cost           = billed_seconds × 0.0015  +  delivery_gb × 0.30
```

The cost is rounded half-up to 4 decimal places.

| Compute                     | Floor | Rate (USD/s) | Status              |
| --------------------------- | ----- | ------------ | ------------------- |
| Shared `cpu` infrastructure | 5s    | 0.0015       | Generally available |

| Output delivery            | Value                                                   |
| -------------------------- | ------------------------------------------------------- |
| Included per job           | The first 200 MB (0.2 GB) of output — free on every job |
| Excess above the allowance | \$0.30/GB, charged on the excess only                   |

Jobs run on shared `cpu` infrastructure; a legacy `tier` field is still accepted on requests but never needs to be sent.

* **Only ffmpeg wall time is metered for compute.** `execution_wall_seconds` is the time the media command actually runs. The *time* spent on input staging, queueing, and artifact upload is never metered. This is why `metrics.predict_time` — the prediction's full wall time, which includes those phases plus the platform's \~5 s status-check quantization — is **not** the billed quantity and essentially always differs from it (it can even read lower than the billed seconds when a short job bills the 5 s floor).
* **200 MB of output delivery included.** Every job includes the first 0.2 GB of delivered output free; only the excess bills, at \$0.30/GB. An output of exactly 0.2 GB is inside the allowance. Most capabilities emit far less than 200 MB and never pay a delivery cent, and the analysis capabilities (`probe`, `scene_detect`, `silence_detect`, `audio_analysis`) produce no artifact at all — the delivery term only bites on genuinely large outputs.
* **No multipliers.** Resolution, codec, container, and input duration never multiply the price — you pay for execution seconds, plus delivery only beyond the included 200 MB.
* **Integer seconds, floored and capped.** Wall time is rounded up to whole seconds, floored at 5 seconds (a 1.2 s job bills 5 s), and hard-capped at 3600.
* **Reproduce your bill from the response.** On video-api predictions, `metrics` carries the meter fields — `tier`, `billed_seconds`, and `output_gb` — alongside `cost` and `predict_time`. `metrics.cost` is the authoritative charge, and on a success it always equals the formula applied to those two meter fields. Failed predictions carry only `cost` and `predict_time` — the meter fields are absent even on the classes the engine meters (`exec_error`, `billed_seconds_cap`), so reproduce a bill from a `success` only.
* **Worked example.** A cpu job whose ffmpeg run takes 190.3 s bills `ceil(190.3) = 191` seconds; say it delivers a 1.404 GB output. Cost = `191 × 0.0015 + (1.404 - 0.2) × 0.30` = `0.2865 + 0.3612` = \*\*$0.6477** — reproducible from `metrics.billed_seconds: 191` and `metrics.output_gb: 1.404` on the polled prediction. Had the same job delivered 50 MB, the delivery term would be $0 and it would bill \$0.2865 flat. (`predict_time` might read `194.86` — the extra \~4 s is queueing and poll quantization on top of the metered 191 s.)

## When you pay nothing — guaranteed \$0

| Case                                                                          | Example                                                                                                                                                                                                                                                      |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Invalid request                                                               | Unknown capability, wrong enum, out-of-range value, unknown parameter key, wrong input count                                                                                                                                                                 |
| Input rejected pre-compute                                                    | Input over the size budget, input duration over 3600s, unsupported input URL, an input missing a stream the capability declares it needs — the closed per-capability list is in [What the probe gate checks](/video/capabilities#what-the-probe-gate-checks) |
| Parameters that would produce an empty output — when the source can be probed | `trim` starting at/past the end of the clip, `fade` scheduled beyond the video's duration, sub-frame output windows. On a source the probe cannot read the gate fails open: the job runs and bills its execution seconds                                     |
| Platform faults                                                               | Any failure on our side — including output-upload failure. **Deliver-or-don't-bill:** if the failure is ours, you were not billed. `platform_fault` is retryable; `platform_drift` is deterministic (see [the drift class](#platform_drift-defined))         |

## Failed compute

A failure caused by your input **after compute starts** is metered by the engine. Metering records what the engine measured; settlement is what you are charged — during the beta, failed compute meters normally but settles at `metrics.cost: 0`. Two failure cases work this way:

| Event                                                                                                                                                              | Delivered                              | Engine meters                        | `failure_class`      | Retryable |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | ------------------------------------ | -------------------- | --------- |
| The job reaches 3600 s of metered ffmpeg wall                                                                                                                      | Nothing — the job is killed at the cap | Exactly 3600 s                       | `billed_seconds_cap` | No        |
| ffmpeg fails mid-run after N seconds, on input content that passed the pre-compute probe gate (corrupt media, an impossible mux, an out-of-bounds crop/pad window) | Nothing                                | `ceil(N)` seconds, 5 s floor applies | `exec_error`         | No        |

<Note>
  **Beta settlement.** Today, every prediction that ends in any status other than `success` settles with `metrics.cost: 0` — including `exec_error`, `billed_seconds_cap`, and `cancelled` (verified against production; see the captured `exec_error` example below). The **Engine meters** column is what the engine records for these outcomes and the reserved charge basis for them; if charging failed compute activates during the beta it will be announced [with notice](/video/versioning), per the note at the top of this page. Do not design flows that rely on failed compute staying free — the cap kill in particular is designed to bill exactly 3600 s.
</Note>

There is no other execution kill: the 3600 s billed-seconds cap is the only ceiling that terminates a running job. Total job wall clock (staging + queueing + upload) is unmetered and can exceed 3600 s without a kill. A platform fault at any point — before, during, or after compute — always bills \$0.

## Error catalog

Every failure surfaces on the polled prediction as `status: "error"` — never `"failed"` — and `output` carries the error envelope; `metrics.cost` carries what it billed. Submitting a prediction essentially always succeeds with a prediction ID: errors arrive when you poll.

### The error envelope

On `status: "error"`, `output` is an object with these fields (captured from production below) — except on a dispatch failure at submit, which writes a minimal envelope carrying only `error`, set to the dispatch error text:

| Field                                                | Carries                                                                                                                                                                                                                                                                                                                                                       |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `error_code`                                         | The public error code — the codes in the table below, plus `PROVIDER_AUTH_ERROR` (engine 401/403), `PROVIDER_SERVER_ERROR` (engine 5xx), `NETWORK_ERROR`, `EXECUTION_TIMEOUT`, and `INPUT_DOWNLOAD_ERROR`. Treat an unknown code as terminal unless `retryable` is `true`                                                                                     |
| `error_message`                                      | The specific reason: the engine's failure reason when it provides one (validation rejects carry the full gate reason), otherwise the `failure_class`                                                                                                                                                                                                          |
| `classification.category`                            | One of `VALIDATION` (your request/input — fix before resubmitting), `PROVIDER`, `AUTH`, `RATE_LIMIT`, `NETWORK`, `TIMEOUT`, `INTERNAL`, or `UNKNOWN` for a code outside this set                                                                                                                                                                              |
| `retryable`                                          | Whether resubmitting the identical request could plausibly succeed. Do not read `false` as *your fault* or as *never retry* — a post-accept `platform_fault` is transient, bills \$0, and still reads `false`. It is not a complete signal for transience: read the failure class from `error_message` and follow [Retries](#retries--read-before-automating) |
| `error`                                              | The constant string `Execution failed` — except in the dispatch-failure envelope above, where it carries the dispatch error text                                                                                                                                                                                                                              |
| `error_step`                                         | The pipeline step that surfaced it — validation rejects show a reject step, post-accept failures show `poll`                                                                                                                                                                                                                                                  |
| `success`                                            | `false`                                                                                                                                                                                                                                                                                                                                                       |
| `execution_id`, `model_id`, `model_slug`, `provider` | Identifiers; `provider_error_code` additionally appears on post-accept failures                                                                                                                                                                                                                                                                               |

A real \$0 validation reject, captured from production, exactly as polled:

```json theme={"dark"}
{
  "id": "1e44fdd0-a565-4f59-908c-c4625dfe5395",
  "input": {
    "capability": "silence_remove",
    "input_url": "https://cdn-us.eachlabs.ai/uploads/35b956ed-0907-474c-86f9-7f3c418212c2.mp4",
    "params": { "ranges": [{ "end": 210, "start": 200 }] },
    "tier": "cpu"
  },
  "logs": null,
  "status": "error",
  "output": {
    "classification": { "category": "VALIDATION" },
    "error": "Execution failed",
    "error_code": "INPUT_VALIDATION_ERROR",
    "error_message": "ranges: range start 200s is at or beyond source duration 5s (empty output)",
    "error_step": "Surface Customer Reject.0",
    "execution_id": "1e44fdd0-a565-4f59-908c-c4625dfe5395",
    "model_id": "e3b3d84f-a2dc-46a3-b6e7-e9d80face367",
    "model_slug": "eachlabs-video-api",
    "provider": "videoapi",
    "retryable": false,
    "status": "ERROR",
    "success": false
  },
  "metrics": { "predict_time": 3.439075, "cost": 0 },
  "urls": {
    "cancel": "https://api.eachlabs.ai/v1/prediction/1e44fdd0-a565-4f59-908c-c4625dfe5395/cancel",
    "get": "https://api.eachlabs.ai/v1/prediction/1e44fdd0-a565-4f59-908c-c4625dfe5395"
  }
}
```

A real execution failure (`exec_error` — a crop window larger than the source frame, which passes the probe gate and fails in ffmpeg), captured from production; note the failure class arrives in `error_message`, the code is `PROVIDER_FAILED`, and the beta settlement is `cost: 0`:

```json theme={"dark"}
{
  "id": "f2762a7e-2362-40cf-be4a-b0cffe2e1177",
  "input": {
    "capability": "crop",
    "input_url": "https://cdn-us.eachlabs.ai/uploads/35b956ed-0907-474c-86f9-7f3c418212c2.mp4",
    "params": { "container": "mp4", "height": 7000, "width": 7000 },
    "tier": "cpu"
  },
  "logs": null,
  "status": "error",
  "output": {
    "classification": { "category": "PROVIDER" },
    "error": "Execution failed",
    "error_code": "PROVIDER_FAILED",
    "error_message": "exec_error",
    "error_step": "poll",
    "execution_id": "f2762a7e-2362-40cf-be4a-b0cffe2e1177",
    "model_id": "e3b3d84f-a2dc-46a3-b6e7-e9d80face367",
    "model_slug": "eachlabs-video-api",
    "provider": "videoapi",
    "provider_error_code": "failed",
    "retryable": false,
    "status": "ERROR",
    "success": false
  },
  "metrics": { "predict_time": 2.691812, "cost": 0 },
  "urls": {
    "cancel": "https://api.eachlabs.ai/v1/prediction/f2762a7e-2362-40cf-be4a-b0cffe2e1177/cancel",
    "get": "https://api.eachlabs.ai/v1/prediction/f2762a7e-2362-40cf-be4a-b0cffe2e1177"
  }
}
```

### The public error codes

| Code                                                   | When                                                                                                                                                                                   | Where the specific reason lives                                                                                                                | Billing                           |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| `PROVIDER_REJECTED`                                    | An input URL could not be staged (unreachable, not public, not identifiable as media)                                                                                                  | The message names the offending URL                                                                                                            | \$0                               |
| `INPUT_VALIDATION_ERROR`                               | The engine rejected the job before compute — invalid command/params (`invalid_command`) or input rejected at the probe gate (`input_rejected`)                                         | `error_message` carries the specific gate reason (first example above)                                                                         | \$0                               |
| `PROVIDER_BAD_REQUEST`                                 | The engine refused the submit itself — synchronous validation, `error_step: "Submit Job"`                                                                                              | `error_message` carries the engine's JSON body `{code, message, retryable, stage}`; codes in the table below                                   | \$0                               |
| `PROVIDER_FAILED` — post-accept (`error_step: "poll"`) | The job failed after the engine accepted it                                                                                                                                            | `error_message` carries the `failure_class` — `exec_error`, `billed_seconds_cap`, `platform_fault`, or `platform_drift` (second example above) | Per the failure-class table below |
| `PROVIDER_RATE_LIMIT`                                  | Throttling the platform could not absorb — either the engine submit bucket after the platform's retries, or a job held back by an in-flight concurrency cap past the re-enqueue budget | `error_step` distinguishes the two                                                                                                             | \$0                               |

### Engine submit rejects — synchronous, all \$0

The engine validates every submit synchronously. The platform submits on your behalf, so you see these with `error_step: "Submit Job"` and the engine's body `{code, message, retryable, stage}` returned verbatim as a JSON string in `error_message` (verified against production — e.g. a `tier: "gpu"` request polls with `error_message: {"code":"invalid_command","message":"tier: must be one of cpu","retryable":false,"stage":"validate"}`). An engine 401 or 403 surfaces as `PROVIDER_AUTH_ERROR`, a 429 as `PROVIDER_RATE_LIMIT`, a 502 as `PROVIDER_SERVER_ERROR`, and every other 4xx (400, 404, 409) as `PROVIDER_BAD_REQUEST`. Submit-stage rejects additionally carry diagnostic fields (`http_status`, `root_cause`, `causes`, `timing`) not present on post-accept failures.

| Code                                  | HTTP | Meaning                                                           | Retryable                             |
| ------------------------------------- | ---- | ----------------------------------------------------------------- | ------------------------------------- |
| `invalid_command`                     | 400  | Malformed request, params, or argv                                | No                                    |
| `unknown_capability`                  | 400  | Capability name not recognized                                    | No                                    |
| `encoder_unavailable`                 | 400  | Requested encoder not on the compute image                        | No                                    |
| `filter_unavailable`                  | 400  | Requested filter not on the compute image                         | No                                    |
| `output_resolution_too_large`         | 400  | Output dimension over the engine's limit                          | No                                    |
| `protocol_denied`                     | 400  | argv referenced a denied protocol or URL scheme (`run_ffmpeg`)    | No                                    |
| `flag_denied`                         | 400  | argv used a denied ffmpeg flag (`run_ffmpeg`)                     | No                                    |
| `literal_path`                        | 400  | Literal filesystem path in argv — use placeholders (`run_ffmpeg`) | No                                    |
| `placeholder_unbound`                 | 400  | argv placeholder with no bound input (`run_ffmpeg`)               | No                                    |
| `placeholder_syntax`                  | 400  | Malformed argv placeholder (`run_ffmpeg`)                         | No                                    |
| `unauthorized`                        | 401  | Missing or invalid engine credentials (platform-managed)          | No                                    |
| `scope_denied`                        | 403  | Capability or mode not granted to the organization                | No                                    |
| `job_not_found`                       | 404  | Unknown job ID on poll/result/cancel                              | No                                    |
| `idempotency_conflict` / `job_exists` | 409  | Duplicate submit under the same idempotency key                   | No                                    |
| `rate_limited`                        | 429  | Global engine submit bucket tripped                               | Yes — the platform retries it for you |
| `platform_fault`                      | 502  | Engine-side fault                                                 | Yes                                   |

### Failure classes after the engine accepts a job — asynchronous, at poll time

| `failure_class`      | Surfaces as                                                                                                                                | Billing                                                                   | Fault    | Retryable                                                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_command`    | `INPUT_VALIDATION_ERROR`                                                                                                                   | \$0                                                                       | Customer | No                                                                                                                                    |
| `input_rejected`     | `INPUT_VALIDATION_ERROR`, `error_message` carries the gate reason (e.g. `duration_over_cap`)                                               | \$0                                                                       | Customer | No                                                                                                                                    |
| `exec_error`         | `PROVIDER_FAILED`, `error_message: "exec_error"`                                                                                           | Engine meters elapsed ffmpeg wall (`ceil`, 5 s floor) — settles \$0 today | Customer | No                                                                                                                                    |
| `billed_seconds_cap` | `PROVIDER_FAILED`, `error_message: "billed_seconds_cap"`                                                                                   | Engine meters exactly 3600 s — settles \$0 today                          | Customer | No                                                                                                                                    |
| `platform_fault`     | `PROVIDER_FAILED`; `error_message` is `platform_fault`, or the engine's own reason where it has one (`execution_deadline_exceeded`)        | \$0                                                                       | Platform | **Yes** — but no envelope field isolates it, and `retryable` reads `false` even here. See [Retries](#retries--read-before-automating) |
| `platform_drift`     | `PROVIDER_FAILED`; `error_message` is `platform_drift`, or the engine's own reason where it has one (the clip-count limit reports its own) | \$0                                                                       | Platform | No — deterministic (below)                                                                                                            |
| — (job cancelled)    | Prediction status `cancelled`                                                                                                              | \$0 — a cancelled prediction never bills                                  | —        | —                                                                                                                                     |

`platform_fault` is the one class where retrying the same request can succeed, but nothing in the envelope tells you that: the engine marks the class retryable internally and that flag is not carried through, so every failure class in this table reaches you with `retryable: false`. Read the class from `error_message`. Every other failure class is deterministic for the same request; the load-dependent throttling codes (`PROVIDER_RATE_LIMIT`, `rate_limited`) are the carve-out — see [Retries](#retries--read-before-automating).

### `platform_drift`, defined

`platform_drift` is an engine-side **delivery defect detected after a run**: the job's compute finished, but what the engine needed to assemble your result is missing or malformed. The causes are internal and deterministic — the same request will drift again, so the class is \$0 **and** non-retryable:

* the metadata sidecar an analysis run must produce is absent, or parses to zero events (ffmpeg print-format drift);
* a `silence_split` plan whose split stage lost its computed cut times;
* a `segment` `first_frame` split that produced more than 999 clips (the numbered names stop pairing with their stills — raise `segment_seconds`).

Where the cause is parameter-reachable (the 999-clip case) change the request; otherwise report it — automated retry of the identical request only reproduces the drift. This is the difference from `platform_fault`, which is transient and safe to retry as-is.

## Cancelling a prediction

Cancel with `POST https://api.eachlabs.ai/v1/prediction/{id}/cancel` (`Authorization: Bearer` header) — the exact URL is returned on every poll as `urls.cancel`. A cancel is accepted while the prediction has not reached a terminal status and answers `{"status": "success", "message": "Prediction canceled successfully", ...}`; the prediction then polls as `cancelled` with `metrics.cost: 0` — **a cancelled prediction never bills** (verified against production).

<Warning>
  Cancellation is **record-only**: it finalizes your prediction immediately, but it does not reach into the engine — a job already executing runs to completion and its output is discarded. Cancel to detach from a job, not to stop its compute; the 3600 s execution cap is the only thing that terminates a running job.
</Warning>

## Rate limits & concurrency

* **No per-organization rate limit on submits today** (beta). `POST /v1/prediction` is not throttled per key or per org.
* **One global engine submit bucket** — 50 requests/s, burst 100, shared across all organizations — protects the engine. You never see it as an HTTP 429: the platform absorbs it and retries the submit internally (3 attempts, 2 s base backoff). A persistently throttled submit surfaces as the prediction erroring with `PROVIDER_RATE_LIMIT` at \$0.
* **In-flight execution caps** exist per provider on the platform side. A job held back by a cap is re-enqueued with backoff — you observe a prolonged `created` status, not an error. If the platform exhausts its re-enqueue budget, the prediction errors with `PROVIDER_RATE_LIMIT` at \$0.
* **Worker concurrency is bounded platform-wide** (32 concurrent job actions per worker); bursts queue rather than fail — a 100-job burst of short transcodes drains in \~15-30 s.

## Limits

| Limit                    | Value                         | Behavior at the limit                                                                                                                                                                                                                       |
| ------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Total input size per job | 2 GiB (all inputs combined)   | Rejected at $0. An over-budget input is refused from its declared `Content-Length` before download; a source that declares no length is downloaded against the remaining budget and rejected mid-stream the moment it exceeds it — still $0 |
| Max input duration       | 3600 seconds                  | Rejected at \$0 at the probe gate (`input_rejected`, reason `duration_over_cap`)                                                                                                                                                            |
| Execution kill cap       | 3600 s of metered ffmpeg wall | The job is killed, delivers nothing, and the engine meters exactly 3600 s (`billed_seconds_cap`, customer fault, non-retryable; settles \$0 in the beta). This is the only execution kill — see [Failed compute](#failed-compute)           |
| Output artifacts         | Hosted URLs (CDN)             | Treat URLs as valid for at least 24 hours; download or persist promptly                                                                                                                                                                     |

## Retries — read before automating

A retried submit creates a **second, separately billed job** — the prediction API does not deduplicate submits today. Submit once, persist the returned `predictionID`, and drive all retry logic through polling that ID. Gateway-level idempotency keys are on the beta roadmap.

For a *failed* prediction, three rules cover every class. Apply them in order: the `retryable` field alone is not sufficient, because the engine's one transient class arrives marked `retryable: false`.

* **Retry the same request once** for any post-accept `PROVIDER_FAILED`. Platform faults — \$0, and the only transient class after accept — hide inside this code, and no envelope field isolates them: `retryable` reads `false`, and the failure class reaches you only through `error_message`, which carries the engine's own reason where it has one (`execution_deadline_exceeded` for a job its runtime deadline cut off) and the class name (`platform_fault`) otherwise. Do not test for a fixed string. One retry settles it — a platform fault usually clears, anything else fails the same way — and it is free today, because every post-accept failure settles at `metrics.cost: 0` during the beta. A submit-time `platform_fault` is different: it is an engine 502 and reaches you as `PROVIDER_SERVER_ERROR`, under the next rule.
* **Resubmit after backoff** for anything the envelope marks `retryable: true` — a `true` marks a load-dependent failure rather than a request defect, so a later resubmit can succeed. Branch on the field itself, never on a set of codes you maintain: the value is not a lookup on `error_code`, and which codes carry `true` is not part of the contract.
* **Everything else** — every code left after the two rules above, plus any `PROVIDER_FAILED` that failed identically on its one retry — is deterministic and will fail again: fix the request or the input instead of retrying. `platform_drift` always ends here.

## Raw ffmpeg (`run_ffmpeg`)

Beyond the named capabilities there is a raw-ffmpeg mode for long-tail cases, **available to beta organizations today**. The full contract — parameter table, `output_format` allowlist, placeholder binding, and the enforced safety boundary — lives in the [capabilities reference](/video/capabilities#run_ffmpeg). Every `run_ffmpeg` job bills under the same formula as any capability. Access may move to per-key grants later in the beta.

Policies: [Acceptable Use](/video/acceptable-use) · [Abuse & Takedown](/video/abuse-and-takedown) · [Versioning & Deprecation](/video/versioning)
