Public API

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:

toolwhat it does
whoamiThe account this connection belongs to: email, username and balanceUsd. get_balance adds the top-up link.
list_modelsThe 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_modelOne 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_pipelinesThe 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_pipelineOne 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_pipelineSave 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_pipelineReplace 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_pipelineDelete a pipeline. Its past runs stay readable under their execution ids.
draft_pipelineHave 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_specThe 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_pipelineStart 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_executionOne 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_executionsRecent runs, newest first, optionally for one pipeline: status, timing, step progress (no inputs or results — use get_execution).
start_buildOpen 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_buildAppend 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_buildA build with its whole journal, file references carrying fresh urls. Read it to resume a build: continue from the last entry.
list_buildsThe account's builds, open ones first.
finalize_buildClose 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_buildClose a build without a pipeline, when the person stops. The journal stays readable.
begin_uploadStart 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_uploadAfter 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_fileUpload 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_fileMetadata 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_appsPublished 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_appOne published app with its steps and inputs.
get_app_specThe 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_appRun 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_balanceThe 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.
ledgerThe 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.

routepurpose
GET /v1/users/meThe account behind the key.
GET /v1/balanceBalance in dollars, the top-up link and the offer behind it.
GET /v1/ledgerBalance and the last 100 movements.
GET /v1/connectHow to connect, for this deployment: MCP address, commands, config snippets, skill link (public).
GET /v1/modelsThe catalog; filters and view=summary as above.
GET /v1/models/{id}One model with its full option schema.
POST /v1/pipelinesCreate; body as in Pipeline shape.
GET /v1/pipelinesList (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/draftBuilder draft from a brief (202, then poll).
GET /v1/pipelines/{id}/specThe run contract; documented per pipeline at /docs/pipelines/{id}.
POST /v1/pipelines/{id}/executionsStart a run (202).
GET /v1/executionsRuns, newest first; ?pipeline_id= to narrow.
GET /v1/executions/{id}Full state with results; ?wait= until settled.
GET /v1/executions/{id}/eventsSSE stream of full snapshots.
POST /v1/executions/{id}/cancelStop a run; unrun steps are refunded.
POST /v1/executions/{id}/resumeContinue 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}/finishDirect upload: a presigned PUT url, then finish.
POST /v1/buildsGET /v1/buildsOpen a build with {name, brief}; list builds, open ones first.
GET /v1/builds/{id}POST /v1/builds/{id}/entriesA 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/publicationsDiscover apps; ?query=&sort=stars|runs&filter=starred.
GET /v1/publications/{id}/spec/runsOne app, its run contract (also at /docs/apps/{id}), your runs of it.
POST /v1/publications/{id}/executeRun an app on your balance (202).
POST /mcpThe MCP server (JSON-RPC over Streamable HTTP).
/.well-known/oauth-authorization-serverOAuth 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).

statusmeaning
401Missing, invalid, or revoked key or connection.
402The balance does not cover this run or draft; topup_url says where to add funds.
404Unknown id, or not yours.
409The pipeline is still drafting; wait before editing.
422The pipeline or run request does not validate; error says what is off.
429Rate limit hit; wait for Retry-After seconds.