---
title: Create a run
description: POST /v1/formats/{handle}/{slug}/runs, field by field. The request body, idempotency, spend caps, keys and scopes, and every error the create call returns.
---

One `POST` starts a run. This is the shape production integrations send: your data in
`input`, a schema for the result, a per-run spend cap, and a webhook so you do not have to
poll.

```bash
curl -sS -X POST "https://api.sume.com/v1/formats/acme/live-commerce/runs" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8823-lc-v1" \
  -d '{
    "instruction": "Use the Intro/Mid/Fin script as written. Korean host, vertical 9:16, no BGM, no captions.",
    "input": {
      "product_url": "https://shop.example.com/p/8823",
      "product_name": "Aurora Headphones",
      "host_image_url": "https://cdn.example.com/hosts/yura.png",
      "vo_language": "ko",
      "on_card_name": "Aurora Headphones",
      "price": { "list": "31,000원", "sale": "22,940원", "discount_label": "26%" },
      "script": {
        "segments": [
          { "tag": "Intro", "text": "안녕하세요, …" },
          { "tag": "Mid", "text": "…" },
          { "tag": "Fin", "text": "…" }
        ]
      }
    },
    "output_schema": {
      "name": "acme/live-commerce/v1",
      "strict": true,
      "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": ["full_video"],
        "properties": { "full_video": { "$ref": "SumeMediaFile#" } }
      }
    },
    "primary_output_key": "full_video",
    "generation_spend_cap_usd": 120,
    "communication": { "webhook_url": "https://acme.example.com/hooks/sume" }
  }'
```

`202 Accepted`:

```json
{
  "data": {
    "id": "arun_e43e6c5cb2b74052",
    "object": "format.run",
    "status": "queued",
    "format": { "id": "skl_…", "slug": "live-commerce", "title": "Live commerce", "version": 23 },
    "trigger": { "source": "api", "idempotency_key": "order-8823-lc-v1" },
    "output_schema": { "name": "acme/live-commerce/v1", "strict": true, "source": "request_override" },
    "usage": { "currency": "USD", "billable_amount_usd_micros": 0, "generation_spend_cap_usd_micros": 120000000 },
    "webhook_delivery": { "url": "https://acme.example.com/hooks/sume", "status": "not_armed", "attempts": 0, "max_attempts": 10 },
    "status_url": "https://api.sume.com/v1/format-runs/arun_e43e6c5cb2b74052/status",
    "result_url": "https://api.sume.com/v1/format-runs/arun_e43e6c5cb2b74052/result",
    "events_url": "https://api.sume.com/v1/format-runs/arun_e43e6c5cb2b74052/events",
    "cancel_url": "https://api.sume.com/v1/format-runs/arun_e43e6c5cb2b74052/cancel",
    "expires_at": "2026-08-24T00:53:39.094Z",
    "thread_id": "thr_9af848b9-…",
    "previous_run_id": null,
    "idempotency_hit": false,
    "next_action": "poll_status",
    "request_id": "req_6f560f0f004f4025be47d3d317b6ecf7"
  }
}
```

Two status codes mean success. `202` is a fresh run. `200` is an idempotent replay, the same
`Idempotency-Key` with the same body, and returns the original run with
`idempotency_hit: true`. Both carry the full receipt: store `data.id` and follow the URLs on
it. [Runs and results](/formats/runs) covers what happens next.

## Build your own call

Fill in the fields to rewrite the cURL, TypeScript, JavaScript and Python snippets.
Operations in the TypeScript SDK resolve with `{ data, error, response }` rather than
throwing, so check `error` before reading `data`.

<!-- api-call-example:format-vanity-run -->

## Address a Format

| Shape | Example | Use it when |
|---|---|---|
| `{handle}/{slug}` | `POST /v1/formats/acme/live-commerce/runs` | Every new integration. It is the address the Format detail page shows, and the shape production callers use. |
| `{format_id}` | `POST /v1/formats/skl_…/runs` | A stored URL that must survive a handle or slug rename. Permanent, identical behaviour. |
| `sume/{slug}` | `POST /v1/formats/sume/sume-product-promo/runs` | The [Formats by Sume](/formats/catalog) catalog. Any key with the scopes may call it; the run belongs to that key. |

