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:
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. |
{"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.
[{"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.