# HTTP API When to read this: you call TweenFrame over HTTP instead of through the `tweenframe` CLI, or you need the exact request and response shapes. The machine-readable contract is the OpenAPI 3.1 document at `/api/v1/openapi.json`; this guide is the short version. ## Base URL and auth All endpoints live under `/api/v1`. Bodies are JSON (`Content-Type: application/json`) except uploads (`multipart/form-data`). Create a token once and keep it secret: ```http POST /api/v1/identity ``` ```json { "id": "…", "token": "…", "expires_at": "2027-10-04T12:00:00Z", "docs": { "skill": "/docs/agent/SKILL.md", "api": "/docs/agent/api.md" } } ``` Send it on every call that needs it as `Authorization: Bearer `. Email sign-in, with the anonymous token as the bearer: ```http POST /api/v1/auth/email/start {"email": "person@example.com"} ``` ```json { "challenge_id": "…", "expires_at": "2026-10-04T12:10:00Z" } ``` ```http POST /api/v1/auth/email/verify {"challenge_id": "…", "code": "042917"} ``` ```json { "token": "…", "kind": "account", "email": "person@example.com", "account_id": "…", "workspace_id": "…" } ``` The `token` is an account API key without an expiry; send it as the bearer from now on and repeat the request. Images uploaded with the anonymous token move to the account and keep their URLs. The code is a string of 6 digits (leading zeros count). ## Endpoints | Method and path | Token | Purpose | | ------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /identity` | no | Create an anonymous identity and token. | | `POST /auth/email/start` | yes | `{email}`: send a 6-digit code; answers `{challenge_id, expires_at}`. | | `POST /auth/email/verify` | yes | `{challenge_id, code}`: check the code; answers an account key `{token, kind, email, account_id, workspace_id}`. | | `GET /recipes` | no | List published recipes with `inputs_summary`. | | `GET /recipes/{id}?version=` | no | One recipe: `input_schema`, `asset_paths`, `output`, `examples`; `document=1` adds its recipe `document`. `{id}` contains a slash, e.g. `/recipes/captions/default`. | | `GET /fonts` | no | Font groups of the TweenFrame set; a Typography `font` also takes any Google Fonts family by its exact name. | | `GET /library` | no | Library sets: icons and decor shapes a `vector` or `image` node takes as `src`. | | `GET /library/items?q=&set=&limit=&offset=` | no | Search library items by words (or browse a set without `q`); each item has a library path per variant. | | `POST /assets` | yes | Upload one file (JPEG, PNG, WebP or SVG image, GIF or video) in the multipart field `file`, or JSON `{url}` to have it downloaded (needs an account). Answers 201 with the asset. | | `POST /assets/analyze` | yes | `{url, aspects?}`: focal point, subject, faces and a cover crop per aspect. | | `POST /recipes/check` | yes | `{document, inputs?}` or `{recipe, inputs}`: check your own document or a catalog recipe; answers its public view and its layout: `timeline` (ms), `scene` (size) or `pages` (count, size, print_size). | | `POST /renders` | yes | `{recipe: {id, version?} \| {document} \| {template_id}, inputs, output?}`: render a catalog recipe, your own document or a saved one. | | `GET /renders/{uid}?poll_token=` | yes | Poll a pending render, compression or conversion: `poll_url` as the pending answer gives it (`uid` and `poll_token` are its fields). | | `POST /templates` | yes | `{document, name?}`: save a recipe document; `GET` lists, `GET`/`PUT`/`DELETE /templates/{id}` read, replace or delete one. | | `POST /links` | yes | `{template_id}`: a public link to a saved recipe; `GET /links?template_id=` lists, `DELETE /links/{id}` removes. See links.md. | | `POST /publish` | yes | `{result_url}`: a lasting public URL for a result file; `GET /publish` lists, `DELETE /publish/{id}` removes. | | `GET /openapi.json` | no | The contract. | Tools, token required, answering the render envelope: - `POST /images/remove-background`: `{url}`, a PNG of the subject cut out ([assets.md](/docs/agent/assets.md#remove-a-background)). - `POST /gifs/compress`, `POST /videos/to-gif`: `{url, preset?, settings?}` and `{url, settings?}` ([gif-tools.md](/docs/agent/gif-tools.md)). - `POST /recipes/from-figma`: a Figma frame's dump as multipart `file`; answers a recipe document, not a render ([import-figma.md](/docs/agent/import-figma.md)). The recipe list is complete in one response; there is no pagination. Catalog, font and library responses may be cached for a short time. The guides are listed at `/docs/agent/index.json` (name, title, URL), outside the API prefix. ## Successful responses Responses are the documented object itself, without a wrapper (the CLI adds `{"data": …}`). Successful responses carry a `docs` object with origin-relative links to the guide that explains them: `recipes`, `recipes/{id}` and `fonts` link [recipes.md](/docs/agent/recipes.md), `library` responses [icons.md](/docs/agent/icons.md), asset responses [assets.md](/docs/agent/assets.md), render results [output.md](/docs/agent/output.md) as `docs.output`, `identity` the skill and this guide. The sign-in steps under `/auth/email` carry none. ## Rendering and polling `POST /renders` answers 200 with one of three shapes, told apart by `status`: ```json {"status": "completed", "uid": "…", "result_url": "https://…/result.gif", "format": "gif", "stats": {…}, "recipe": {"id": "…", "version": 1, "hash": "…"}, "docs": {"output": "/docs/agent/output.md"}} {"status": "pending", "uid": "…", "poll_url": "/api/v1/renders/…?poll_token=…", "poll_token": "…", "docs": {"output": "/docs/agent/output.md"}} {"status": "failed", "uid": "…", "error": {"code": "render_failed", "message": "…", "hint": "…", "docs": "/docs/agent/errors.md#render_failed"}, "docs": {"output": "/docs/agent/output.md"}} ``` For `pending`, `GET` the `poll_url` (relative to the origin) with the same bearer every one to two seconds until the status changes. Download `result_url` with a plain GET; it is absolute or relative to the origin. The `result_url` is temporary: download the file and keep it, and never hand the URL to the user as a lasting link. For a lasting link to the file, publish it (`POST /publish` with the `result_url`, an account only) and give the published `url`. `recipe` takes one of two forms: ```json {"recipe": {"id": "captions/default", "version": 2}, "inputs": {…}, "output": {…}} {"recipe": {"document": {…your recipe document…}}, "inputs": {…}, "output": {…}} ``` A document is written in the same format as catalog recipes. Its `default_output` is merged with `output` the same way as a catalog recipe's defaults; its inputs take managed URLs and catalog asset paths like a catalog recipe's. The completed render's `recipe.hash` identifies the exact document. `canvas: {"aspect": "9:16"}` beside `inputs` renders the recipe at another aspect (`W:H`, such as `1:1`, `4:5` or `1.91:1`): the width stays the recipe's canvas width and the height follows the aspect; for a recipe whose canvas height would follow its content or a picture, the aspect fixes the height and the recipe's layout shares the space. `POST /recipes/check` takes the same `canvas`, and its `timeline.size` (or `scene.size`) shows the result. A malformed aspect, or one for a recipe that takes none (`canvas.takes_aspect` of `GET /recipes/{id}` is false: its width follows the content, or its scenes derive their own height), is a 422 `invalid_canvas` at `["canvas", "aspect"]`; `details.inputs` then names the recipe's own aspect inputs, when it has any. A completed or pending render of your own document carries the same `warnings` as its check; a catalog recipe's render carries none, so check it first. Inputs follow the recipe's `input_schema`; image values are managed URLs from `POST /assets` or catalog asset paths (`file://` is a CLI feature). Output options are described in [recipes.md](/docs/agent/recipes.md#output-options) and, with formats, sizes and file sizes, in [output.md](/docs/agent/output.md). ## Checking a document `POST /recipes/check` validates your own document without rendering it and is not billed: ```http POST /api/v1/recipes/check {"document": {…}, "inputs": {…}} ``` ```json { "title": "…", "input_schema": { "…": "…" }, "asset_paths": [["photos", "*", "src"]], "inputs_summary": { "required": ["photos"], "assets": [], "texts": 2, "colors": 1, "lists": 1, "media": 1 }, "output": { "format": "gif", "width": 900 }, "produces": "timeline", "engine": "…", "timeline": { "size": [900, 900], "duration_ms": 6000, "clips": [{ "id": "p0", "start_ms": 0, "duration_ms": 2000 }, "…"], "seams": [ { "after": "p0", "before": "p1", "kind": "fade", "start_ms": 1800, "end_ms": 2200, "region": null } ], "overlays": [{ "id": "brand", "start_ms": 0, "duration_ms": 6000 }] }, "scene": null, "pages": null, "warnings": [], "docs": { "guide": "/docs/agent/recipes.md", "errors": "/docs/agent/errors.md" } } ``` `inputs` is optional: without it only the document itself is checked and `timeline`, `scene` and `pages` are `null`; with it the document is also expanded with those inputs, which catches mistakes in substitutions, conditions and repeats. `produces` echoes the document's kind: `timeline` (motion), `scene` (a still) or `pages` (printed pages); a scene recipe answers `"scene": {"size": [w, h]}`, a pages recipe `"pages": {"count", "size", "print_size"}` (pages, pixels of a page, points of the paper), and the other layouts stay `null`. A rejected document or input answers 422 with the same codes as a render (`invalid_recipe_document`, `invalid_recipe_inputs`) and `details.path`. With inputs, `warnings` lists what renders but not as written (a check of a catalog recipe reports the layout only): `{code: "missing_glyphs", path, font, chars}` when the text at `path` has characters its font cannot draw (they come out as empty boxes); change the font or the characters. `{code: "layout_overflow", path, node_id, axis, required, available}` when the frame at `path` lays its children out in more room than it has along `axis` (in canvas pixels), so what does not fit may be cut; let a child fill or shrink, or choose another aspect. Image values in the document and inputs are managed URLs; the CLI (`tweenframe recipes check --document --input `) uploads `file://` URLs first. `timeline` is the layout of the expanded recipe in time, with the durations the render will use: each clip's start and length, each transition window (`start_ms` to `end_ms`, on the same clock as `output.time`; a seam from the last clip back to the first ends past `duration_ms` and continues from 0; `region` is the node a transition is confined to, `rect` or `null`) and each overlay. Image and GIF sources are measured, so a canvas that hugs its media and a clip as long as its GIF show their real numbers; where a source cannot be read before rendering (a video's length), `estimated: true` marks the clip and the layout, whose numbers are then upper bounds. Pick still moments from it. A catalog recipe is checked the same way with `{"recipe": {"id": …, "version"?: …}, "inputs": {…}}` (CLI: `tweenframe recipes check --input `). ## Errors Every failure has a non-2xx status and this body: ```json { "error": { "code": "invalid_recipe_inputs", "message": "…", "details": { "path": ["slides", 0, "backdrop", "media", "src"] }, "hint": "One sentence: what to do next.", "docs": "/docs/agent/errors.md#invalid_recipe_inputs", "reference": "/api/v1/recipes/captions/default?version=2", "output_reference": "/api/v1/openapi.json#/components/schemas/OutputOptions" } } ``` `code`, `message`, `hint` and `docs` are always present. `details.path` points at the offending value as a list of keys and indexes. `reference` (the recipe whose schema the inputs broke) and `output_reference` (the output options schema) come with 422 answers from `POST /renders`. A 429 may carry `Retry-After`. Every code is described in [errors.md](/docs/agent/errors.md). ## Limits Request bodies and uploads have size limits by kind; one over its limit fails with `payload_too_large`, which names the limit. Output options are bounded by their schema in `openapi.json`; the renderer may refuse an oversized document with its own error. See [recipes.md](/docs/agent/recipes.md#limits).