The two shapes resolve to the same Format and run the same pipeline: same body, headers,
idempotency, caps and receipt. The receipt's `format.id` is always the opaque `skl_…` id,
whichever shape you used. A renamed handle keeps resolving for 90 days.

A team Format lives at the **team's** handle, and any key created in that workspace with the
right scopes may `POST` it. See [Team Formats need a team key](#team-formats-need-a-team-key).
An unknown handle, an unknown slug, and a handle you cannot see all answer the same
`404 format_not_found`.

`GET /v1/formats/{handle}/{slug}` and `GET /v1/formats/{handle}/{slug}/runs` take the same
address.

## Request body

Every field is optional on its own, but the body must name at least one of `instruction`,
`input`, `previous_run_id` or `attachments`. `{}` or `{"input": {}}` is
`400 invalid_request`, not a run on the Format's default. Unknown top-level fields are
`400 unknown_parameter`, with a suggestion when the name is close (`webook_url` →
`webhook_url`).

| Field | Notes |
|---|---|
| `instruction` | The task in your words, up to 8000 characters. Omit it to run the Format's own default instruction. Composed after the Format body, so it wins where they disagree. See [What is accepted and what is carried](#what-is-accepted-and-what-is-carried). |
| `input` | A JSON object of caller data: at most 64 top-level keys and 2 MiB (2097152 UTF-8 bytes, compact). You choose the shape; the Format reads the keys it knows. Media URLs inside it share the run's attachment budget. See [`input` caller data](#input-caller-data). |
| `attachments` | Up to 30 images the agent can see: `{ "type": "input_image", "image_url": … }` or `{ …, "asset_id": … }`. See [Attachments](/formats#attachments). |
| `output_schema` | Bind a JSON Schema and `output` comes back in that shape. `response_format` is the OpenAI-shaped alias; sending both is `400 invalid_request`. Rules and failure modes: [Structured output](/formats/structured-output). |
| `primary_output_key` | The key in `output` whose URL becomes `primary_output_url`. Up to 64 characters. |
| `generation_spend_cap_usd` | This run's generation ceiling, up to the platform maximum of $500. Omit it to inherit the Format's cap; a number above the Format's cap is honored, not clamped; `null` runs at $500; `0` is rejected. See [Spend caps](#spend-caps). |
| `communication.webhook_url` | Public HTTPS URL that receives one signed `format.run.terminal` POST when the run completes or fails. `callback_url` is an accepted alias; top-level `webhook_url` / `callback_url` / `mode` are normalized into `communication`. Delivered on both hosts. See [Webhook](/formats/runs#webhook). |
| `communication.mode` | `async` (default) or `webhook`. Descriptive only: supplying a URL is what arms delivery. |
| `previous_run_id` | Continue an earlier run of this Format as another turn of the same conversation instead of starting fresh. See [Continue a run](/formats/runs#continue-a-run). |
| `on_active_run` | What to do when a run of this Format is already in flight. Default `allow` (runs concurrently; workspace generation concurrency still applies). `skip` records a `skipped` run instead; `reject` answers `409 format_run_in_progress`. Scheduled Actions default to `skip`, so do not copy their bodies here. |
| `model` | Agents catalog id for the LLM that orchestrates the run, for example `gpt-5.6-sol`, the default. Selects the orchestrator only; image, video and audio models are chosen by the Format's tools. An id outside the catalog is `400 invalid_request`. The receipt echoes the id that ran. |
| `idempotency_key` | Body spelling of the `Idempotency-Key` header. The header wins when both are sent. |

Headers: `Authorization: Bearer $SUME_API_KEY` **or** `x-api-key: $SUME_API_KEY` (one, never
both), `Content-Type: application/json`, and `Idempotency-Key` on every create. The request
body is capped at 4 MiB (`413 payload_too_large`).

## `input` caller data

`input` is the JSON object your service hands to the run. It is not a wire schema and Sume
publishes no field list for it: you choose the shape and the Format's recipe reads the keys it
recognises. Two integrations calling the same Format may send completely different objects,
and both are correct. The example bodies on these pages are one integrator's convenient
shape, not a contract.

What the API checks, and nothing else:

| Check | Rule | On failure |
|---|---|---|
| Type | A JSON object. Arrays, strings and numbers are refused; `null` and omission both mean no input. | `400` |
| Property count | At most 64 top-level keys. Nested keys are not counted, so grouping is free. | `400` |
| Size | At most 2097152 UTF-8 bytes (2 MiB) on the compact serialization. | `400` |
| Media references | HTTPS URLs to image, video or audio files, at any depth, share the run's attachment budget: 30 in total, at most 30 images, 10 videos, 10 audio. See [Media referenced from `input`](/formats#media-referenced-from-input). | `400 invalid_attachment` |

Keep the two JSON fields apart in your head: `input` is loose data going in, `output_schema`
is a strict contract coming out. `input` is written whole to a file in the run's workspace and
the agent is told it is caller-supplied data, not instructions. That is exactly where scraped
product copy, a customer's message or a supplier's field belongs, rather than concatenated into
`instruction`. It is a trust boundary, not a sandbox: runs are spend-capped, so the blast
radius of a hostile payload is bounded by the cap, but do not pass raw untrusted text through
on purpose.

`input` does not reach the structured output. `output` is produced from what the run made and
said, so a value you sent, an order id or a SKU, cannot be echoed back unless the run repeats
it. Keep your identifiers on your side, keyed by `data.id` or by your `Idempotency-Key`. See
[Where your object comes from](/formats/structured-output#where-your-object-comes-from).

### What is accepted and what is carried

| Field | Accepted | Carried to the run |
|---|---|---|
| `instruction` | 8000 characters | The first ~4000 characters, as prompt text. Keep it well inside that and put data in `input`. |
| `input` | 2 MiB | All of it, as a file the agent reads. Never truncated. |
| The Format body | No cap beyond 100 MiB per package file | All of it, attached as files. |

An empty `input` (`{}`) adds no file and no block at all, byte-identical to omitting the field.
A Format that says "read `product_url` from the input" then has nothing to read.

## Idempotency

Send `Idempotency-Key` on every create and derive it from the thing being made, your order id
and a version you bump when you deliberately want a re-run, not from the moment of asking. A
`uuidgen` per request makes the header decorative.

| Replay | Result |
|---|---|
| Same key, same body | `200` with the original receipt and `idempotency_hit: true`. No second run, no second charge. |
| Same key, different body, including a different `instruction` or attachment list | `409 idempotency_conflict`. Nothing runs. |
| Same key, two requests at the same moment | One wins; the other gets `409 idempotency_key_in_use`, which is retryable. Wait about a second and resend to receive the original run. |
| Same key after a create that failed (`402`, `503`, …) | The key was released. Fix the cause and retry with the same key. |

Keys are scoped to one Format: the same key sent to two Formats starts two runs. A key is up
to 255 characters.

## Spend caps

Every Format carries a generation spend cap and a run can never spend past its own effective
cap. Read the Format's cap from `generation_spend_cap_usd_micros` on
[`GET /v1/formats/…`](/formats#find-your-formats); a Format that never named one reports the
platform default of $400.

`generation_spend_cap_usd` on the request names this run's own ceiling:

| You send | The run's cap |
|---|---|
| Nothing | The Format's cap. |
| A number up to 500 | That number. Above the Format's own cap is honored, not clamped. |
| `null` | The platform maximum, $500. It lifts the ceiling; it does not remove it. |
| `0`, or above 500 | `400`. A run that cannot spend cannot deliver. |

The effective cap comes back on every receipt as `usage.generation_spend_cap_usd_micros`, and
what the run actually spent against it as `usage.billable_amount_usd_micros`. Production
live-commerce integrations run with caps around $120; a single-scene retry on the same thread
needs a fraction of that. What counts against the cap is metered at the rates on the
[API pricing page](https://www.sume.com/pricing/api).
[Errors and spend](/formats/errors#credits-and-spend) covers what happens at the wallet.

## Keys and scopes

### Scopes

| Scope | Needed for |
|---|---|
| `formats:read` | List and read Formats, read and list runs, read queues. |
| `formats:write` | Create a run, create a bulk queue, cancel a run, redeliver a webhook. |

Scopes are fixed when a key is minted. Keys created before the Formats API shipped do not carry
these, and a key missing one fails every Format request with `403 insufficient_scope`, never a
`404`. Create a new key at [API keys](https://www.sume.com/dashboard/api-keys) and rotate to
it.

Service-account keys cannot create Format runs. They fail with `403 insufficient_scope` and
`details.reason` of `service_account_format_runs_unsupported`.

### Team Formats need a team key

A Format owned by a team workspace is invoked with an API key created in that workspace.
Membership is not enough: a personal key held by a team member is refused with
`403 workspace_key_required`, and `details.workspace_id` names the workspace the key has to
come from.

```json
{
  "error": {
    "code": "workspace_key_required",
    "message": "This Format belongs to a team workspace. Create an API key in that workspace and use it instead of a personal key.",
    "details": { "workspace_id": "org_…" }
  }
}
```

The rule follows the money. A team Format's runs bill the team wallet, count against the
team's generation concurrency, and read their media back through the team workspace. A
personal key would split those, and used to produce runs that made a real video and then
reported `output_schema_unsatisfied` with nothing harvested. Create the key from the team's
dashboard. Personal keys stay right for personal Formats.

Reading is keyed the same way: a team key lists that workspace's Formats for every member, and
never your personal ones. A team handle you are not a member of is `404`, indistinguishable
from one that does not exist, so a `403 workspace_key_required` always means "right team,
wrong key".

### Running a Format another workspace shared with you

A team Format can be shared with another **workspace** — never a user — the way a GitHub
repository takes an outside collaborator. The owner workspace adds your team handle on the
Format's **Access** tab (live immediately), or invites it with
`POST /v1/formats/{handle}/{slug}/grants` and your admin accepts with
`POST /v1/format-grants/{grant_id}/accept` using a key created in _your_ workspace. From then on
you call the Format at the **owner's** address with **your own team key**:

```sh
curl -sS -X POST "https://api.sume.com/v1/formats/{owner-handle}/{slug}/runs" \
  -H "Authorization: Bearer $YOUR_TEAM_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"instruction":"…"}'
```

There is no workspace field on the request: the key is the actor. The run, its spend, its
concurrency slot and its media are your workspace's, not the owner's, and `GET …/runs` with
your key lists your runs only. The owner still decides whether the Format takes API calls at
all — its Status and API call trigger apply to every caller — and can remove your access, after
which the address is a `404` for you again. A personal key is refused the same way it is on any
team Format, and membership of the owner workspace does not stand in for a grant. If you hold
seats in both workspaces, the key you bring decides: each workspace keeps its own run history
and its own bill, and a key from one never reads the other's runs.

## Runs over the API are unattended

A Format written for chat may pause and wait for a person: "approve these stills before I make
the video" is a deliberate quality gate in the Agents UI. Over the API nobody is there, so the
run is told those approvals are already granted and to carry on to the paid step within its
spend cap.

A run that genuinely cannot finish comes back `failed`, never a half-finished `completed`:

```json
{
  "data": {
    "status": "failed",
    "output": null,
    "output_error": { "code": "unattended_blocked", "message": "no avatar matched the brief, so no video was made." },
    "error": { "code": "unattended_blocked", "message": "no avatar matched the brief, so no video was made." }
  }
}
```

One qualification: a `completed` run always did real work and always fills `artifacts[]`, but
it can still carry `output_error` when the projection did not match your `output_schema`.
Check `output_error` before reading `output`. See
[When output cannot be produced](/formats/structured-output#when-output-cannot-be-produced).

## 401 vs 403 vs 404

HTTP class is the first branch; `error.code` is the second. A missing key is
`401 unauthorized`. A known key missing `formats:read` / `formats:write` is
`403 insufficient_scope`, **never** `404 format_not_found`. Scopes cannot be patched onto an
existing key; mint a new one and rotate.

| HTTP | `error.code` | When |
|---|---|---|
| 401 | `unauthorized` | No key, malformed key, two credentials at once, revoked, or unknown. `next_action` is `authenticate`. |
| 403 | `insufficient_scope` | Valid key missing `formats:read` / `formats:write`; `details.required_scope` names it. Also a service-account key on a run create or package write, with `details.reason` set. `next_action` is `authenticate`. |
| 403 | `workspace_key_required` | You are a member of the team workspace but brought a personal key. `details.workspace_id` names the workspace to mint one in. A team key from _another_ workspace is judged by the grant instead: it runs when that workspace holds an accepted grant, and is a `404` when it does not. |
| 404 | `format_not_found` | Unknown, archived, outside this key's workspace, a team handle you are not a member of, or a shared Format whose grant is still pending or was removed. A member's team key on the right handle is never this 404. |
| 404 | `format_run_not_found` | Unknown run id, or a run belonging to another owner. |
| 404 | `format_run_queue_not_found` | Unknown bulk queue, or another owner's. |
| 404 | `previous_run_not_found` | `previous_run_id` is unknown or not yours. |
| 404 | `format_content_not_found` | The Format exists for this key but that package path does not. |

Status classes follow RFC 9110; `insufficient_scope` is the RFC 6750 vocabulary. The `404` on
someone else's Format is a deliberate tenancy hide, not a hint that the Format exists
elsewhere (design record: #2393).

## Errors

Everything the create call can answer with, in one table, is on
[Errors and spend](/formats/errors#errors-at-create). The ones you will meet first:

| Status | `error.code` | What to do |
|---|---|---|
| 400 | `invalid_request` | The body named none of `instruction` / `input` / `previous_run_id` / `attachments`, sent both `output_schema` and `response_format`, or failed a size cap. Read `message`. |
| 400 | `output_schema_invalid` | Your schema is outside the supported subset. `details.violations[]` names every problem. |
| 401 | `unauthorized` | Fix the header, not the body. |
| 402 | `insufficient_credits` | The workspace cannot fund the run. `next_action` is `add_funds`. |
| 403 | `insufficient_scope`, `workspace_key_required` | Mint the right key. See above. |
| 404 | `format_not_found` | Check the address and which key you are holding. |
| 409 | `format_inactive`, `format_api_trigger_disabled` | The owner workspace sets these on the Format page's **API** tab. They apply to every caller, including workspaces the Format is shared with. |
| 409 | `idempotency_conflict`, `idempotency_key_in_use` | Unstable key derivation, or a concurrent duplicate. See [Idempotency](#idempotency). |
| 429 | `rate_limited` | Wait `retry-after` seconds. |
| 503 | `studio_agent_upstream_unavailable` | A Sume-side outage. Retry with the same `Idempotency-Key`. |

`error.code` is a lowercase token you can `switch` on; `message` is for humans and may change.

## Rate limits

Every response carries `ratelimit-limit`, `ratelimit-remaining` and `ratelimit-reset`, and a
`429` adds `retry-after`. Reads and writes have separate budgets and the read budget is forty
times the write one, so a poll loop cannot starve your own creates. A `429` names the budget
it came from in `error.details.scope`. Full table:
[Errors and spend](/formats/errors#rate-limits).

## Next

- [Runs and results](/formats/runs): the receipt, polling, webhooks, continuing and canceling
- [Structured output](/formats/structured-output): bind a schema and get typed JSON back
- [Errors and spend](/formats/errors): every code, credits, and rate limits
- [Cookbook](/formats/cookbook): real-shaped bodies you can paste
- [Bulk runs](/formats/bulk-runs): the same body, up to 100 times, with a concurrency window
