Skip to content

REST API

The control plane is a FastAPI app served under /api on the same address as the web app; this page lists every route, who may call it, what it takes and what it returns.

The web app uses this API for everything that needs your provider keys or the server (launching runs, estimates, Hub search, deployments, the playground). Plain reads and writes of your own rows (pipelines, datasets, runs, logs) go straight to Supabase under row level security and are not part of this API; see Architecture.

Note

FastAPI also serves an interactive schema at /api/docs and the OpenAPI document at /api/openapi.json. Most routes take a free-form JSON object, so this page is the more precise description of the fields.

Conventions

Base URL. https://tuneline-studio.vercel.app for the hosted app, http://localhost:8000 for a local API (see Contributing). All routes below are relative to it.

Authentication. Routes use one of four levels:

Level How to call Used by
Public No header needed. Health, config, registry, catalogs, estimate, validation
Optional user Works without a header; with Authorization: Bearer <access token> the API also uses your saved Hugging Face token (private and gated models, higher Hub limits). Hub search and details, estimate
User Authorization: Bearer <access token> is required. Runs, deployments, playground, live prices, key tests
Server x-tuneline-server-key: <TUNELINE_SERVER_KEY>, or Authorization: Bearer <CRON_SECRET>. The reconcile job

The access token is the Supabase session token of a signed-in user (session.access_token in supabase-js). The API checks it with Supabase's GET /auth/v1/user and caches the answer for 60 seconds.

Errors. Every failure is JSON with an HTTP status and a plain-English detail that says what went wrong and how to fix it:

{"detail": "Sign in first: this action needs your Tuneline account."}

Some errors add keys:

Key When
issues The pipeline is not valid (list of issues, same shape as POST /api/pipelines/validate).
missing_secrets API keys are missing: [{name, label, reasons}] (reasons only on runs).
run The run was created but its launch failed; the run is marked failed ({id, number, status}).
deployment The deployment row was created but the provider refused it; the row is marked failed.
hf_status The Hugging Face status code behind a Hub error.
errors Request validation failed (HTTP 422): [{loc, msg, type}].

Common statuses: 400 bad input or missing keys, 401 not signed in or session expired, 403 wrong server key or Hub access refused, 404 not found or not yours, 409 the object is in the wrong state (for example a run that already finished), 422 malformed request, 500 server misconfiguration, 502/503/504 a provider or Supabase failed or timed out. Unknown paths under /api return 404 with "No API endpoint at /api/<path>.". Secret values never appear in responses or logs.

CORS. Browsers may call the API from the app's own address, localhost / 127.0.0.1 on any port, and the origins in TUNELINE_CORS_ORIGINS. Allowed methods are GET, POST and OPTIONS; allowed headers are Authorization, Content-Type and x-tuneline-server-key.

Route summary

Method and path Auth Purpose
GET /api/health Public Is the deployment configured?
GET /api/config Public Connection settings for the web app
GET /api/registry Public The block registry
GET /api/templates Public The pipeline templates
GET /api/catalog/models Public Curated model catalog
GET /api/catalog/hardware Public GPUs, providers, prices, free options
POST /api/estimate Optional user Memory, GPU fit, time and cost
POST /api/pipelines/validate Public Check a graph
GET /api/prices/live User Live GPU prices
POST /api/secrets/test User Test a saved API key
GET /api/hf/models Optional user Search Hub models
GET /api/hf/model/{repo_id} Optional user One Hub model in detail
GET /api/hf/dataset-preview Optional user Rows of a Hub dataset
POST /api/runs User Create and launch a run
POST /api/runs/{run_id}/cancel User Cancel a run
POST /api/runs/{run_id}/retry User Start a new run with the same graph and target
GET /api/runs/{run_id}/notebook User Notebook that runs a queued run
GET /api/runs/{run_id}/command User One-line commands that run a queued run
POST /api/deployments User Create an endpoint
POST /api/deployments/{id}/{action} User Stop, resume, delete or refresh an endpoint
GET /api/playground/models Public Chat models Inference Providers serve now
POST /api/playground/chat User Chat with an endpoint or a Hub model
POST /api/cron/reconcile Server Background status sync
GET /api/cron/reconcile Server Same, for Vercel Cron
GET /runner/install.sh, install.ps1, wheel Public Runner installers and package

