Developer · API · MCP · CLI

Build on Distribute.

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.

Create your first token Read the quickstart

Getting started

Three steps from zero to controlling your apps from a script:

  1. Create an API token from your dashboard · pick the scopes you need
  2. Send any request to http://distribute.app/api/v1/... with header Authorization: Bearer <your-token>
  3. For Claude Desktop or Cursor, install the MCP server · same token, hands-off

Authentication

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

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.

apps:read List, view, and read logs from your apps
apps:write Restart, stop, delete apps
apps:deploy Deploy new apps and redeploy existing ones
apps:env Read and write environment variables on your apps
apps:files Read, write, and delete files inside your running app containers (live edit)
apps:stream Subscribe to live event streams (file updates, deploy events, hot-reload status)
billing:read Read your wallet balance and usage
domains:read List your custom domains
domains:write Add and remove custom domains
workspaces:read List and view your workspaces

Wildcard tokens (*) exist only for the legacy CLI device-flow login · they cannot be created via the dashboard or API.

REST 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.

CLI

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

MCP server

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.

Install

curl -fsSL http://distribute.app/mcp/distribute -o ~/distribute-mcp.py
chmod +x ~/distribute-mcp.py

Claude Desktop config

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:

Example · manage

"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."

Deploy an app

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.

From a zip URL

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"
  }'

Multipart upload

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"

Fields

Field Type Required Notes
app_namestringyes1-50 chars, lowercase, [a-z0-9_-]
source_urlstringyes (JSON mode)Public http(s) URL to a zip · 500MB max
filemultipartyes (multipart mode).zip blob
portintnoAuto-detected from EXPOSE / source if omitted
custom_domainstringnoDNS CNAME setup follows · Distribute issues SSL automatically
envobjectno{"KEY": "VALUE"} pairs · null deletes a key on redeploy
instance_planstringnonano (default), micro, small, medium, large
descriptionstringnoFree text · shown in your dashboard

Response

{
  "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).

Tip · what happens server-side

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.

Manage running apps

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

Set env vars

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.

Live edit · streaming

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.

Two scopes

Issue narrow tokens for editor integrations:

apps:files Read, write, and delete files inside running app containers
apps:stream Subscribe to the events stream

A token with only these two scopes can update code but cannot redeploy, stop, or delete the app · safe for editor extensions.

Write a single file

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.

Batch · multiple files at once

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.

Read a file

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}

Delete a file

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"}

Subscribe to events · SSE

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"}}

Event types

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}

Editor integration · full pattern

// 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 }),
    }
  );
}

Hot-reload compatibility

Frameworks that watch /app/ and pick up changes automatically:

  • Vite dev server (HMR)
  • Nodemon · Node.js
  • Flask debug mode (Werkzeug reloader)
  • uvicorn --reload · FastAPI / Starlette
  • Next.js dev (Fast Refresh)
  • Webpack dev server
  • Django runserver (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.

Path safety

The server rejects any path that is:

  • Absolute (anything starting with / · the leading slash is stripped before validation, so the rest must be relative)
  • Containing parent traversal · any segment equal to ..
  • Containing a NUL byte
  • Longer than 500 characters

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.

Limits

Per-file size · 5 MB · larger files require full deploy
Batch · 100 files / 20 MB total
Read · capped at 1 MB per file
SSE subscribers per app · unbounded · queue size 256 events / subscriber (drop-oldest if slow)
Heartbeat interval · 15 seconds

Endpoints reference

Live list · this is rendered from the same source the API uses.

MethodPathRequired scope
GET/api/v1/meany
GET/api/v1/appsapps:read
POST/api/v1/apps/deployapps:deploy
GET/api/v1/apps/{name}apps:read
GET/api/v1/apps/{name}/logsapps:read
GET/api/v1/apps/{name}/envapps:env
POST/api/v1/apps/{name}/envapps:env
POST/api/v1/apps/{name}/restartapps:write
POST/api/v1/apps/{name}/stopapps:write
POST/api/v1/apps/{name}/startapps:write
DELETE/api/v1/apps/{name}apps:write
GET/api/v1/apps/{name}/files?path=...apps:files
PUT/api/v1/apps/{name}/filesapps:files
PUT/api/v1/apps/{name}/files/batchapps:files
DELETE/api/v1/apps/{name}/files?path=...apps:files
GET/api/v1/apps/{name}/eventsapps:stream
GET/api/v1/billing/balancebilling:read
GET/api/v1/domainsdomains:read
GET/api/v1/workspacesworkspaces:read

Errors

HTTP status codes mean what you'd expect:

  • 400 · malformed request · check the field your client sent
  • 401 · token missing, invalid, or expired
  • 402 · billing block · wallet empty or unpaid plan · top up to continue
  • 403 · token valid but missing the required scope
  • 404 · app or resource doesn't exist (or isn't yours)
  • 409 · conflict · usually a name collision with another user's app
  • 413 · upload too large · zips capped at 500MB
  • 500 · build or deploy error · the response includes a build_log excerpt
  • 502 · couldn't fetch your source_url · check the URL is public and serves a zip

Error 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.

Changelog

What's shipped recently · most recent first.

v1 · new 2026-05
Live edit · file CRUD + SSE events
Code editors can now patch single files into running containers without a full deploy · 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.
v1 2026-05
Auto-suffix on cross-user collision
When the API deploy receives a 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.
v1 2026-05
Deploy from URL
POST /api/v1/apps/deploy with {source_url, app_name, ...}. Returns the live URL once the build finishes. New scope apps:deploy.
v1 2026-05
MCP server · 14 tools
Zero-dependency Python MCP server bridges Claude Desktop / Cursor to your account. Tools include deploy_app, list_apps, get_logs, set_env, get_balance.
v1 2026-05
Public REST API + scoped tokens
/api/v1/* namespace, scope-gated Bearer tokens, dashboard UI for token issuance and revocation.
platform 2026-04
Auto-repair for unhealthy apps
Diagnose-502 endpoint classifies why an app is broken (OOM, missing module, port mismatch, network, …) and offers one-click repair.

Need something else?

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.