---
title: Format API
description: Run a saved video recipe from your backend with one HTTP call. Create a run, take the receipt by webhook or poll, and read back durable media plus JSON in a schema you supply.
---

A Format is a saved production recipe: a house style, an output contract, a playbook for one
kind of video. Your backend calls it by name, Sume runs it in a fresh sandbox with the
generation tools, and you get back finished media on `media.sume.com` plus, when you ask for
it, a JSON object in a shape you defined.

This page takes you from an API key to a finished run. The pages after it are the reference
behind each step.

## Your first run

You need an API key that carries the `formats:read` and `formats:write` scopes. Create one at
[API keys](https://www.sume.com/dashboard/api-keys) and keep it server-side.

### 1. List the Formats your key can call

```bash
export SUME_API_KEY="sume_live_..."

curl -sS "https://api.sume.com/v1/formats" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": [
    {
      "id": "skl_…",
      "object": "format",
      "handle": "acme",
      "slug": "live-commerce",
      "title": "Live commerce",
      "status": "active",
      "api_trigger_enabled": true,
      "io": { "profile": "url_to_video", "input_kind": "url", "output_kind": "video" },
      "generation_spend_cap_usd_micros": 120000000,
      "vanity_invoke_url": "https://api.sume.com/v1/formats/acme/live-commerce/runs",
      "invoke_url": "https://api.sume.com/v1/formats/skl_…/runs"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

You see what your key's workspace owns plus the ready-made [Formats by Sume](/formats/catalog).
`vanity_invoke_url` is the address you call next.

### 2. Start a run

```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-v1" \
  -d '{
    "instruction": "Vertical 9:16 host video. Use the script as written. No BGM, no captions.",
    "input": {
      "product_url": "https://shop.example.com/p/8823",
      "host_image_url": "https://cdn.example.com/hosts/yura.png",
      "vo_language": "ko"
    },
    "generation_spend_cap_usd": 120,
    "communication": { "webhook_url": "https://acme.example.com/hooks/sume" }
  }'
```

```json
{
  "data": {
    "id": "arun_e43e6c5cb2b74052",
    "object": "format.run",
    "status": "queued",
    "format": { "id": "skl_…", "slug": "live-commerce", "title": "Live commerce", "version": 23 },
    "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",
    "webhook_delivery": { "url": "https://acme.example.com/hooks/sume", "status": "not_armed" },
    "usage": { "currency": "USD", "billable_amount_usd_micros": 0, "generation_spend_cap_usd_micros": 120000000 },
    "thread_id": "thr_…",
    "next_action": "poll_status"
  }
}
```

`202` means a fresh run was accepted. Store `data.id`. Everything else you need is a URL on the
receipt, so you never build a path by hand. Full body reference: [Create a run](/formats/call).

### 3. Take the result

When the run finishes, Sume POSTs the same receipt to your `webhook_url` as one signed
`format.run.terminal` event. If you would rather poll, read the run until `status` is terminal:

```bash
curl -sS "https://api.sume.com/v1/format-runs/arun_e43e6c5cb2b74052" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

A finished run reads:

```json
{
  "data": {
    "id": "arun_e43e6c5cb2b74052",
    "status": "completed",
    "primary_output_url": "https://media.sume.com/artifacts/artf_…/full_video.mp4",
    "artifacts": [
      {
        "id": "artf_…",
        "type": "video",
        "url": "https://media.sume.com/artifacts/artf_…/full_video.mp4",
        "content_type": "video/mp4",
        "duration_ms": 73360
      }
    ],
    "output": {
      "text": "…",
      "videos": [{ "type": "video", "url": "https://media.sume.com/artifacts/artf_…/full_video.mp4", "…": "…" }],
      "images": [],
      "audio": [],
      "files": []
    },
    "usage": { "currency": "USD", "billable_amount_usd_micros": 14959638, "generation_spend_cap_usd_micros": 120000000 },
    "next_action": "none"
  }
}
```

`primary_output_url` is the one thing to show. `artifacts[]` is every file the run made. Media
URLs are durable and public: store them. Bind an [output schema](/formats/structured-output)
and `output` comes back in your own shape instead of the built-in one.

That is the whole loop: create, then webhook or poll, then read. Runs that make video take
minutes, not seconds. Long-form host video typically finishes in 15 to 30 minutes, so design
for the asynchronous path from the start.

## Two hosts, one contract