Meta

GET /api/health

Public. Reports whether this deployment is configured. The first call of each server process also registers the app's address with the database so the 5-minute background job can reach it.

Field Meaning
ok Always true when the API answers.
version Runner and API version.
runner_wheel_url Where jobs and installers download the runner.
server_key_configured TUNELINE_SERVER_KEY is set. Without it nothing that writes runs works.
background_checks The database knows the app's address and can call the reconcile job.
curl -s https://tuneline-studio.vercel.app/api/health
{"ok": true, "version": "0.1.0",
 "runner_wheel_url": "https://tuneline-studio.vercel.app/runner/tuneline-0.1.0-py3-none-any.whl",
 "server_key_configured": true, "background_checks": true}

GET /api/config

Public. The connection settings the web app and the installers need: {supabase_url, supabase_key, runner_supabase_key, app_url, runner_version, runner_wheel_url}. supabase_key is the public key for the browser; runner_supabase_key is the public anon key the runner and workers call the database with (the installers pass it on). Both are safe to share: every table is protected by row level security. Like /api/health, it registers the app's address on first use.

GET /api/registry

Public. The block registry as JSON (the same content as web/src/generated/registry.json): version, artifact_types, dataset_types, categories, secret_names and nodes (one entry per block with type, version, label, category, description, runtime, inputs, outputs, params, cacheable, presets, secrets, help). The Block reference is generated from it.

GET /api/templates

Public. The list of pipeline templates: [{id, name, description, tags, graph}], where graph is {nodes, edges} as the canvas stores it. See Templates.

curl -s https://tuneline-studio.vercel.app/api/templates
[{"id": "sanctions-triage", "name": "Sanctions alert triage",
  "description": "Fine-tunes a small model to decide ESCALATE or CLOSE on screening alerts, gates it on label accuracy and publishes it. Ships with a synthetic sample of 400 alerts.",
  "tags": ["text", "classification", "banking"],
  "graph": {"nodes": [{"id": "load", "type": "data.load", "position": {"x": 40, "y": 40},
                       "params": {"source": "url", "url": "{app_url}/samples/sanctions-alerts.jsonl"}}, "…"],
            "edges": ["…"]}},
 "…"]

GET /api/catalog/models

Public. The curated model catalog: {checked_at, source, schema_version, notes, families, models}. Each model has id, family, params_b, context, context_label, modality, model_type, license, license_note, commercial_use, gated, unsloth, base_model, released, too_large_for_single_node and notes.

GET /api/catalog/hardware

Public. The hardware catalog used by the estimator: {gpus, providers, prices, free_options, checked_at, notes}.

POST /api/estimate

Optional user. Memory breakdown, which GPUs fit, and time and cost per provider option. Send either estimator inputs or a pipeline graph.

Estimator inputs (all optional):

Field Meaning Default
model Hugging Face model id. The API reads the catalog, or the model's config.json and size from the Hub. —
params_b Parameter count in billions, when there is no model. —
active_params_b Active parameters for mixture-of-experts models. —
config, dims A model config.json or explicit dimensions instead of a Hub lookup. —
revision Hub revision to read the config from. main
method qlora, lora or full. qlora
lora_r, target_modules Adapter rank and layers. 16, all-linear
seq_len (or max_seq_length) Maximum sequence length. 2048
batch_size (or per_device_train_batch_size), grad_accum (or gradient_accumulation_steps) Batch settings. 1, 1
gradient_checkpointing On or off. true
optimizer (or optim), precision, attention Training options. auto, auto, sdpa
use_unsloth, use_liger_kernel Speed-ups. false
dataset_rows (or rows), avg_tokens, total_tokens Dataset size. —
epochs, max_steps Training length. 1, -1
mfu Model FLOPs utilisation assumed for time estimates, 0.01 to 1. 0.35
providers Limit the options to these providers. all
include_unfit Also list GPUs the model does not fit on. true
serving {context, concurrency, quant} for the serving memory estimate. min(seq_len, 8192), 1, bf16

