Every workflow you can do in the dashboard, you can do programmatically. REST API for scripts and CI, MCP server for Claude and Cursor, CLI for your terminal. One token, your apps from anywhere.
Three steps from zero to controlling your apps from a script:
http://distribute.app/api/v1/... with header Authorization: Bearer <your-token>
Every request to /api/v1/* carries a Bearer token in the Authorization
header. Tokens are issued and revoked from your dashboard · Distribute never
shows a token twice, so you'll be asked to copy it once at creation. Lose it,
and you create a new one.
curl -H "Authorization: Bearer $DISTRIBUTE_TOKEN" \
http://distribute.app/api/v1/me
# {"username": "you", "admin": false}
Tokens have an optional expires_in_days · use it for ephemeral CI tokens
and rotate aggressively. Long-lived tokens have no expiration but get a last_used_at
stamp so you can spot dormant credentials.
Scopes constrain what a token can do. Pick the minimum set your script needs ·
a deploy script doesn't need billing:read, an analytics script doesn't
need apps:write.
Wildcard tokens (*) exist only for the legacy CLI device-flow login ·
they cannot be created via the dashboard or API.
Plain HTTP, JSON in and out, predictable status codes. Discovery endpoint
/api/v1 returns the live route list so tooling stays in sync.
# Probe your token
curl -H "Authorization: Bearer $TOKEN" \
http://distribute.app/api/v1/me
# → {"username": "you", "admin": false}
# Discover live endpoints
curl http://distribute.app/api/v1
# → {"endpoints": [...]}
Detailed examples for each common task are in the Deploy, Manage, and Set env vars sections below.
A single Python script · no install, no SDK, no native binary. Runs anywhere with Python 3.8+.
curl -fsSL http://distribute.app/install.sh | sh
distribute login # opens browser · authenticates
distribute apps # lists your apps
distribute deploy ./myapp # one-shot deploy from current dir
distribute logs myapp --tail 50
distribute env myapp DEBUG=false
Model Context Protocol bridge so Claude Desktop, Cursor, and any other MCP-compatible client can drive your Distribute account directly from a conversation. No copy-paste of curl commands, no juggling tabs.
The server is a single zero-dependency Python file. Download it, point your
client at it, paste your token in the env. Twelve tools available out of
the box: list_apps, get_app, restart_app,
get_logs, get_env, set_env,
get_balance, list_domains, list_workspaces,
and more.
curl -fsSL http://distribute.app/mcp/distribute -o ~/distribute-mcp.py
chmod +x ~/distribute-mcp.py
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"distribute": {
"command": "python3",
"args": ["/Users/you/distribute-mcp.py"],
"env": {
"DISTRIBUTE_API_TOKEN": "your-token-from-dashboard",
"DISTRIBUTE_BASE_URL": "http://distribute.app"
}
}
}
}
Restart Claude Desktop. You can now ask:
"List my Distribute apps. For any that crashed in the last hour, get their last 50 log lines and restart them."
Example · deploy"Deploy this Next.js project · I just pushed it to https://github.com/me/site/archive/main.zip · call it 'site' and use port 3000."
A deploy is a single POST to /api/v1/apps/deploy. Distribute
accepts your source as a zip · either uploaded directly (multipart) or
fetched from a URL you control (GitHub release, S3 presigned URL, etc.).
The response includes the live HTTPS URL once the build finishes.
The cleanest mode for CLI, CI, and MCP · no multipart bodies to assemble:
curl -X POST https://distribute.app/api/v1/apps/deploy \
-H "Authorization: Bearer $DISTRIBUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"app_name": "myapi",
"source_url": "https://github.com/me/myapi/archive/main.zip",
"port": 3000,
"env": {"NODE_ENV": "production", "DB_URL": "postgres://..."},
"instance_plan": "small",
"custom_domain": "api.example.com"
}'
For local file uploads from a script that has the zip on disk:
curl -X POST https://distribute.app/api/v1/apps/deploy \
-H "Authorization: Bearer $DISTRIBUTE_TOKEN" \
-F "app_name=myapi" \
-F "app_port=3000" \
-F "file=@./myapi.zip"
| Field | Type | Required | Notes |
|---|---|---|---|
app_name | string | yes | 1-50 chars, lowercase, [a-z0-9_-] |
source_url | string | yes (JSON mode) | Public http(s) URL to a zip · 500MB max |
file | multipart | yes (multipart mode) | .zip blob |
port | int | no | Auto-detected from EXPOSE / source if omitted |
custom_domain | string | no | DNS CNAME setup follows · Distribute issues SSL automatically |
env | object | no | {"KEY": "VALUE"} pairs · null deletes a key on redeploy |
instance_plan | string | no | nano (default), micro, small, medium, large |
description | string | no | Free text · shown in your dashboard |
{
"success": true,
"app": "myapi",
"url": "https://myapi.distribute.app",
"custom_url": "https://api.example.com", // present if custom_domain set
"app_type": "nodejs",
"is_redeploy": false,
"deployment": "Docker + Traefik",
"ssl_enabled": true,
"ssl_instructions": "Add CNAME record: api.example.com → myapi.distribute.app",
"cname_target": "myapi.distribute.app"
}
Redeploys are detected automatically · POSTing the same app_name
again rebuilds the image and rolls the container without losing your
custom domain or env vars (any keys you don't pass are preserved).
Distribute runs the same pipeline as the dashboard upload form:
balance check, name uniqueness check, project type detection,
docker build,
port verification against the image's ExposedPorts,
Traefik label assignment, Let's Encrypt SSL provisioning. The
response returns once routing is live · typically 30-60 seconds
from POST to first 200 response.
Every common operation has a dedicated endpoint:
# List
curl -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps
# Inspect
curl -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi
# Tail logs (last 200 lines by default)
curl -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi/logs?tail=500
# Restart
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi/restart
# Stop (compute pauses, data preserved)
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi/stop
# Start a stopped app
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi/start
# Delete · destructive, cannot be undone
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi
Read or update environment variables on a running app. Updates trigger a container restart so the new values take effect immediately.
# Read current env
curl -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi/env
# Set / update keys · pass null to delete a key
curl -X POST https://distribute.app/api/v1/apps/myapi/env \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"env": {
"DEBUG": "false",
"DB_URL": "postgres://prod-host/db",
"OLD_FLAG": null
}
}'
Keys you don't include in the payload are left untouched. This makes it safe to update one variable in a script without having to fetch and re-send the whole environment.
For code editors and IDEs that need to apply changes faster than a full
deploy. Patch a single file (or a small batch) directly into the running
container · no rebuild, no version bump, no container swap. Hot-reload
frameworks (Vite, Nodemon, Flask debug, uvicorn --reload) pick
up the change immediately.
A complementary Server-Sent Events stream pushes file and deploy events back to the client so editors can react without polling.
Issue narrow tokens for editor integrations:
A token with only these two scopes can update code but cannot redeploy, stop, or delete the app · safe for editor extensions.
curl -X PUT https://distribute.app/api/v1/apps/myapi/files \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"path": "src/index.js",
"content": "console.log(\"hello live\")",
"encoding": "text"
}'
# {"success":true,"path":"src/index.js","size":24}
Path is relative to /app/ in the container. encoding
can be text (UTF-8 string, default) or base64
for binary files. Each file caps at 5 MB · larger payloads should go
through a regular deploy.
curl -X PUT https://distribute.app/api/v1/apps/myapi/files/batch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"files": [
{"path": "src/a.js", "content": "..."},
{"path": "src/b.js", "content": "..."},
{"path": "logo.png", "content": "iVBORw0K...", "encoding": "base64"}
]
}'
# {"success":true,"written":["src/a.js","src/b.js","logo.png"],"failed":[]}
Up to 100 files / 20 MB total per batch. Per-file failures don't roll back the others · the response lists what succeeded and what didn't.
curl -H "Authorization: Bearer $TOKEN" \
"https://distribute.app/api/v1/apps/myapi/files?path=src/index.js"
# {"success":true,"path":"src/index.js","content":"...","encoding":"text","size":24}
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
"https://distribute.app/api/v1/apps/myapi/files?path=src/old.js"
# {"success":true,"path":"src/old.js"}
Open a long-lived HTTP connection and receive events as they happen.
Compatible with the browser's native EventSource when using
cookie auth, or with any streaming HTTP client when using Bearer tokens.
curl -N -H "Authorization: Bearer $TOKEN" \
https://distribute.app/api/v1/apps/myapi/events
data: {"type":"hello","app":"myapi","ts":"2026-05-02T12:00:00Z","user":"alice"}
data: {"type":"file_updated","ts":"...","app":"myapi","payload":{"path":"src/index.js","size":24,"by":"alice"}}
: heartbeat
data: {"type":"deploy_event","ts":"...","app":"myapi","payload":{"phase":"success","method":"pipeline","commit_sha":"a1b2c3d4"}}
hello · sent once when the stream opens · {user, app, ts}
heartbeat · every 15s · keeps proxies from idling out · zero payload (SSE comment line)
file_updated · single PUT · {path, size, by}
files_updated · batch PUT · {paths, count, by}
file_deleted · DELETE · {path, by}
file_error · failed mutation · {path, error}
deploy_event · full upload or pipeline rebuild finished · {phase, method, ...}
container_event · container restart / crash · {state}
// Open the events stream once at startup
const events = new EventSource(
`https://distribute.app/api/v1/apps/${appName}/events`,
// Note · browser EventSource doesn't support custom headers, so use
// a session cookie (login first) or a streaming fetch wrapper for tokens
);
events.addEventListener('message', (e) => {
const evt = JSON.parse(e.data);
if (evt.type === 'deploy_event' && evt.payload.phase === 'success') {
refreshPreviewFrame();
}
});
// On every editor save · push the file
async function saveFile(path, content) {
await fetch(
`https://distribute.app/api/v1/apps/${appName}/files`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ path, content, encoding: 'text' }),
}
);
}
// On bulk operations (refactor, find/replace) · use batch
async function saveFiles(files) {
await fetch(
`https://distribute.app/api/v1/apps/${appName}/files/batch`,
{
method: 'PUT',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ files }),
}
);
}
Frameworks that watch /app/ and pick up changes automatically:
--reload · FastAPI / Starletterunserver (StatReloader)
For everything else (production gunicorn, compiled binaries, static
builds), call POST /api/v1/apps/{name}/restart after the
batch of file changes is done.
The server rejects any path that is:
/ · the leading slash is stripped before validation, so the rest must be relative)..
The path is always interpreted relative to /app/ inside the
container. Combined with the container's filesystem isolation, an
attacker cannot reach outside their own app even with a valid token.
Live list · this is rendered from the same source the API uses.
| Method | Path | Required scope |
|---|---|---|
| GET | /api/v1/me | any |
| GET | /api/v1/apps | apps:read |
| POST | /api/v1/apps/deploy | apps:deploy |
| GET | /api/v1/apps/{name} | apps:read |
| GET | /api/v1/apps/{name}/logs | apps:read |
| GET | /api/v1/apps/{name}/env | apps:env |
| POST | /api/v1/apps/{name}/env | apps:env |
| POST | /api/v1/apps/{name}/restart | apps:write |
| POST | /api/v1/apps/{name}/stop | apps:write |
| POST | /api/v1/apps/{name}/start | apps:write |
| DELETE | /api/v1/apps/{name} | apps:write |
| GET | /api/v1/apps/{name}/files?path=... | apps:files |
| PUT | /api/v1/apps/{name}/files | apps:files |
| PUT | /api/v1/apps/{name}/files/batch | apps:files |
| DELETE | /api/v1/apps/{name}/files?path=... | apps:files |
| GET | /api/v1/apps/{name}/events | apps:stream |
| GET | /api/v1/billing/balance | billing:read |
| GET | /api/v1/domains | domains:read |
| GET | /api/v1/workspaces | workspaces:read |
HTTP status codes mean what you'd expect:
400 · malformed request · check the field your client sent401 · token missing, invalid, or expired402 · billing block · wallet empty or unpaid plan · top up to continue403 · token valid but missing the required scope404 · app or resource doesn't exist (or isn't yours)409 · conflict · usually a name collision with another user's app413 · upload too large · zips capped at 500MB500 · build or deploy error · the response includes a build_log excerpt502 · couldn't fetch your source_url · check the URL is public and serves a zipError body is always JSON: {"error": "human-readable message"}
Build failures additionally include a build_log field with the last 2000 chars of compiler/Docker output · most "deploy worked locally but failed on Distribute" issues fall out of reading those lines.
What's shipped recently · most recent first.
PUT /api/v1/apps/{name}/files. The companion GET /api/v1/apps/{name}/events Server-Sent Events stream pushes file and deploy events back. New scopes · apps:files and apps:stream.
app_name already owned by another user, it now auto-appends a UTC timestamp instead of returning 409. Same-owner redeploys still update in place. The response carries auto_suffixed: true + original_name so callers can correlate.
POST /api/v1/apps/deploy with {source_url, app_name, ...}. Returns the live URL once the build finishes. New scope apps:deploy.
deploy_app, list_apps, get_logs, set_env, get_balance.
/api/v1/* namespace, scope-gated Bearer tokens, dashboard UI for token issuance and revocation.
Looking for the CLI quickstart and command reference? See /docs. Have a use case the API doesn't cover yet? Open a ticket from the dashboard · we add endpoints based on what users actually need.