| Host | Use it for | Keys |
|---|---|---|
| `https://api.sume.com` | Production. Runs spend real credits from your workspace. | Created at [API keys](https://www.sume.com/dashboard/api-keys). |
| `https://api.dev.sume.com` | Integration and staging. Same routes, same receipts, same webhook delivery. | Issued for a development workspace. Ask your Sume contact. |

A key works only on the host it was created for; the other host answers `401 unauthorized`.
Both look like `sume_live_…`, so name your environment variables by host rather than by prefix.
The examples on these pages use production. Swap the host and the key to point them at
development.

## How a run works

```text
POST /v1/formats/{handle}/{slug}/runs
  -> 202, receipt with status_url / result_url / events_url / cancel_url
  -> fresh sandbox boots, the Format's package lands on disk
  -> the agent follows the recipe and calls generation tools
       (host takes, B-roll, voiceover, captions, timeline assembly …)
  -> media is mirrored to media.sume.com
  -> terminal receipt: webhook POST format.run.terminal, or your next poll
  -> read output, artifacts[], primary_output_url
```

Four pieces are in play on every call:

| Piece | What it is | Where it lives |
|---|---|---|
| The recipe | The Format: a `SKILL.md` body plus reference files. The *how*: house style, branch rules, quality bar. | Authored in the Agents dashboard, in chat, or over the [Contents API](/formats/contents). |
| The call | Your `instruction` and `input`. The *what*: product URL, brief, script, prices. | The request body. |
| The run | One fresh sandbox, one agent turn, one receipt. Never a partial delivery: a run that could not finish comes back `failed`. | `arun_…`, at `/v1/format-runs/{run_id}`. |
| The result | Durable media plus `output`, either the built-in shape or one projected onto your [schema](/formats/structured-output). | The terminal receipt. |

Two properties follow. A run is one unit of work: a bulk request is a server-side queue of
ordinary runs, not a different engine ([Bulk runs](/formats/bulk-runs)). And the recipe is
established before your instruction, so you are not re-sending a system prompt on every call
and hoping it holds.

### This is not chat

A Format run is one unattended turn with the Format attached. It does not stop to ask a
person anything: approvals a chat-authored recipe would request are pre-granted, and the run
carries on within its spend cap. The Agents chat UI at
[sume.com/agents](https://www.sume.com/agents) is the surface for a human in the loop, and a
chat turn does not necessarily attach a Format at all. Build partner integrations on runs,
not on chat threads.

## Find your Formats

Three reads, all needing `formats:read`:

| Call | Returns |
|---|---|
| `GET /v1/formats?limit=50` | The Formats your key's workspace owns, plus the first-party catalog. Keyset pages: pass `next_cursor` back as `cursor` while `has_more` is `true`. |
| `GET /v1/formats/{handle}/{slug}` | One Format by its address. |
| `GET /v1/formats/{format_id}` | The same Format by its opaque `skl_…` id. |

Visibility follows the key. A personal key lists your personal Formats; a team key lists that
workspace's Formats, for every member; neither lists the other's. A Format outside your key's
workspace is `404 format_not_found`, the same answer as an id that does not exist. If a Format
you expect is missing, you are holding the other key.

The fields that matter when you pick one:

| Field | Notes |
|---|---|
| `handle`, `slug`, `vanity_invoke_url` | The address to call. `handle` is the owning workspace's handle for a team Format and your own for a personal one; `sume` for the catalog. |
| `invoke_url` | The opaque `skl_…` path. Permanent across renames: persist it if a stored URL must survive a handle or slug change. Renamed handles keep resolving for 90 days. |
| `status`, `api_trigger_enabled` | Both must allow API runs: `inactive` or `false` refuses a create with `409`. A Format you have never run over the API may read `inactive` / `false` until its first run, and still runs. Do not gate your integration on polling them true. |
| `io` | What the Format takes and makes: `input_kind` is `url`, `text`, `image` or `product`; `output_kind` is `video`, `image` or `text`. `null` on Formats saved before this existed. |
| `showcase` | A real output the Format produced at registration, or `null`. |
| `generation_spend_cap_usd_micros` | What a run inherits when it names no cap of its own. $400 for a Format that never set one. |
| `version` | Bumps on every edit. The receipt's `format.version` says which one ran. |
| `package_sha`, `contents_url` | The package behind the Format, for the [Contents API](/formats/contents). |

The recipe body is deliberately not in this shape; it reaches the agent, not the caller.

Formats by Sume answer at the reserved `sume` handle, `POST /v1/formats/sume/{slug}/runs`,
with any key that carries the scopes. The run, its media and its spend belong to the key that
called. See the [Format catalog](/formats/catalog).

Every Format with an address also has a call sheet on this site at
`https://docs.sume.com/formats/{handle}/{slug}`: a share link with the curl, scopes and poll
loop for that Format, and nothing from its body.

## Instruction composition

What the agent actually receives, in order:

```text
[Format: live-commerce v23]         <- a pointer at the recipe; the body is never inlined
[Format attached: … SKILL.md]       <- the whole package, on disk in the run's workspace
[Format run instruction]            <- your `instruction`, or the Format's default
[Sume unattended run]               <- API and scheduled runs only
[Sume action input]                 <- a pointer at your `input`, written whole to a file
[Attached files]                    <- your `attachments`, when present
```

The Format comes first because it is the *how*; your instruction comes after it, so where the
two disagree the model follows what you asked for. Your `input` is written to
`/workspace/inputs/sume-action-input.json`, whole at any size up to the cap, and the agent is
told to read it as data, never as instructions. Open the run's `thread_id` in Agents and the
first message is exactly this text, which is the first thing to read when a run did something
you did not expect.

### How big `SKILL.md` should be

There is no size limit on the body beyond the 100 MiB per file and per package that every
package file shares. Size changes how reliably a recipe is *followed*, not whether it runs:
keep `SKILL.md` a short index the agent can hold at once, and push detail into
`references/*`, which sit beside it and cost nothing until opened. Authoring lives with the
[Contents API](/formats/contents).

## Attachments

A run can carry up to 30 images the agent can look at, as `attachments[]` on the create body:

```json
{
  "instruction": "Make a product hero from the attached photo.",
  "input": { "brand": "Acme" },
  "attachments": [
    { "type": "input_image", "image_url": "https://cdn.example.com/shot.jpg" },
    { "type": "input_image", "asset_id": "asset_…", "filename": "packshot.png" }
  ]
}
```

| Field | Required | Notes |
|---|---|---|
| `type` | yes | `input_image`, the only type today. |
| `image_url` | one of | Public HTTPS URL. Sume fetches it when you create the run, so it must be reachable without auth. |
| `asset_id` | one of | An image you uploaded through the [Assets API](/api/reference#media-inputs), ready, in the same workspace. |
| `filename` | no | The label the agent sees. Defaults to the URL's basename. |

Sume fetches every attachment at create time, checks its real type and size, and copies it into
durable storage, so a broken or private image fails the create with a `4xx`/`5xx` you can act on
instead of killing the run minutes later. An `asset_id` or a URL already on `media.sume.com` is
not re-copied.

| Limit | Value |
|---|---|
| Types | JPEG, PNG, WebP, GIF, AVIF |
| Images per run | 30 |
| Bytes per image | 30 MB |
| Bytes per run | 500 MB |

### Media referenced from `input`

Many Formats take their references through `input` fields instead: `host_image_url`,
`product_image_urls[]`, `input_reference_image_urls[]`, narration URLs. Those count too,
against one budget shared with `attachments[]`: 30 files per run in total, of which at most 30
images, 10 videos and 10 audio files. The check is by file type, not field name. Any HTTPS URL
anywhere in `input` whose filename ends in a media extension counts, however deeply nested; a
product page URL does not, and the same URL repeated counts once. Media the agent finds for
itself during the run is not yours and does not count. Over any of these is
`400 invalid_attachment` on the create.

### Attachment errors

| Status | Code | Cause |
|---|---|---|
| `400` | `invalid_attachment` | Wrong `type`, missing or non-HTTPS URL, both `image_url` and `asset_id`, too many items, or a source that is not an allowed image type. |
| `400` | `attachment_not_found` | `asset_id` is unknown in this workspace. |
| `413` | `attachment_too_large` | An image is over 30 MB, or the set is over 500 MB. |
| `502` | `attachment_fetch_failed` | Sume could not fetch the image: unreachable host, hotlink protection, or a non-2xx answer. `details.index` names the attachment. |

`Idempotency-Key` covers attachments: replaying a key with a different image list is
`409 idempotency_conflict`, and a true replay does not re-fetch your images.

## What the API does not do

- **No push channel for progress.** There is no SSE or WebSocket stream. `events_url` is a
  polled phase timeline (`preparing`, `running`, `finalizing`), not agent output or logs.
  Completion is pushed; that is what the webhook is for.
- **No list of all runs.** Runs are listed per Format (`GET /v1/formats/{handle}/{slug}/runs`)
  and read one at a time at `/v1/format-runs/{run_id}`. There is no `GET /v1/format-runs`.
- **Team Formats need a team key.** A Format owned by a team workspace is callable, on both
  URL shapes, only with a key created in that workspace; a personal key fails with
  `403 workspace_key_required`. See [Create a run](/formats/call#team-formats-need-a-team-key).
- **Images only as attachments.** `input_image` is the only attachment type. Send documents by
  URL in `input`, and video or audio references the same way.
- **Authoring is a separate surface.** Create and edit the package over the
  [Contents API](/formats/contents), or in the dashboard. The run endpoints only execute.

## Next

- [Create a run](/formats/call): the request body, idempotency, spend caps, keys, and every create error
- [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 in one place, credits, rate limits
- [Cookbook](/formats/cookbook): copy-paste recipes for a real-shaped run, a webhook receiver, a scene retry, a batch
- [Bulk runs](/formats/bulk-runs): queue up to 100 runs with a concurrency window
- [Format catalog](/formats/catalog): ready-made Formats by Sume
- [Embed a Format in your product](/cookbooks/embed-a-format): key custody, spend tiers and artifact handling for a multi-tenant product
