Pipelines, models and runs, from code or an agent.
Everything the slai 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://slai-api.flybywire.se/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 slai 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://slai-api.flybywire.se/mcp, then Connect and sign in.
Claude Code
claude mcp add --transport http slai https://slai-api.flybywire.se/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 slai https://slai-api.flybywire.se/mcp \
--header "Authorization: Bearer slai_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": {
"slai": {
"type": "http",
"url": "https://slai-api.flybywire.se/mcp",
"headers": {"Authorization": "Bearer slai_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://slai-api.flybywire.se/docs/skill.md: the same rules and endpoints as a Markdown
skill that reads the key from SLAI_API_KEY. Save it as .claude/skills/slai/SKILL.md
(or wherever your agent keeps skills), put the key in the environment, and the agent knows how to use slai.
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. |
| 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 slai'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). |
| start_build | Open a build: the journal of a pipeline made stage by stage under the build contract (the build_pipeline prompt has the contract; follow it). Returns the build with its id. The name becomes the pipeline's name. |
| log_build | Append one entry to a build's journal. kind: frame (the restated brief), plan (the numbered stages), stage (one scratch pipeline built and run: give stage, and refs with pipeline_id, execution_id, inputs, outputs, cost_usd), review (the person's verdict: author person, stage, verdict approve|change|stop, their words in body), assemble (the final pipeline run end to end), note (anything learned). Log before you ask the next question; every id you mention goes in refs. |
| get_build | A build with its whole journal, file references carrying fresh urls. Read it to resume a build: continue from the last entry. |
| list_builds | The account's builds, open ones first. |
| 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. 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. |
| get_file | Metadata and a fresh download url (valid about an hour) for a file the account uploaded or a run produced. 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. |
| list_apps | Published apps on slai's Discover shelf — pipelines other users made public, 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 slai_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 to Discover. 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://slai-api.flybywire.se/v1/balance -H "Authorization: Bearer slai_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://slai-api.flybywire.se/v1/models?view=summary&output=image" \
-H "Authorization: Bearer slai_your_key_here"
Filters: provider, input, output (a type: text, image, audio, video,
document, web, archive), available=true. 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://slai-api.flybywire.se/v1/pipelines \
-H "Authorization: Bearer slai_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://slai-api.flybywire.se/v1/pipelines/PIPELINE_ID/executions \
-H "Authorization: Bearer slai_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://slai-api.flybywire.se/v1/executions/EXECUTION_ID?wait=60" \
-H "Authorization: Bearer slai_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-pro",
"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",
"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", "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. Only option keys the model lists exist; get_model shows them with choices, ranges and what each does to the price.
- input_meta gives run inputs a label, help text and (text inputs only) a fixed list of choices, keyed by input name.
- 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 slai'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://slai-api.flybywire.se/v1/files \
-H "Authorization: Bearer slai_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.
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/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/filesGET /v1/files/{id} | Upload; metadata with a fresh download url. |
| POST /v1/files/uploadsPOST /v1/files/{id}/finish | Direct upload: a presigned PUT url, then finish. |
| 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 | Discover apps; ?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. |