Martini Workflows API

Run a saved workflow over HTTPS with your own material, poll the run, and collect the output files.

Set this up with a coding agent

Paste this into Claude, Cursor, or another coding agent working in your backend. It covers credentials, the calls, polling, the budget rule, error codes, and an acceptance checklist. Pick TypeScript or Python for the SDK guide, or Any language for the bare HTTP contract.

Overview

The Workflows API executes reusable processes you have saved in Martini. The API starts runs with your material, reports their progress, and hands back the output files. It does not author or edit workflows; do that on a canvas in Martini, or through the MCP connector.

Runs started over the API auto-approve. Once a run is submitted, nobody has to click anything in the app.

The same API key serves the Generation API. Every workflow route lives under https://api.martini.film/v1.

Workflows and runs

A workflow is a reusable production task saved in your workspace: its steps, the variables it reads, and the bins it expects material in. A run is one complete execution—like rolling the camera. Every run starts from a fresh copy of the workflow with the material you pass; running it again with the same material can produce a new variation without requiring any change to the workflow.

One workflow can have many runs. Each run has its own inputs, canvas, progress, spend, and outputs.

More examples

WorkflowRuns
Create establishing shotHarbor at dawn A; Harbor at dawn B; Mountain lodge in heavy rain
Animate storyboard frameKitchen reveal, slow push; Rooftop escape, handheld
Create prop conceptHero sword, weathered A; Hero sword, weathered B; Evidence recorder, damaged
Storyboard a scene beatSophie finds the letter, classic coverage; Gabriel enters the bar, single take
In API terms: workflowId chooses the workflow, the request body carries the material (bins) and text (variables), and the response's id identifies that roll. Use the run id to poll progress and fetch results. Runs land in your workspace's Workflow runs project in Martini, one canvas each; openInMartini on the run links straight to it.

Quick start

To try the calls without writing code, open the playground: paste a key, pick a workflow, start a run, and read the curl for each request.

1. Create an API key

An organization admin creates keys under Settings → Martini API for a personal workspace, or the team's Martini API tab. The full key is shown once. Store it in a server-side secret such as MARTINI_API_KEY; never expose it in browser code or a NEXT_PUBLIC_* variable.

2. Find the workflow and upload your material

On the Workflows page, open the workflow and use the Connect dialog to copy the curl below with its id and input names filled in. Upload the files its input bins expect with the asset endpoints (POST /assets/uploads/…, or assets.upload() in the client); they land in your API Generations project and their assetIds are what a run takes.

3. Check the connection

curl https://api.martini.film/v1/me \ -H "Authorization: Bearer $MARTINI_API_KEY"
{ "organization": { "id": "…", "name": "Acme Films" }, "key": { "label": "Production" }, "apiVersion": "2026-08-13" }

4. Start a run

curl -X POST https://api.martini.film/v1/workflows/$WORKFLOW_ID/runs \ -H "Authorization: Bearer $MARTINI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "bins": { "References": ["$ASSET_ID"] }, "variables": { "Script": "INT. LAB - NIGHT." } }'

The response is HTTP 201 with the run object. Pass asset ids for every input bin the workflow lists (keyed by bin name or id) and, optionally, values for its variables.

5. Poll, then fetch the results

curl https://api.martini.film/v1/runs/$RUN_ID \ -H "Authorization: Bearer $MARTINI_API_KEY"
curl https://api.martini.film/v1/runs/$RUN_ID/results \ -H "Authorization: Bearer $MARTINI_API_KEY"

Poll status until it is completed, failed, or cancelled. Results answers at any time with the takes that have settled so far, so a dashboard can show outputs as they land; status in its body says whether more may come.

Or with a client

npm install --save @martini-film/client
import { createMartiniClient } from '@martini-film/client' const martini = createMartiniClient({ apiKey: process.env.MARTINI_API_KEY }) const reference = await martini.assets.upload(file, { wait: true }) const { run, results } = await martini.workflows.subscribe(workflowId, { bins: { References: [reference.assetId] }, variables: { Script: 'INT. LAB - NIGHT.' }, onUpdate: run => console.log(run.status), }) for (const output of results.outputs) { if (output.status === 'ready') console.log(output.filename, output.url) }