Response: {model, dims, memory, gpu_fit, tokens, flops, options, serving, assumptions, warnings, checked_at}. memory holds total_gb, recommended_gb (15% headroom) and breakdown_gb (weights, adapter, gradients, optimizer, activations, logits, overhead). gpu_fit lists every catalog GPU with fits and min_gpus. options ranks provider × GPU choices with price_hr, est_hours, est_cost_usd, free, covered_by_credit and launch_target. assumptions explains the method in plain words.

curl -s -X POST https://tuneline-studio.vercel.app/api/estimate \
  -H "Content-Type: application/json" \
  -d '{"model": "Qwen/Qwen3-4B-Instruct-2507", "method": "qlora", "seq_len": 2048,
       "dataset_rows": 5000, "avg_tokens": 400, "epochs": 2}'
{"model": "Qwen/Qwen3-4B-Instruct-2507",
 "dims": {"params_b": 4.0, "hidden_size": 2560, "num_layers": 36, "source": "config", "…": "…"},
 "memory": {"method": "qlora", "optimizer": "paged_adamw_8bit", "trainable_params_m": 33.03,
            "breakdown_gb": {"weights": 2.656, "adapter": 0.132, "gradients": 0.132, "optimizer": 0.066,
                             "activations": 0.556, "logits": 1.245, "overhead": 1.0},
            "total_gb": 5.79, "recommended_gb": 6.65, "…": "…"},
 "gpu_fit": [{"gpu": "T4", "name": "NVIDIA T4", "vram_gb": 16, "fits": true, "min_gpus": 1,
              "headroom_gb": 9.35, "bf16": false, "notes": ["No BF16: trains in FP16."]}, "…"],
 "tokens": 4000000, "flops": 8.32e+16,
 "options": [{"provider": "modal", "provider_name": "Modal", "gpu": "L4", "gpu_count": 1, "price_hr": 0.8,
              "fits": true, "est_hours": 0.85, "est_cost_usd": 0.68, "free": false, "covered_by_credit": true,
              "launch_target": "modal", "notes": ["Covered by Modal's $30 monthly credit."], "rank": 2, "…": "…"},
             "…"],
 "serving": {"quant": "bf16", "weights_gb": 8.0, "total_gb": 10.96, "size_class": "16GB", "…": "…"},
 "assumptions": ["QLoRA keeps linear layers in 4-bit NF4 (0.52 bytes per parameter) and embeddings in bf16.", "…"],
 "warnings": [], "checked_at": "2026-09-25"}

(Trimmed; a real response lists 25 GPUs and about 100 options.)

Graph mode: send {graph, target?, target_config?, dataset_rows?, avg_tokens?, total_tokens?, mfu?, providers?}. The API finds the training step, its base model and dataset, and returns the full estimate plus trains: true, node, model and method. With a target it adds summary for that target: gpu, gpu_count, suggested_gpu, fits, memory_gb, recommended_gb, breakdown_gb, est_hours, est_cost_usd, price_hr, warnings. A graph without a training step returns {"trains": false, "detail": "This pipeline has no training step, so there is no training estimate.", "gpu_needed": …}.

Errors: 400 for invalid inputs (for example "MFU must be between 0.01 and 1.") or a malformed graph; Hub errors as in Hub.

POST /api/pipelines/validate

Public. Checks a graph without running it. Body: {"graph": {"nodes": [...], "edges": [...]}} (at most 500 nodes).

