Pipelines, models and runs, from code or an agent.
Everything the heddla app can do with a pipeline, from your own code or from an AI agent such as Claude, Claude Code or Cursor: browse the model catalog, author pipelines, run them, and read the results. Two doors, one account: plain HTTP/JSON with an API key, or the same operations as MCP tools, connected by signing in or with a key.
Connect an agent over MCP
The API is an MCP server at https://api.heddla.com/mcp
(Streamable HTTP, stateless). Any client that speaks MCP over HTTP connects to that address. No key is needed: the
client sends you to the heddla studio, where you sign in the way you made your account (Apple, Google or a password)
and approve the connection (OAuth 2.1 with PKCE, the protocol's own authorization). The connection then appears under
Settings → API & agents in the studio, named after the client, and is disconnected there like any key.
Claude (claude.ai, Claude Desktop)
Settings → Connectors → Add custom connector, paste https://api.heddla.com/mcp, then Connect and sign in.
Claude Code
claude mcp add --transport http heddla https://api.heddla.com/mcp
Claude Code opens the sign-in the first time you use a tool (/mcp shows the status). To skip the
sign-in, on a server or in CI, pass a key instead:
claude mcp add --transport http heddla https://api.heddla.com/mcp \
--header "Authorization: Bearer heddla_your_key_here"
Cursor, Codex, ChatGPT and others
Anything that takes an HTTP MCP server. Sign-in happens on connect when the client supports OAuth; otherwise add the header:
{
"mcpServers": {
"heddla": {
"type": "http",
"url": "https://api.heddla.com/mcp",
"headers": {"Authorization": "Bearer heddla_your_key_here"}
}
}
}
As a skill
Agents that work from instructions rather than tools (a Claude Code skill, a Codex AGENTS.md, a
system prompt) can load https://api.heddla.com/docs/skill.md: the same rules and endpoints as a Markdown
skill that reads the key from HEDDLA_API_KEY. Save it as .claude/skills/heddla/SKILL.md
(or wherever your agent keeps skills), put the key in the environment, and the agent knows how to use heddla.
Tools
On connect the server hands the agent the pipeline rules below as its instructions, then offers these tools:
| tool | what it does |
|---|---|
| whoami | The account this connection belongs to: email, username and balanceUsd. get_balance adds the top-up link. |
| list_models | The model catalog: id, provider, input and output types, price (price_usd + price_unit), typical response time, availability and option keys. Filter by input/output type to find a capability (e.g. output=video). Use get_model for a model's option schema and long description. |
| get_model | One model in full: what it is good for, its inputs and outputs, and every option with type, default, choices or range, and how each choice changes the price (delta_usd). |
| list_pipelines | The account's pipelines, newest first: id, name, description, status (ready, drafting, draft_failed), step count, cost per run. Set detailed for the full steps. |
| get_pipeline | One pipeline with its steps, wiring, options, input_meta, status and cost. While a draft is in flight (status drafting), wait_seconds holds until it is ready or has failed; draft_events narrates the builder's progress. |
| create_pipeline | Save a new pipeline. The body is validated against the catalog: every declared input wired, types matching, sources referencing earlier steps only, options known to the model. A 422 says exactly what to fix. The pipeline is ready to run on success. Pass workshop_id to build it on that workshop's bench instead: a working copy the person's list does not show until they save it. |
| update_pipeline | Replace a pipeline's name, description, steps and input_meta in full (there is no partial update — send the whole definition). Same validation as create_pipeline. Refused while a draft is still in flight. |
| delete_pipeline | Delete a pipeline. Its past runs stay readable under their execution ids. |
| draft_pipeline | Have heddla's builder compose a pipeline from a plain-language brief — useful when the goal is clear but the model choice and prompt craft are not. Asynchronous: returns the pipeline in status drafting; poll get_pipeline with wait_seconds until it is ready, then review the steps and adjust with update_pipeline. With a test_budget_usd the builder runs real test generations while drafting (the budget is reserved up front, the unused part refunded); 0 drafts in one shot without spending. |
| get_pipeline_spec | The run contract of a pipeline: the inputs a run must send (name, type, choices), the options open to runners, the outputs, the price (total_cost_usd), and an example request body. Read this before run_pipeline. |
| run_pipeline | Start a run. The whole run's price is reserved from the owner's balance up front (402 if short) and the run continues server-side. By default the call waits up to 30 seconds for the run to finish and returns the execution: status, per-step results (text inline, binary outputs as file_id plus a download url), error. If it is still running, keep polling with get_execution. |
| get_execution | One run's state: status (pending, running, completed, failed), current step, per-step results and errors, inputs. wait_seconds holds the call until the run settles. Download urls on binary outputs expire after about an hour; read again for fresh ones. |
| list_executions | Recent runs, newest first, optionally for one pipeline: status, timing, step progress (no inputs or results — use get_execution). Runs of a workshop bench are tries and stay out of this list unless pipeline_id names the bench; the workshop's journal shows them. |
| start_build | Open a workshop: a bench with a pipeline on it and the journal of the work done to it (the build_pipeline prompt explains how the bench works; follow it). Pass pipeline_id to copy an existing pipeline onto the bench, leaving the original untouched, or leave it out for an empty bench and create the pipeline with create_pipeline + workshop_id. Returns the workshop with its id. |
| log_build | Append one entry to a workshop's journal. kind: frame (what is wanted, restated), plan (what you propose to do), stage (something built and run: give stage, and refs with pipeline_id, execution_id, inputs, outputs, cost_usd), review (the person's answer: author person, their words, and for a choice the option id in answer), assemble (the whole thing run end to end), note (anything learned). Add `ask` to put a question to the person: the page then shows it with the controls to answer, instead of leaving them to work out what you want. Log before you ask; every id you mention goes in refs. |
| get_build | A workshop with its bench and its whole journal, file references carrying fresh urls. Read it to resume: continue from the last entry. working says heddla's own agent is mid-turn at the bench, which answers 409 to your writes until that turn ends. |
| list_builds | The account's builds, open ones first. |
| save_pipeline | Take what is on a workshop's bench and save it as a pipeline of the owner's own, frozen as it is right now. This is the only thing that adds to their list. Save whenever something is worth keeping; the workshop keeps going and can be saved again later, each save its own pipeline. Returns the saved pipeline. |
| finalize_build | Close a build with the pipeline it produced. Do this after the person approved the assembled pipeline's end-to-end run, then delete the scratch pipelines. |
| abandon_build | Close a build without a pipeline, when the person stops. The journal stays readable. |
| begin_upload | Start a direct upload of a file on disk: returns a file id and a presigned PUT url, valid 30 minutes. Send the bytes there yourself from the shell (curl -sS -T photo.jpg '<upload_url>') — nothing passes through the tool call — then call finish_upload with the file id. This is the way to upload photos and videos; images, audio and documents up to 25 MB, video up to 200 MB. Say what the file is with a filename that has a known extension (jpg, png, webp, gif, mp3, m4a, wav, webm, ogg, mp4, mov, pdf, docx) or a mime type. |
| finish_upload | After begin_upload and your PUT: the bytes are inspected (type sniffed, size checked) and the file becomes ready, usable as a run input or a baked step value. Returns the file like get_file. When the account already keeps these exact bytes, the new upload is dropped and the answer is the file kept before, with existing: true and its own id — always use the id this returns. Harmless to call again on a finished file; a 409 means nothing has been PUT to the url yet. |
| upload_file | Upload a small file or a public link. Send the bytes as base64 (up to 25 MB decoded, but every byte travels inside this call, so keep it to small files) or a public https url the server fetches (images, audio and documents up to 25 MB, video up to 200 MB); exactly one of the two. For a photo or video on disk use begin_upload instead. The type is sniffed from the bytes — set mime when the bytes alone are ambiguous (m4a audio). Returns the file: id (use it as file_id), kind, mime_type, size_bytes and a download url; the same bytes uploaded again return the file already kept (existing: true). |
| get_file | Metadata and a fresh download url (valid about an hour) for a file the account uploaded or a run produced, shaped like a list_files item: preview, size on screen, length, the run that made it and what uses it, and for a cut what it was cut from (cut_from). Upload with begin_upload + finish_upload (files on disk) or upload_file (small files, links), or through REST: POST /v1/files as multipart with the same key. A 409 names a pending upload that was never finished. For a cut still being made (cut_clip answered status: cutting) it answers the same way, and wait_seconds holds the call until the cut is made or has failed. |
| list_voices | The voices the account saved, newest first: name, the recordings they were cloned from, when consent was given and in what words. Each one's value is the answer that picks it on any option marked saved_voices. Saving and removing voices is the person's to do, since it takes their consent to cloning a voice. |
| list_files | The account's library, most recently added first (added_at; an upload of bytes already kept counts as added again): the files it uploaded (kept until the owner removes them) and the files its runs made (they go when their run is deleted). By default uploads plus what runs handed back as results; role intermediate or any adds the other steps' outputs. Each file has its id (use it as file_id instead of uploading the same thing again), kind, mime_type, size_bytes, a fresh url, a preview_url for pictures and clips, width, height and duration_seconds when known, source (upload or run), for a run's file its role and the run that made it (run id, pipeline or recipe, step), and used_by: the pipelines that bake it in, the recipes that use it and the runs that took it as an input. Page with cursor: pass the next_cursor of the previous answer; it is null on the last page. |
| storage_usage | How much room the account's library takes: used_bytes and file_count, broken down by kind, by source (upload or run) and, for runs' files, by role (result or intermediate), against allowance_bytes. Nothing is enforced (enforced: false): going over the allowance stops nothing and removes nothing. |
| find_people | For a pipeline or recipe with a cast input (an input whose spec carries cast: {of: <video input>}): looks at one of the account's video files and lists the people in it, each with an id (p1, p2, … in the order they first appear), a few words on how they look, their face picture (face.url), the moment it is clearest and where they are in every frame looked at. Run it on the clip you will send for cast.of, then send the cast input as [{"type": "image", "file_id": <photo>, "person": <id>}, …], one photo per person to replace, in the order the photos should go (Image 1 first); anyone not named stays as they are. The first call on a clip takes a minute or two (wait_seconds holds the call; status is finding until then); later calls answer at once with the same people. |
| cut_clip | Cut the parts a person wants out of one of the account's videos, joined in the order they play, into a new video of theirs. Use it before a step that takes a clip by length (an option marked clip_length: true) instead of sending the whole clip, which the run would otherwise cut from its start, and before find_people, so the search looks only at what will be used. parts is [{"start": 12.5, "end": 20}, …] in seconds from the clip's start (to the millisecond): in order, not overlapping, each at least 0.5 s, at most 10, together no longer than the longest clip a model takes (a refusal names the limit). Each part is cut on its frames — within a frame of its length, never longer — with its sound in step, and parts join with a hard cut. Returns the new file like get_file, with cut_from {id, parts}; the same parts of the same clip again return the same file (existing: true). Cutting takes seconds, or minutes for a long cut of a large clip: wait_seconds holds the call (60 by default), and status: cutting means it is still being made — get_file with its id and wait_seconds keeps waiting. A cut that failed is an error whose details say why; calling cut_clip again starts it over. Then send the cut's id as the clip, and set the step's clip-length option to the parts' total rounded up to the option's step: the cut never runs longer than that, so the run takes it as it is. |
| list_apps | Published recipes on heddla's shelf — pipelines made public by heddla and its creators, runnable on the caller's balance. Search by words in the title, tagline or author; sort by newest (default), stars or runs. |
| get_app | One published app with its steps and inputs. |
| get_app_spec | The run contract of a published app: required inputs, open options, outputs, the price (total_cost_usd) and an example request. Read this before run_app. |
| run_app | Run a published app on the caller's balance, exactly like run_pipeline: reserved up front, continues server-side, waits up to 30 seconds by default, then get_execution. |
| get_balance | The owner's balance in US dollars (balance_usd) and topup_url, the page where they add funds. Call it when a run or draft is refused with 402 and hand the owner the link — you cannot add funds yourself. amount_usd preselects a suggested amount on that page. |
| ledger | The account's last 100 balance movements (amount_usd each): run reservations and refunds (with execution ids), purchases, grants. For the balance alone with the top-up link, use get_balance. |
Every tool runs the matching REST endpoint in-process, so validation, pricing and error messages are identical
whichever way you call. Files included: upload_file takes the bytes as base64 or a public
https url and runs the same upload as POST /v1/files; for real files on disk,
begin_upload hands back a presigned PUT url so the bytes go straight to storage
from the shell, and finish_upload makes the file ready.
API keys
For your own code, or an agent without a browser: create a key under Settings → API & agents in the studio, or Settings → API keys in the app. It is shown once. Send it on every request:
Authorization: Bearer heddla_your_key_here
A key, and a signed-in agent, acts as the account behind it: the pipelines it makes are that account's, the runs it starts spend that account's balance and show up in the Runs tab. Neither can mint or revoke keys, add funds, or publish recipes. Those stay in the app and the studio, so a leaked key is contained. Revoke it there if it leaks.
Balance and funds
curl https://api.heddla.com/v1/balance -H "Authorization: Bearer heddla_your_key_here"
Answers with balance_usd and topup_url, the page where the account's owner adds
funds by card (?amount_usd=25 preselects an amount). A run or draft the balance cannot cover is refused
with 402 before anything starts, and the error carries the same topup_url; an agent hands
it to you. Agents cannot add funds themselves.
The workflow
1 · Browse the catalog
curl "https://api.heddla.com/v1/models?view=summary¤t=true&output=image" \
-H "Authorization: Bearer heddla_your_key_here"
Filters: provider, input, output (a type: text, image, audio, video,
document, web, archive), available=true, and current=true, which leaves out models a newer
one replaces (they carry superseded_by; steps that already use them keep running, new steps take the
successor). view=summary leaves out the long descriptions and option schemas;
GET /v1/models/{id} has everything for one model.
2 · Create a pipeline
curl -X POST https://api.heddla.com/v1/pipelines \
-H "Authorization: Bearer heddla_your_key_here" \
-H "Content-Type: application/json" \
-d @pipeline.json
The body is validated against the catalog; a 422 says exactly what to fix. The answer carries the
pipeline's id and total_cost_usd per run. GET /v1/pipelines/{id}/spec shows the
run contract: the inputs a caller must send, the options open to them, and an example request.
3 · Run it
curl -X POST https://api.heddla.com/v1/pipelines/PIPELINE_ID/executions \
-H "Authorization: Bearer heddla_your_key_here" \
-H "Content-Type: application/json" \
-d '{"inputs": {"brief": {"type": "text", "text": "a lighthouse at dawn"}},
"step_options": {"paint": {"size": "1024x1024"}}}'
The API answers 202 at once with the execution; the price is reserved from the balance up front and
the run continues server-side whether or not you keep watching.
4 · Read the result
curl "https://api.heddla.com/v1/executions/EXECUTION_ID?wait=60" \
-H "Authorization: Bearer heddla_your_key_here"
?wait= holds the request until the run has completed or failed, up to 120 seconds, and returns
the latest snapshot either way: one call instead of a polling loop. GET /v1/executions/{id}/events streams
the same snapshots over SSE. Results sit under results[step_id].outputs, and the outputs a pipeline
names as its deliverables come back under deliverables as well. Binary outputs carry a
file_id and a presigned url that expires after about an hour.
Pipeline shape
{
"name": "Poster maker",
"description": "A one-line brief becomes a poster",
"steps": [
{
"step_id": "write",
"name": "Write the image prompt",
"model_id": "google-gemini-4-argon",
"input_sources": {
"text": {
"pre_input": "Write one vivid, specific image prompt for this brief:\n",
"sources": [{"step_id": "", "output_key": "brief"}]
}
}
},
{
"step_id": "paint",
"name": "Paint it",
"model_id": "openai-image-2-5",
"input_sources": {
"text": {"sources": [{"step_id": "write", "output_key": "text"}]}
},
"options": {
"size": {"ask": true, "default": "1024x1536", "note": "Pick the canvas"}
}
}
],
"input_meta": {
"brief": {"label": "Brief", "placeholder": "A lighthouse in a storm, seen from the sea", "help": "What should the poster show?"}
}
}
- Steps run in array order. Each step has step_id (^[a-z0-9_-]{1,50}$), model_id (a catalog id), input_sources, and optional name and options.
- input_sources has one entry per input TYPE the model declares — the key is the type name ("text", "image", "audio", "video", "document"), the value is {"sources": [...]} with an optional "pre_input" (text inputs only: hidden text placed before the joined sources).
- A source is exactly one of: an earlier step's output {"step_id": "<earlier step>", "output_key": "<that step's output type>"}; a run input the caller supplies at execution time {"step_id": "", "output_key": "<input name>"}; a fixed text value {"value": {"type": "text", "text": "..."}} (bake prompts and instructions in here); or a baked file {"value": {"type": "image|audio|video|document", "file_id": "..."}} — a file the account uploaded that every run reuses (a driving clip for motion transfer, a style frame, a logo), so runners never supply it. Only reference file_ids that exist on the account.
- Types line up exactly: an image input needs an image source. Text inputs may fan in several sources (joined in order); binary inputs take one source unless the model's multi_inputs allows more (then order matters, e.g. clips join in wiring order).
- Run input names are shared identity: two steps wiring the same name receive the same caller value; different names mean separate values. Name inputs after what they are ("photo", "clip", "brief"), never a different type than the input they feed.
- Options: {"value": "<canonical string>"} fixes an option; {"ask": true, "default": "...", "note": "..."} opens it to runners, who pass it as step_options[step_id][key] at run time; an asked choice option may add "choices": ["<answer>", ...] to offer only those answers, in that order (at least two, the default among them). Only option keys the model lists exist; get_model shows them with choices, ranges and what each does to the price. An option marked "clip_length": true (in get_model and the spec) is the length in seconds of the clip its step takes in, and what the step is priced for: one longer video clip is cut to it from its start; a longer recording, or clips that run longer together, are refused. To send other parts of a clip, cut them first: cut_clip (POST /v1/files/{id}/cut) joins the parts a runner picks into a new video of theirs; send that as the clip, and set the option to the parts' total rounded up to the option's step (at least its min), so the step takes the cut as it is.
- input_meta gives run inputs a label, help text and, for text inputs, a placeholder (an example of what to type, shown greyed in the empty field, at most 120 characters) or a fixed list of choices, keyed by input name.
- Optional inputs and steps: input_meta {"optional": true} lets runners leave a run input empty, and the sources that read it drop out. A step may carry "when": {"input": "<run input>", "provided": true} (or false: the optional input was left empty) or {"input": "<choice input>", "equals": "<one of its choices>"}; when it does not hold the step is skipped, and so is any step left with nothing to feed one of its inputs. A one-value binary input may list alternatives: the first source that is there is used (a cover from the photo step when there is a photo, else from the words step). input_meta {"choices": ["Off", "On"], "toggle": true} shows a two-choice input as a switch, off unless sent; an input only conditions read needs no wiring, just its choices. A deliverable's "alternatives": [{"step_id", "output"}] stand in, in order, when its step was skipped. Every way a run can go must still make at least one deliverable, and a run reserves only the steps it will take.
- Casts: input_meta {"multi": true, "cast": {"of": "<video run input>"}} makes an image run input the photos that replace people found in that clip. Runners find the people with find_people (POST /v1/files/{clip}/people) and send one photo per person to replace, each naming them: [{"type": "image", "file_id": "...", "person": "p2"}]; anyone not named stays. Read as images (an image input wired to it) the cast is the photos in the order sent, Image 1 first; read as text it is the cast list: who each Image replaces, with their clearest moment and box, who stays, and who is where at those moments. tool-cast-sheet draws that on one picture for a vision step (its video must be the cast's own clip input).
- deliverables names what a run gives: a list of {"name", "step_id", "output"} promoting step outputs to results by name (e.g. hero → step "page" output "web"); runs return them under deliverables, the spec lists them, and screens show them first. Set them on every pipeline whose result is more than its last step's output.
- Two output kinds exist only as step outputs, never as uploads: web (one self-contained HTML page: its url renders it in a sandbox, its download_url is the file) and archive (a zip, download only). tool-web-page makes both from a template baked into its template option — placeholders {{text}}, {{image}}, {{video}}, {{audio}}, {{document}} and {{video.mime}} and the like, nothing executable — so a pipeline can hand over a piece of a website, not just a clip.
- Keep pipelines lean — no steps the goal does not need — and price with get_pipeline_spec (total_cost_usd) before running.
Prefer not to wire by hand? POST /v1/pipelines/draft with {"brief": "..."} asks heddla's
builder to compose a pipeline from plain language. It answers 202 with a pipeline in status
drafting; GET /v1/pipelines/{id}?wait=60 holds until it is ready (or
draft_failed, with draft_error). Drafts may spend a test_budget on real
test generations; what they do not spend is refunded.
Making a pipeline together with a person, stage by stage? Keep the record as a build: POST /v1/builds
opens one, journal entries go to POST /v1/builds/{id}/entries, and POST /v1/builds/{id}/finalize
closes it with the finished pipeline's id. Over MCP the same steps are start_build,
log_build and finalize_build, and the
build_pipeline prompt carries the contract.
Values and files
Every input and output is a typed value. Text travels inline; binary data never does:
{"type": "text", "text": "hello"}
{"type": "image", "file_id": "<id from POST /v1/files>"}
Upload first (images, audio, video, PDF and DOCX; 25 MB, or 200 MB for video), then use the returned id:
curl -X POST https://api.heddla.com/v1/files \
-H "Authorization: Bearer heddla_your_key_here" \
-F "file=@photo.jpg"
For large files, or from a machine that should not stream through the API: POST /v1/files/uploads
with the file's name and size answers with a file id and a presigned PUT url, the bytes go straight to storage, and
POST /v1/files/{id}/finish makes the file ready.
Uploads stay in the account's library until the owner removes them, next to what its runs made; the same bytes
uploaded twice are kept once, and the second upload answers with the first file (existing: true), which then lists first.
GET /v1/files lists the library to pick from, GET /v1/files/usage says how much room it takes.
To send part of a clip to a step that takes a clip by length (an option marked clip_length), cut it
first: POST /v1/files/{id}/cut with {"parts": [{"start": 12.5, "end": 20}]} cuts the parts,
joined in the order they play, into a new video kept like an upload (cut_from names the clip and the
parts; the same parts again answer with the same file). It answers 202 with the cut's id while it cuts,
?wait= holds until it is made, and GET /v1/files/{id}?wait= on the cut's id keeps waiting. Send the cut as the clip and set the option to the parts' total, rounded
up to its step: the cut never runs longer, so the step takes it as it is.
A voice worth keeping: POST /v1/voices with {"name": "Anna", "file_ids": ["…"], "consent": true}
clones audio files from the library and keeps the voice on the account. consent is the person's own answer to the words
GET /v1/voices gives as consent_text, recorded with the time. Each voice has a value:
send it as the answer to any option marked saved_voices. A voice works only on the account that saved it, so a
recipe cannot fix one, and a person running a recipe answers with their own. DELETE /v1/voices/{id} removes it at
any time; its recordings stay in the library until they are removed too.
Reference
Everything a key can reach. Pagination is ?limit=&offset=; ids are UUIDs.
| route | purpose |
|---|---|
| GET /v1/users/me | The account behind the key. |
| GET /v1/balance | Balance in dollars, the top-up link and the offer behind it. |
| GET /v1/ledger | Balance and the last 100 movements. |
| GET /v1/connect | How to connect, for this deployment: MCP address, commands, config snippets, skill link (public). |
| GET /v1/models | The catalog; filters and view=summary as above. |
| GET /v1/models/{id} | One model with its full option schema. |
| POST /v1/pipelines | Create; body as in Pipeline shape. |
| GET /v1/pipelines | List (newest first); view=summary omits steps. |
| GET /v1/pipelines/{id} | Read; ?wait= while drafting. |
| PUT /v1/pipelines/{id} | Full replace, same validation as create. |
| DELETE /v1/pipelines/{id} | Delete; past runs stay readable. |
| POST /v1/pipelines/bulk-delete | Delete up to 100 at once: {ids} → {results: [{id, ok, error}]}. |
| POST /v1/pipelines/draft | Builder draft from a brief (202, then poll). |
| GET /v1/pipelines/{id}/spec | The run contract; documented per pipeline at /docs/pipelines/{id}. |
| POST /v1/pipelines/{id}/executions | Start a run (202). |
| GET /v1/executions | Runs, newest first; ?pipeline_id= to narrow. |
| GET /v1/executions/{id} | Full state with results; ?wait= until settled. |
| GET /v1/executions/{id}/events | SSE stream of full snapshots. |
| POST /v1/executions/{id}/cancel | Stop a run; unrun steps are refunded. |
| POST /v1/executions/{id}/resume | Continue a stopped or failed run as a fresh execution seeded with the finished steps; only the rest is priced. |
| DELETE /v1/executions/{id} | Remove a finished run and the files nothing else references. |
| POST /v1/executions/bulk-deletePOST /v1/executions/bulk-cancel | Remove finished runs, or stop live ones, up to 100 at once: {ids} → {results: [{id, ok, error}]}. |
| POST /v1/filesGET /v1/files/{id} | Upload (the same bytes again answer with the file already kept); one file with fresh links, what made it and what uses it. |
| POST /v1/files/uploadsPOST /v1/files/{id}/finish | Direct upload: a presigned PUT url, then finish. |
| GET /v1/files | The library, most recently added first: uploads and what runs made; ?kind=&source=upload|run&role=result|intermediate|any&limit=&cursor=. |
| GET /v1/files/usage | How much room the library takes, by kind and source, against the allowance (not enforced). |
| POST /v1/files/{id}/cut | Cut parts of a video, joined in order, into a new video in the library: {parts: [{start, end}]} in seconds → the file with cut_from; 202 while it cuts, ?wait= until it is made. |
| DELETE /v1/files/{id}POST /v1/files/bulk-delete | Remove files, up to 100 at once; 409 with in_use while a pipeline, a recipe (including what went into its examples), a running run or the profile photo needs one. |
| POST /v1/buildsGET /v1/builds | Open a build with {name, brief}; list builds, open ones first. |
| GET /v1/builds/{id}POST /v1/builds/{id}/entries | A build with its journal; append an entry. |
| POST /v1/builds/{id}/finalizePOST /v1/builds/{id}/abandonDELETE /v1/builds/{id} | Close a build with its pipeline, close it without one, or remove it. |
| GET /v1/publications | Published recipes as cards; ?query=&sort=stars|runs&filter=starred. |
| GET /v1/publications/{id}/spec/runs | One app, its run contract (also at /docs/apps/{id}), your runs of it. |
| POST /v1/publications/{id}/execute | Run an app on your balance (202). |
| POST /mcp | The MCP server (JSON-RPC over Streamable HTTP). |
| /.well-known/oauth-authorization-server | OAuth 2.1 discovery; /oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke behind it. |
Limits and errors
Key-authed requests are budgeted per key at 120 per minute with a burst of 60; a run that exceeds the account's
balance is refused with 402 before anything starts. Error bodies are
{"error": "…", "details": "…"} (a 402 adds topup_url).
| status | meaning |
|---|---|
| 401 | Missing, invalid, or revoked key or connection. |
| 402 | The balance does not cover this run or draft; topup_url says where to add funds. |
| 404 | Unknown id, or not yours. |
| 409 | The pipeline is still drafting; wait before editing. |
| 422 | The pipeline or run request does not validate; error says what is off. |
| 429 | Rate limit hit; wait for Retry-After seconds. |