Authentication

Send the key as Authorization: Bearer <key>. The Generation API's Authorization: Key <key> form is accepted too, so one client can serve both. Requests belong to the key's organization; anything outside it reads as 404, never 403, so ids from another workspace cannot be probed.

An optional X-Martini-API-Version: 2026-08-13 header pins the response envelope. The served version is echoed back on every response. Runs act as the key's creator: for a placed workflow, or a projectId you name, that user must be able to edit the project, and the project's organization must have workflows enabled.

Endpoints

GET /v1/me

Connection check. Returns the organization and key label the request authenticated as, plus the API version in effect.

GET /v1/projects

The organization's projects the key's user can see — the same set the MCP connector's get_projects shows them — most recently joined first. Use it to pick the projectId a run, a generation, or an upload should land in. A project the user can only view is listed with canEdit: false rather than hidden; naming it for a run answers PROJECT_WRITE_FORBIDDEN.

ParameterTypeNotes
querystringOptional. A project name or id, or a fragment of either (1–200 characters). Matches are ranked: an exact id or name first, then a name that starts with, contains, or holds every word of the query, then an id fragment.
exactNamebooleanOptional, with query. true keeps only projects whose name matches it exactly (case-insensitive), or whose id is it.
limitintegerOptional page size, 1–50 (default 20). totalMatched counts every match; truncated says whether more matched than were returned.
curl "https://api.martini.film/v1/projects?query=Pilot" \ -H "Authorization: Bearer $MARTINI_API_KEY"
{ "projects": [ { "id": "44444444-4444-4444-8444-444444444444", "name": "Pilot", "createdAt": "2026-08-30T09:00:00.000Z", "visibility": "private", "canEdit": true, "openInMartini": "https://www.martini.film/p/44444444-4444-4444-8444-444444444444", "links": { "canvases": "/v1/projects/44444444-4444-4444-8444-444444444444/canvases" } }, { "id": "66666666-6666-4666-8666-666666666666", "name": "Dailies", "createdAt": "2026-08-12T15:20:00.000Z", "visibility": "link_view", "canEdit": false, "openInMartini": "https://www.martini.film/p/66666666-6666-4666-8666-666666666666", "links": { "canvases": "/v1/projects/66666666-6666-4666-8666-666666666666/canvases" } } ], "totalMatched": 2, "limit": 20, "truncated": false }

visibility is what a share link grants someone who is not a member: private, link_view, or link_edit. openInMartini opens the project on its default canvas. A parameter out of range answers 400 INVALID_REQUEST.

GET /v1/projects/{projectId}/canvases

A project's canvases in the order the app lists them. The first carries isDefault: true: it is where the project opens, and where a generation into the project lands when no canvasId is given. The project must be the organization's and readable by the key's user; anything else answers 404 PROJECT_NOT_FOUND, so a canvas name never crosses a project boundary.