Field Meaning
ok true when there are no errors (warnings are allowed).
issues [{level, code, message, node, port, param}], level is error or warning.
order Run order of the node ids (empty when there are errors).
required_secrets Every key name the pipeline may use.
mandatory_secrets Keys it cannot run without: [{name, label, reasons}].
gpu_needed Whether any block needs a GPU.
{"ok": false,
 "issues": [{"level": "error", "code": "type_mismatch",
             "message": "sft.train expects ChatDataset, but load.data gives Dataset. Add a 'Format as chat' node between them.",
             "node": "sft", "port": "train", "param": null},
            {"level": "error", "code": "missing_input", "message": "sft: input 'Base model' is not connected",
             "node": "sft", "port": "model", "param": null}],
 "order": [], "required_secrets": [], "mandatory_secrets": [], "gpu_needed": true}

GET /api/prices/live

User. Live GPU prices from one provider. Query: provider = runpod (uses your saved RunPod key), vast or hf (also accepts huggingface).

Response: {provider, prices, count, live: true, fetched_at}. Each price has provider, gpu, gpu_name, gpu_count, price_hr, price_per_gpu_hr, product, source_url, checked_at, serving_only and provider-specific fields such as cloud and tier.

Errors: 400 for another provider or when the RunPod key is not saved.

POST /api/secrets/test

User. Checks one of your saved keys against its provider. Body: {"name": "hf_token"}. Any key name of a provider tests that provider's keys together: hf (hf_token), modal (modal_token_id + modal_token_secret), runpod, kaggle (API token or legacy username + key), openai_compat (key + base URL), wandb.

Response (HTTP 200 whether or not the key works): {ok, detail, provider}, plus account where the provider reports one. The Hugging Face test also returns hf_username (saved to your profile) and a warning when the token can only read.

{"ok": true, "account": "your-name", "hf_username": "your-name", "provider": "hf",
 "detail": "Signed in to Hugging Face as your-name (token 'tuneline', write access)."}

Hub

All three routes are optional-user: they work without signing in, and use your saved Hugging Face token when you are signed in. Hub errors: 403 (token needed or access refused, for example a gated model), 404 (repository not found), 502 (Hub error), 503 (Hub unreachable), with hf_status when the Hub answered.

GET /api/hf/models

Search models on the Hugging Face Hub.

Query Meaning Default
search Text to search for. —
arch Hub filter, for example an architecture tag. —
task Pipeline tag, for example text-generation. —
sort downloads, likes, trending, created_at or last_modified. downloads
limit 1 to 100. 30
include_gguf Also return GGUF repositories. false

Response: {models, count}. Each model has id, author, downloads, likes, gated, pipeline_tag, library, model_type, params_b, modality, last_modified, created_at, in_catalog and license.

GET /api/hf/model/{repo_id}

One model in detail, for example /api/hf/model/Qwen/Qwen3-4B-Instruct-2507. Query: revision (optional). 400 when the id is not owner/name.

Response fields: id, author, sha, private, gated, license, commercial_use, pipeline_tag, library, model_type, architectures, modality, params_b, params_source, params_by_dtype, active_params_b, context, dims, config_access (ok, denied, missing or error), trust_remote_code, is_adapter, downloads, likes, last_modified, catalog (the catalog entry or null), vram (memory hints) and warnings (gated, custom code, GGUF repository, LoRA adapter, too large).

GET /api/hf/dataset-preview

Rows of a Hub dataset through the Hugging Face dataset viewer.

Query Meaning Default
repo Dataset repository id (required). —
config Config / subset. first available
split Split. train, else the first available
offset First row, 0 or more. 0
length Rows, 1 to 100. 20

Response: {repo, config, split, offset, length, columns: [{name, type}], rows, num_rows_total, partial}, plus splits: [{config, split}] when the API had to look them up. 404 when the viewer has no splits for the dataset (it may still be processing).

Runs

See Running pipelines for the same flow in the app.

POST /api/runs

User. Validates and compiles the graph, creates the run and launches it.