curl https://api.martini.film/v1/projects/$PROJECT_ID/canvases \ -H "Authorization: Bearer $MARTINI_API_KEY"
{ "project": { "id": "44444444-4444-4444-8444-444444444444", "name": "Pilot" }, "canvases": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Episode 1", "description": null, "createdAt": "2026-08-30T09:00:00.000Z", "isDefault": true, "openInMartini": "https://www.martini.film/p/44444444-4444-4444-8444-444444444444/episode-1-1z6h" }, { "id": "77777777-7777-4777-8777-777777777777", "name": "Storyboard", "description": "Beats for act one", "createdAt": "2026-09-01T11:30:00.000Z", "isDefault": false, "openInMartini": "https://www.martini.film/p/44444444-4444-4444-8444-444444444444/storyboard-2k9p" } ] }

GET /v1/workflows

The saved workflows in the key's workspace: steps, inputs (variables with their saved default values, and the input bins a run must fill), outputs, and the fingerprint of the machinery. Add ?all=true to append the placed workflows the key's user can run in place, each with its project, canvas, and the canvas state as it stands.

curl https://api.martini.film/v1/workflows \ -H "Authorization: Bearer $MARTINI_API_KEY"
{ "workflows": [ { "id": "b6723905-40d0-42e5-a136-f6f1f592489b", "kind": "saved", "name": "Script to Video", "description": "Storyboard a script, then shoot each panel.", "version": 3, "fingerprint": "9f2c1c0e8b7a…", "updatedAt": "2026-09-01T09:12:00.000Z", "steps": [{ "name": "Storyboard", "agent": "martini" }, { "name": "Shoot", "agent": "martini" }], "inputs": { "variables": [{ "id": "a1…", "name": "Script", "value": "INT. LAB - NIGHT. …" }], "bins": [{ "id": "b1…", "name": "References" }] }, "outputs": [{ "kind": "bin", "name": "Storyboard frames" }, { "kind": "bin", "name": "Shots" }], "links": { "runs": "/v1/workflows/b6723905-40d0-42e5-a136-f6f1f592489b/runs" } } ] }

A placed workflow in the same listing (with ?all=true):

{ "id": "88888888-8888-4888-8888-888888888888", "kind": "placed", "name": "Script to Video", "savedWorkflow": { "id": "b6723905-40d0-42e5-a136-f6f1f592489b", "version": 3 }, "fingerprint": "9f2c1c0e8b7a…", "project": { "id": "44444444-…", "name": "Pilot" }, "canvas": { "id": "55555555-…", "name": "Episode 1" }, "createdAt": "2026-09-02T08:00:00.000Z", "inputs": { "variables": [{ "id": "c1…", "name": "Script", "value": "<current canvas value>" }], "bins": [{ "id": "d1…", "name": "References", "assetCount": 3 }] }, "links": { "runs": "/v1/workflows/88888888-8888-4888-8888-888888888888/runs" } }

GET /v1/workflows/{workflowId}

One workflow of either kind in the same shape as the listing, or 404 WORKFLOW_NOT_FOUND when it is not in the key's workspace, is a copy stamped for a run, or lives in a project the key's user cannot edit.

curl https://api.martini.film/v1/workflows/$WORKFLOW_ID \ -H "Authorization: Bearer $MARTINI_API_KEY"

POST /v1/workflows/{workflowId}/runs

Start a run. For a saved workflow, Martini stamps a fresh copy onto its own canvas in your workspace's Workflow runs project (or in the projectId you pass), fills its input bins with the assets you pass, and runs it. For a placed workflow, the run happens in place and reads the canvas bins you do not pin.

FieldTypeNotes
binsobjectAsset ids per input bin, keyed by bin name (case-insensitive) or id. Required for every input bin of a saved workflow. See Inputs.
inputsobjectJSON documents for the input bins that declare a fileInput, keyed by its key: { json: document } sends the document, { fileId } reuses a JSON File in the project, an array supplies several. See Inputs.
variablesobjectOptional. Values for the variables the workflow reads, keyed by variable name (case-insensitive) or id. See Inputs.
fingerprintstringOptional. The fingerprint you last read for this workflow; a mismatch answers 409 WORKFLOW_CHANGED. See Fingerprint.
oliveBudgetnumberOptional cap on generation spend for the run, in olives. See Budget.
idempotencyKeystringOptional. Re-posting the same key returns the run the first call created instead of starting another (and stamps no second copy).
projectIduuidSaved workflows only. Optional. Run in this project of your workspace, on a new canvas, so the outputs land where your team works. A run always gets a canvas of its own.

Responds 201 with the run object and links.status / links.results. The run starts immediately.

GET /v1/runs/{runId}

The run object, with per-Action and per-item progress and the live phase line. Add ?include=activity to get each Action's events (its activity trail) as well; leave it off for a plain poll.

{ "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "savedWorkflow": { "id": "b6723905-40d0-42e5-a136-f6f1f592489b", "version": 3 }, "fingerprint": "9f2c1c0e8b7a…", "status": "running", "phase": "unit 2/4 · waiting on generations", "origin": "api", "createdAt": "2026-09-02T12:00:00.000Z", "startedAt": "2026-09-02T12:00:02.000Z", "completedAt": null, "error": null, "variables": { "Script": "INT. LAB - NIGHT. Sophie reads the results." }, "bins": { "References": ["e7…", "e8…"] }, "olives": { "budget": 50, "budgetSource": "auto", "generation": 12.5, "agent": 0.4 }, "actions": [ { "id": "c1…", "name": "Storyboard", "status": "completed", "outcome": "generated", "error": null, "startedAt": "2026-09-02T12:00:02.000Z", "completedAt": "2026-09-02T12:01:40.000Z", "plan": { "units": 2, "outputsPerUnit": 1, "estimatedOlives": 8, "summary": "One frame per scene beat" }, "olives": { "generation": 7.5, "agent": 0.2 } }, { "id": "c2…", "name": "Shoot", "status": "running", "outcome": null, "error": null, "startedAt": "2026-09-02T12:01:41.000Z", "completedAt": null, "plan": { "units": 4, "outputsPerUnit": 1, "estimatedOlives": 40, "summary": "One shot per storyboard frame" }, "olives": { "generation": 5, "agent": 0.2 } } ], "items": [ { "id": "i1…", "actionId": "c1…", "status": "completed", "outcome": "generated", "outputAssetId": "e1…", "error": null, "revision": null, "createdAt": "2026-09-02T12:01:38.000Z", "completedAt": "2026-09-02T12:01:38.000Z", "verdict": null, "replacesItemId": null }, { "id": "i2…", "actionId": "c1…", "status": "needs_revision", "outcome": "needs_revision", "outputAssetId": null, "error": "Stopped before generating: the inputs need revision. See the reason for what to change.", "revision": { "reason": "The script runs about 95 seconds read aloud; the brief caps the dub at 30. Cut it to roughly a third, or raise the cap.", "inputRefs": ["Script"] }, "createdAt": "2026-09-02T12:01:39.000Z", "completedAt": null, "verdict": null, "replacesItemId": null }, { "id": "i3…", "actionId": "c2…", "status": "completed", "outcome": "generated", "outputAssetId": "e9…", "error": null, "revision": null, "createdAt": "2026-09-02T12:04:10.000Z", "completedAt": "2026-09-02T12:04:10.000Z", "verdict": { "pass": true, "reason": null, "take": 1 }, "replacesItemId": null } ], "openInMartini": "https://www.martini.film/project/…/canvas/script-to-video-2026-09-04-…", "links": { "status": "/v1/runs/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "results": "/v1/runs/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/results" } }

GET /v1/runs/{runId}/status

Compact live activity: the last activity time, executor, model steps, output counts, and olives. Plans, inputs, and traces are omitted. Progress describes active work only while the run is running. Accepted counts takes that passed review; planned counts cover plans produced so far, not every shot required by a brief. Older runs may have no activity record.

GET /v1/runs/{runId}/results

Answers at any time with the takes that have settled so far; status says whether more may come. Call it when a status poll shows a new completed item, or once the run settles, rather than on every poll: every call presigns every output.

{ "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "status": "completed", "error": null, "outputs": [ { "itemId": "i1…", "actionId": "c1…", "assetId": "e1…", "status": "ready", "filename": "panel-01.png", "mimeType": "image/png", "url": "https://…/panel-01.png?…", "expiresIn": 3600 }, { "itemId": "i3…", "actionId": "c2…", "assetId": "e9…", "status": "ready", "filename": "shot-01.mp4", "mimeType": "video/mp4", "url": "https://…/shot-01.mp4?…", "expiresIn": 3600 } ] }

Only items that completed with an output appear; group them by actionId to read one step's outputs. Each url is a presigned download valid for expiresIn seconds; fetch it promptly rather than storing it. A failed run returns its partial outputs with the failure copy in error. An output whose file cannot be resolved comes back with status: "not_ready" or "missing" and an error instead of failing the whole response. Files from an organization on a free plan carry the same watermark they would through the app.

POST /v1/runs/{runId}/resume

Retry the stopped Action of a failed or needs_revision run. Finished Actions and finished units keep their outputs, and nothing already spent is charged again. Answers 200 with the run, pending again; poll it as before. Any other status answers 409 WORKFLOW_RUN_NOT_RESUMABLE. A needs_revision run re-reads the canvas inputs it names, so a fix on the canvas lands on resume; inputs pinned on the run (variables, bins) do not change, so such a run resumed unchanged stops the same way — change them and start a new run instead.

POST /v1/workflows

The advanced path: place a copy of a saved workflow on a canvas of your choosing, so a team can prepare material there and run it in place later. { "from": "<savedId>", "projectId": "…", "canvasId"?: "…", "name"?: "…" } answers 201 with the new placed workflow in the listing shape. Most integrations never need this; a run of a saved workflow already gets a copy of its own.

The run object

FieldMeaning
idThe run id. Use it for status and results.
savedWorkflowThe saved workflow the run was started from and its version then; null for an unsaved placed workflow.
fingerprintContent identity of the machinery this run executed.
workflowId, project, canvasThe placed workflow that ran and where it lives. Present when the run executed a placed workflow you can see: a run of a placed workflow, or a run of a saved workflow started with projectId (its copy lands in that project). Absent for runs of saved workflows in the Workflow runs project, whose copies are hidden.
statuspending, running, completed, failed, or cancelled. API runs never wait for approval. A failed run can be resumed (POST /v1/runs/{runId}/resume).
phaseWhat Martini is doing right now, the same line the app shows ("unit 3/7 · waiting on generations"). Display copy: show it, do not parse it. Null unless the run is running.
originAlways "api" for runs started here. Runs from the Martini app, the assistant, or MCP carry their own.
createdAt, startedAt, completedAtISO timestamps; startedAt and completedAt are null until reached.
errorCopy written for a human when the run failed; otherwise null.
variablesThe values pinned for this run, keyed by variable name; empty when the canvas values were used.
binsThe asset ids pinned for each input bin, keyed by bin name; empty when the canvas bins were read.
olives.budgetThe spend rail in olives, or null before it is armed.
olives.budgetSource"explicit" when you set oliveBudget; "auto" when Martini armed it from the estimate.
olives.generation, olives.agentOlives spent so far on generation and on the agent.
actions[]One row per Action in run order: id, name, status, outcome, error, startedAt, completedAt, plan (units, outputsPerUnit, estimatedOlives, summary; null before the plan pass), and olives (generation summed from its takes, agent measured on the step).
actions[].eventsOnly with ?include=activity: the step’s activity trail, the feed the app’s inspector shows. Each event has elapsedMs, kind (plan, tool, unit, revision, settle, judge, sweep, note), and optionally tool, unit (1-based), take, durationMs, note, detail, outputAssetIds. A finished step’s events never change.
items[]One row per output take: id, actionId, status, outcome, outputAssetId, error, revision, createdAt, completedAt, verdict (the judge’s pass, reason, take; null without a judge), replacesItemId. A take appears once its outputs have settled. Status is pending, running, completed, failed, needs_revision (the agent stopped before generating because the inputs fail a precondition the brief sets; revision carries its reason), rejected (a judged take that did not pass), superseded, rerunning, or cancelled.
outcomeThe settled result beside status, on every action and item: null until settled, then "generated", "failed", or "needs_revision" — the same word as status, kept for callers that read it. needs_revision means the agent stopped before generating because the inputs fail a precondition the brief sets; the action and the run carry status "needs_revision" too. Change the inputs, then resume the run or start a new one.
items[].revisionSet when status is "needs_revision": the agent’s full reason and the inputRefs it named (ids or names from the run’s inputs; empty when none, and always empty when the agent only answered with the DECLINED: marker). null on every other item.
openInMartiniThe run’s canvas in the Martini app.
links.status, links.resultsPaths of the status and results endpoints for this run.

Inputs

A workflow's inputs are what its briefs reference: canvas variables (a script, a character description, a product name) and the bins it reads that nothing in the chain writes (reference frames, source clips). GET /v1/workflows lists them: a saved workflow from its definition (variables with their saved defaults, bins by name), a placed workflow with the canvas state as it stands.

bins carries the material: keys are bin names (case-insensitive) or ids, values are asset ids in the order the Action should read them. A run of a saved workflow starts from empty bins, so every input bin must be pinned (WORKFLOW_BIN_REQUIRED otherwise). Assets must be completed and in a project the key's user can read: your uploads (POST /assets/uploads/…, which land in the API Generations project), the projectId you passed, or an earlier run's output in Workflow runs. Each is copied into the run's canvas as a new record on the same storage objects. A placed workflow reads the canvas bins you do not pin; pinned assets must be in its project.

An input bin can declare a fileInput (shown on the bin in GET /v1/workflows: its key, the format json, and whether it takes one file or many). Fill it with inputs instead of bins: keys are the declared keys, values are { "json": document } to send the document itself (up to 10 MB, arbitrary JSON; embedded images travel as-is), { "fileId": "…" } to reuse a JSON File already in the project, or an array of those for a many-file input. Martini stores each document as an immutable revision, freezes it into the run, and hands the Action a local copy; the canvas and the run record hold only its descriptor. A saved workflow may carry default documents; a run that names none uses them, and a required input with no default answers INVALID_FILE_INPUT.

variables pins a value for any variable for this run only. Keys are variable names (case-insensitive, as shown on the variable card) or ids; values are strings and replace the whole value. Unpinned variables keep their saved (or canvas) value. Only inputs the workflow's Actions read are accepted; any other key answers 400 WORKFLOW_VARIABLE_NOT_FOUND or WORKFLOW_BIN_NOT_FOUND with the accepted inputs listed under inputs. The run echoes what it pinned under variables and bins on every status read. Limits: 20,000 characters per variable and 100,000 in total; 20 bins with 200 assets each.

Fingerprint

Every workflow and run carries a fingerprint: a hash of the machinery (each Action's brief, references, output bins, agent, pinned settings, and gate, plus the wired bins), never of names, bin contents, or variable values. Ids stay stable when a workflow is edited; the fingerprint changes. Pass the one you read to POST /v1/workflows/{workflowId}/runs and a changed workflow answers 409 WORKFLOW_CHANGED with the current fingerprint instead of running something else. Omit it to run whatever the workflow is now.

Budget

oliveBudget is optional. When omitted, the run records budgetSource: "auto" and Martini arms the spend rail at each Action's gate from that Action's estimate, raising it for a costlier later Action rather than failing. When supplied, a plan estimated over the budget fails the run with the estimate in error; raise the budget and run again. Both generation and agent spend are reported in olives as the run progresses.

Errors

Errors are JSON { "error": "<copy for a human>", "code": "<STABLE_CODE>" }. Branch on code; the copy may change.

HTTP statusCodeWhen
401INVALID_API_KEYMissing, malformed, or unknown key.
401API_KEY_REVOKEDThe key was revoked.
401API_KEY_EXPIREDThe key passed its expiry; CLI keys expire after 90 days — run martini login again.
403API_KEY_DEFAULTS_MISSINGThe key lost its default project. Contact support.
403GENERATION_API_DISABLEDThe organization no longer holds API access.
503GENERATION_API_ACCESS_UNAVAILABLEAccess could not be checked. Retry.
400UNSUPPORTED_API_VERSIONUnknown X-Martini-API-Version header.
400INVALID_REQUESTBody is not JSON, a field does not fit the workflow kind (projectId on a placed workflow), or a query parameter is out of range (GET /v1/projects).
404WORKFLOW_NOT_FOUNDNo such workflow in this organization, or a copy you cannot see.
404PROJECT_NOT_FOUNDprojectId is not a project of this organization, or the key owner cannot read it (GET /v1/projects/{projectId}/canvases).
409PROJECT_DOCUMENT_TOO_LARGEThe project document is too large to open, so its canvases cannot be listed. Durable: retrying will not help. Contact support.
503PROJECT_DOCUMENT_UNAVAILABLEThe project document could not be read while listing canvases. Retry.
403PROJECT_WRITE_FORBIDDENThe key owner cannot edit the project.
403WORKFLOWS_NOT_ENABLEDThe project organization lacks the workflow grant.
400WORKFLOW_VARIABLE_NOT_FOUNDA variables key names no variable the workflow reads; the body lists what it accepts under inputs.
400WORKFLOW_BIN_NOT_FOUNDA bins key names no input bin of the workflow; the body lists what it accepts under inputs.
400WORKFLOW_BIN_REQUIREDA saved workflow input bin was not pinned. A run starts from empty bins, so pass asset ids for every input bin.
400WORKFLOW_ASSET_NOT_FOUNDA bins value names an asset the key owner cannot read, or one that is still processing. Upload it first.
4xxINVALID_FILE_INPUTAn inputs entry is not valid JSON or exceeds 10 MB (400), names a JSON File the key owner cannot read (404), or reuses an idempotency key with different documents (409).
409WORKFLOW_CHANGEDThe workflow changed since the fingerprint you passed; the body carries the current one.
4xxWORKFLOW_RUN_REJECTEDThe run could not start, for example because the workflow has no Actions.
409WORKFLOW_RUN_NOT_RESUMABLEOnly a failed run can be resumed.
500WORKFLOW_RUN_FAILEDStarting or resuming the run failed on the Martini side. Retry.
4xxWORKFLOW_CREATE_REJECTEDPOST /v1/workflows was refused, for example because the canvas does not exist.
500WORKFLOW_CREATE_FAILEDPlacing the workflow failed on the Martini side. Retry.
404WORKFLOW_RUN_NOT_FOUNDUnknown run, or a run from another organization.

Using the client

@martini-film/client 0.10.0 and later (JavaScript, TypeScript) and martini-client 0.3.0 and later (Python) wrap every call. Martini-native routes reject with MartiniApiError, which carries status, the stable code, and the parsed body; the Python client raises a MartiniAPIError subclass per status (ConflictError, RateLimitError, …) with the same fields.

For any other language, the complete /v1 surface is described by an OpenAPI 3.1 document at api.martini.film/v1/openapi.json (no authentication) — point a client generator or a coding agent at it.

import { createMartiniClient, MartiniApiError } from '@martini-film/client' const martini = createMartiniClient({ apiKey: process.env.MARTINI_API_KEY }) const me = await martini.me() const { workflows } = await martini.workflows.list() const workflow = workflows.find(candidate => candidate.name === 'Script to Video') const reference = await martini.assets.upload(file, { wait: true }) const run = await martini.workflows.run(workflow.id, { bins: { References: [reference.assetId] }, variables: { Script: 'INT. LAB - NIGHT. Sophie reads the results.' }, fingerprint: workflow.fingerprint, idempotencyKey: 'job-42', }) const status = await martini.runs.status(run.id) console.log(status.phase, status.actions.map(action => [action.name, action.status])) // A step's activity trail, for a detail view. const detailed = await martini.runs.status(run.id, { activity: true }) // Outputs that have settled so far; call it when a poll shows a new completed item. const partial = await martini.runs.results(run.id) try { await martini.runs.status('not-a-run') } catch (error) { if (error instanceof MartiniApiError && error.code === 'WORKFLOW_RUN_NOT_FOUND') { // unknown run, or one from another organization } } const settled = await martini.runs.wait(run.id, { onUpdate: run => console.log(run.status, run.phase) }) // A failed run: retry its failed step with the same inputs, then wait again. if (settled.run.status === 'failed') { const resumed = await martini.runs.resume(run.id) await martini.runs.wait(resumed.id) } // Pick the project (and canvas) a run or a generation should land in. const { projects } = await martini.projects.list({ query: 'Pilot' }) const pilot = projects.find(project => project.canEdit) const { canvases } = await martini.projects.canvases(pilot.id) const episode = canvases.find(canvas => canvas.isDefault) // Advanced: placed workflows and placing a copy yourself. const { workflows: everything } = await martini.workflows.list({ all: true }) const placed = await martini.workflows.create({ from: workflow.id, projectId: pilot.id })

workflows.subscribe() and runs.wait() poll with jittered backoff from 2 seconds up to 15 seconds and give up after 30 minutes by default; pass pollInterval and timeoutMs (poll_interval and timeout in Python) to change that. Both return a failed or cancelled run rather than throwing; check status. Use Node.js 18 or later or Python 3.10 or later, and call Martini from trusted server code only.

Not yet available

  • Webhooks when a run settles. Poll the status endpoint for now.
  • Bins as an identity on the wire (what each bin holds so far on the run, the bin name on each result). Group takes by actionId for now.
  • Authoring or editing a workflow over this API. Build it on a canvas in Martini, or via MCP.
  • Notes and directly tagged assets as per-run inputs. Only variables and input bins can be pinned.
  • Deleting a run or its canvas. Runs stay in the Workflow runs project until you remove them in the app.

© 2026 C47 Inc.