Field Meaning
pipeline_id Your pipeline's id (optional; 404 if it is not yours).
name Run name (default: the pipeline's name).
graph The pipeline graph (required).
target local, modal, runpod, hf_jobs, kaggle or notebook (required).
target_config Where and how to run (below).
settings.use_cache Reuse cached steps. Default true.

target_config:

Field Meaning
gpu Hardware-catalog GPU id such as L4, RTX-4090, A100-80GB. When empty the API picks the first GPU that fits in the estimate's ranking for that target (fallbacks: L4 on Modal and Hugging Face Jobs, RTX-4090 on RunPod). Kaggle is always T4 × 2.
gpu_count 1 to 8. Default 1.
max_hours 0.1 to 72. Default 6. The runner stops itself at this limit.
max_cost_usd The runner stops when the estimated cost reaches it.
cpu_only Run without a GPU (tiny models, the CPU smoke test).
cloud RunPod only: SECURE (default) or COMMUNITY.
flavor Hugging Face Jobs hardware flavor.
worker_id local only: which of your computers runs it.
disk_gb, image, memory_gb, cpu Provider-specific overrides.
{"pipeline_id": "…", "graph": {"nodes": ["…"], "edges": ["…"]}, "target": "modal",
 "target_config": {"gpu": "L40S", "gpu_count": 1, "max_hours": 6, "max_cost_usd": 20},
 "settings": {"use_cache": true}}

Response:

{"run": {"id": "…", "number": 12, "status": "launching"},
 "launch": {"provider_job_id": "…", "message": "…"},
 "estimate": {"gpu": "L40S", "fits": true, "est_hours": 0.4, "est_cost_usd": 0.8, "…": "…"},
 "warnings": []}

For local and notebook targets the run stays queued and launch holds a message and, where relevant, command / commands to start it. estimate is the target summary described under POST /api/estimate (or null when there is no training step). warnings include validation warnings and a memory warning when the chosen GPU looks too small.

Errors:

Status When
400 Unknown target; invalid target_config; pipeline errors (issues); missing API keys (missing_secrets with the reasons, including the keys the target needs to start jobs); a dataset, model version, golden set or computer that no longer exists or is not yours; a GPU the target does not offer.
404 pipeline_id is not yours.
502 / 503 Supabase or the provider failed. When the launch itself fails the response carries run (marked failed).

POST /api/runs/{run_id}/cancel

User. Cancels the run in the database and, for Modal, RunPod, Hugging Face Jobs and Kaggle, at the provider. Local and notebook runs stop at the runner's next check-in.

Response: {run: {id, status, cancel_requested: true}, provider: {ok, detail}, message}. 404 when the run is not yours, 409 when it already finished.

POST /api/runs/{run_id}/retry

User. Starts a new run with the same graph, target, target settings (and computer, for local runs) and cache setting. Same response and errors as POST /api/runs, plus retry_of (the original run's id).

GET /api/runs/{run_id}/notebook

User. Downloads a Jupyter notebook (application/x-ipynb+json, file name tuneline-run-<number>.ipynb) that installs the runner and executes this run in Colab, Kaggle, Lightning or any Jupyter. Each download adds a new run token (valid 14 days).

409 when the run is not queued (already running elsewhere, or finished: use Retry).

GET /api/runs/{run_id}/command

User. One-line commands that execute a queued run on any machine, each with a new run token:

{"bash": "curl -fsSL <app>/runner/install.sh | bash -s -- --run-token tlr_…",
 "powershell": "$env:TUNELINE_RUN_TOKEN='tlr_…'; irm <app>/runner/install.ps1 | iex",
 "pip": "python -m pip install \"tuneline[train,gguf] @ <app>/runner/tuneline-0.1.0-py3-none-any.whl\" && tuneline run --token tlr_…",
 "run": {"id": "…", "number": 12}, "expires_in_hours": 336,
 "note": "Each call adds a new token; commands copied earlier keep working until 2 hours after the run ends."}

Same 409 rule as the notebook. Every call adds a token; earlier ones stay valid until they expire (14 days if the run never starts) or 2 hours after the run ends.

Deployments

See Deployments. Providers: runpod_serverless (RunPod Serverless), hf_endpoint (Hugging Face Inference Endpoints) and modal (Modal). Ollama packages are made by the Ollama package block on your own computer, not through this API.

POST /api/deployments

User. Creates an OpenAI-compatible vLLM endpoint.

Field Meaning
provider runpod_serverless, hf_endpoint or modal (required).
model_uri Model to serve, such as hf://model/<user>/<repo>@<revision>.
model_version_id Or a model version from your Registry (GGUF versions are refused). An adapter's base model is filled in.
name Endpoint name (default: from the model).
config {gpu, max_model_len, min_workers, max_workers, idle_timeout_s, base_model?}; gpu is a size class such as 24GB.
run_id The run that produced the model (optional, for lineage).

Response: {deployment, message}. deployment is the stored row (id, name, provider, model_uri, config, status, endpoint_url, openai_model, provider_id, provider_state, run_id, error); its status starts at creating. Errors: 400 (unknown provider, no model, missing keys with missing_secrets, a GGUF version), 404 (model version not yours), provider errors with deployment marked failed.

POST /api/deployments/{id}/{action}

User. action is stop, resume, delete or refresh (asks the provider for the current status). Response: {deployment} with the updated row. Errors: 404 (unknown action or not yours), 409 (never created at the provider, or already deleted), 400 for Ollama deployments (manage them with the ollama command).

GET /api/playground/models

Public. The chat models Hugging Face Inference Providers serve right now, for the playground's model picker (from the router's public list, cached for 10 minutes).

Response: {models, count, checked_at}. Each model: {id, providers, context_length, price_in, price_out, vision, tools}, where providers are the providers serving it live, price_in and price_out are the cheapest prices in USD per million tokens and context_length is the longest context among them. Errors: 503 when Hugging Face can't be reached.

POST /api/playground/chat

User. Sends one chat request through a deployment or through Hugging Face Inference Providers (billed to your Hugging Face account).

Field Meaning
deployment_id One of your deployments, or
hf_model any Hub model id served by Inference Providers.
messages [{role, content}], roles system, user, assistant or tool; at most 200 messages and 200,000 characters.
system System prompt, added when messages has none.
temperature, max_tokens, top_p, stop, seed Sampling options, passed through.

Response: {content, usage, latency_ms, model, finish_reason, reasoning} plus deployment_id and provider, or hf_model. Errors: 400 (bad messages, missing key, Ollama deployment, a Hugging Face token without the Inference Providers permission, a gated model whose license you haven't accepted, a model no provider serves, such as a private fine-tuned one), 409 (endpoint stopped, failed, without a URL or not reporting a model yet), 503 (busy or cold start: try again in a minute), 504 (no answer in time).

Cron

POST /api/cron/reconcile

Server. Syncs cloud runs with their providers, fails or cancels stale runs, removes finished RunPod pods and refreshes endpoints. The database's tuneline-tick job calls it every 5 minutes with x-tuneline-server-key; Vercel Cron calls GET /api/cron/reconcile (once a day on the Hobby plan) with Authorization: Bearer $CRON_SECRET. Both methods accept either header.

Response: {ok, checked, updated, failed, cancelled, pods_removed, deployments_refreshed, errors, partial, took_s}. 403 without a valid key.

Runner downloads

These are served by the same app, outside /api, without authentication.

Path What
/runner/install.sh Installer for Linux and macOS, with this deployment's addresses filled in.
/runner/install.ps1 Installer for Windows PowerShell, likewise.
/runner/tuneline-0.1.0-py3-none-any.whl The runner package (built from the deployed code when no static file exists).
/samples/<file> Sample datasets used by templates, such as sanctions-alerts.jsonl.
curl -fsSL https://tuneline-studio.vercel.app/runner/install.sh | bash -s -- --worker-token tlw_...
python -m pip install "tuneline[train,gguf] @ https://tuneline-studio.vercel.app/runner/tuneline-0.1.0-py3-none-any.whl"

See Runner CLI for the installer options and the tuneline command.