Skip to content
Noite
Esc
↑↓navigate↵open⌘Jpreview
On this page

API

Runner HTTP API reference — auth, per-resource routes and statuses.

Base: the runner listens on 127.0.0.1:8080 in-container; Caddy serves it at api.<domain>. All routes below are prefixed with that host unless noted.

Auth

Credential Sent as Used for
RUNNER_TOKEN Authorization: Bearer <token> (or x-runner-token / x-host-token / x-agent-token fallback) Runner API, control UI server calls
Profile API key Git Basic git:<key> git push / git fetch on smart-HTTP remotes
Collaborator role Assigned per app view = fetch only, push = create + fast-forward, admin = anything
curl -H "Authorization: Bearer $RUNNER_TOKEN" https://api.<domain>/v1/apps

The control UI uses the bearer token server-side; browsers never see it. Git smart-HTTP verifies the API key via the UI’s /internal/git-auth and enforces the collaborator role per slug. Unknown slugs 404 at auth.

A profile API key stops working the moment its owner is banned: every consumer (git smart-HTTP’s /internal/git-auth, /mcp, and event ingest) refuses a key whose account is banned, and the ban deletes the account’s keys. An unban does not restore a deleted key — mint a new one.

Public (no credential): /health, /ready, /v1/git/*, /v1/edge/*.

/v1/edge/* is internal edge plumbing (fallback page, TLS ask gate, wake hop) — public and credential-less by design; not a client API.

Apps

Method Route Notes
GET / POST /v1/apps List; create (quota-checked, see Limits)
GET /v1/apps/by-slug/{slug} One app by slug (the create flow and the git-auth hook have a slug, not an id)
GET / PATCH / DELETE /v1/apps/{id} PATCH sets desired_state: running|stopped; an unknown key is rejected (422). DELETE answers 204 once the purge finished, or 202 {"deleting": true} when it still runs in the background after 3 s; the app is gone from every listing either way
POST /v1/apps/{id}/rename Renames the app; rotates scoped credentials once a provider exists (none today — see Tenancy)
POST /v1/apps/{id}/sleep Parks the app; next request wakes it

Deploys

Method Route Notes
GET /v1/apps/{id}/deploys Deploy history
GET /v1/apps/{id}/deploys/stream SSE stream of deploys
GET /v1/apps/{id}/deploys/{deploy_id}/log Build/deploy log
POST /v1/apps/{id}/deploys/{deploy_id}/cancel Stops an in-flight deploy; its row closes cancelled, a finished deploy is 409
POST /v1/apps/{id}/deploy?role= Deploys one branch’s current tip once (body: ref); push role
POST /v1/apps/{id}/rollback Redeploys a previous deploy; wakes asleep apps first
POST /v1/apps/{id}/git-remote Clone coordinates: url (the public remote) plus username and the internal layout (s3Remote, endpoint, bucket, prefix). No remote key

Env

Method Route Notes
GET / POST /v1/apps/{id}/env List; set (applies from the next deploy; not visible to Worker code at runtime)
DELETE /v1/apps/{id}/env/{name} Removes one var

Domains

Method Route Notes
GET / POST /v1/apps/{id}/domains?role= List (view); claim a custom hostname (admin; pending in multi, verified at once in single)
POST /v1/apps/{id}/domains/verify?role=admin Check the claim’s TXT proof; flips it to verified
DELETE /v1/apps/{id}/domains/{hostname}?role=admin Detaches the hostname

Source

Method Route Notes
GET /v1/apps/{id}/tree?ref= File tree at ref (branch, tag or sha; defaults to the deployed sha, else HEAD). Each file carries path, size, the git blob sha and mode (100644/100755/120000); ≤ 5000 files, truncated above that
GET /v1/apps/{id}/blob/{*path}?ref= File contents at ref
POST /v1/apps/{id}/source/commit Browser-edit commit: up to 100 files (text, or base64 uploads chosen by a per-file optional encoding), optional deletes paths and optional moves ({ from, to } pairs), a message and the session actor ({userId, name}), optional branch and fromSha so a protected main can send the edit to a new branch, and optional expectedTip — a compare-and-set: a branch that moved since the caller read it is refused 409 before any object is written; body cap 40 MiB (the runner caps a commit at 25 MiB decoded); wakes asleep apps

Git forge

Method Route Notes
GET /v1/apps/{id}/git/refs Branches with tip, age, author and ahead/behind vs main, plus the deployed sha
GET /v1/apps/{id}/git/log?ref=&path=&skip=&limit= One page of history (NUL-separated git log, 1..100 commits, nextSkip), optionally for one file
GET /v1/apps/{id}/git/commits/{sha} One commit: metadata, body, per-file stats and the patch against its first parent (or the empty tree)
GET /v1/apps/{id}/git/compare?base=&head= base..head commits, the three-dot diff, and whether a squash merge would conflict (git merge-tree --write-tree)
POST /v1/apps/{id}/git/branches Create a branch (body: name, from); 409 when it exists
DELETE /v1/apps/{id}/git/branches/{*name} Delete a branch; the default branch is refused (400)

Reads resolve against the push mirror, which carries every pushed branch; a branch create or delete publishes exactly like a push (tip bundle, manifest, deploy when main moves).

Storage and source reads classify the backend failure: a missing object, a missing D1 table, or a not-yet-deployed source is 404, an invalid path is 400, and every other mirror/S3/celld failure is 500. (These used to be folded into 409.)

Pull requests

Same-repo branches only (no forks); merging is always a squash. actor is {userId, name} and role is the caller’s app role (view/push/admin).

Method Route Notes
GET /v1/apps/{id}/pull_requests?state=&skip=&limit= Newest first, with comment counts and per-state totals; state is open/closed/merged/all
POST /v1/apps/{id}/pull_requests Open one (body: actor, title, body, base, head); 409 when an open PR exists for the pair
GET /v1/apps/{id}/pull_requests/{number} PR + comments, reviews, the live base..head compare, per-comment outdated, counted approvals and the mergeState
PATCH /v1/apps/{id}/pull_requests/{number} Edit title/body and close/reopen (state); the author or an admin
POST /v1/apps/{id}/pull_requests/{number}/comments Comment; a line comment carries path, line, side and commitSha
PATCH / DELETE /v1/apps/{id}/comments/{comment_id} Edit (author) or delete (author or admin) a comment
POST /v1/apps/{id}/pull_requests/{number}/reviews approved / changes_requested at the current head; never the PR author
POST /v1/apps/{id}/pull_requests/{number}/merge Squash-merge (title, message, deleteBranch); 409 on conflicts, insufficient approvals or a moved base
GET / PUT /v1/apps/{id}/branch_rules Per-app protection: requirePr (the push role cannot move main directly) and requiredApprovals (0/1/2)

Counted approvals are the latest review per reviewer that is approved, not dismissed, recorded at the current head and not by the author; admins bypass the threshold. A push moves every open PR whose head branch moved and dismisses its older reviews. Merging writes one commit whose parent is the base tip, publishes it like a push (so a merge into main deploys) and optionally deletes the head branch.

Storage

Method Route Notes
GET /v1/apps/{id}/storage Storage overview
GET /v1/apps/{id}/storage/d1/{database_id}/tables D1 tables with row counts
GET /v1/apps/{id}/storage/d1/{database_id}/tables/{table}/schema Table columns, foreign keys, indexes, DDL
POST /v1/apps/{id}/storage/d1/{database_id}/tables/{table}/rows One page of rows (body: page, page_size, sort, filters, search)
POST /v1/apps/{id}/storage/d1/{database_id}/write Execute a D1 write (insert / update / delete)
POST /v1/apps/{id}/storage/d1/{database_id}/delete-rows Delete 1..100 rows by key in one atomic batch
GET /v1/apps/{id}/storage/do/{class_name} Durable Object instances (id, scope, ?read=1 preview)
GET /v1/apps/{id}/storage/r2/{bucket}?prefix=&cursor= One page of a bucket folder: folders, files (size, content type) and the continuation cursor
GET / PUT / DELETE /v1/apps/{id}/storage/r2/{bucket}/object?key= Read a text preview / upload one object (streamed body; celld r2 put) / delete it
POST /v1/apps/{id}/storage/r2/{bucket}/delete Delete 1..100 objects by key in one batch (body: keys)
GET /v1/apps/{id}/storage/r2/{bucket}/raw?key= Raw object bytes with the object’s own content type

Metrics and telemetry

Method Route Notes
GET /v1/apps/{id}/metrics?hours=24 Hourly counters (requests, 5xx errors, p50/p95 latency in ms, memory peak/mean in bytes, CPU ms)
GET /v1/apps/{id}/metrics/version Change counter for the dashboard’s poll gate
GET /v1/apps/{id}/logs App logs: 24 h of ingested lines plus the live stdout tail
GET /v1/apps/{id}/logs/stream SSE log stream
GET /v1/apps/{id}/errors?status=open Grouped errors in one status (open/resolved/ignored), with counts and a 24 h series
GET /v1/apps/{id}/errors/{fingerprint} One error issue with its occurrences, parsed frames and logs
PATCH /v1/apps/{id}/errors/{fingerprint} Triage: body status (open/resolved/ignored)
GET /v1/apps/{id}/errors/ingest-key?role= The app’s error-ingest identity (numeric project id + key); view role
POST /v1/apps/{id}/errors/ingest-key/rotate?role= Mint a replacement ingest key, keeping the project id; admin role
GET /v1/apps/{id}/errors/stream?status=open SSE stream of grouped errors in one status (errors.list shape)

Every observability read takes an optional environmentId (the same for logs, metrics and events): absent or empty means production.

POST /v1/errors/ingest records one Sentry envelope’s events (body: projectId, key, events) and answers { "fingerprints": [...] }; a wrong key or an unknown project number is one 404 that reveals neither.

Limits

Method Route Notes
GET /v1/apps/{id}/limits The app’s edge rate limits (requests per minute; null = platform default, 0 = off), fleet memory cap override and sleep override, beside the platform defaults
PUT /v1/apps/{id}/limits Sets them (body: clientRpm, appRpm, memoryMb, sleepAfterS, role); admin role, otherwise 403

Events

Method Route Notes
GET / POST /v1/apps/{id}/events List; log an event
GET /v1/apps/{id}/events/stream SSE event stream
GET /v1/apps/{id}/events/channels Known channels
POST /v1/apps/{id}/identify Identify a user
GET /v1/apps/{id}/users/{user_id}/props Stored user props
GET / POST /v1/apps/{id}/insights List; set an insight

Admin

Method Route Notes
GET /v1/admin/stats Fleet-wide stats
POST /v1/admin/snapshot Writes the state snapshot to the bucket

Git smart-HTTP

Stock Git protocol; no git-remote-s3 on clients. Push to main to deploy.

Method Route Notes
GET /v1/git/{slug}/info/refs Advertisement (view suffices)
POST /v1/git/{slug}/git-upload-pack Fetch (view suffices)
POST /v1/git/{slug}/git-receive-pack Push (push = create + fast-forward; admin = anything)
git remote add noite https://git:<key>@git.<domain>/<slug>
git push noite main

Statuses

Code Meaning
400 Invalid path or body (e.g. blob, source/commit, a malformed ref)
401 Missing or wrong bearer token / API key
404 Unknown app, slug, deploy, key, object, table or source
409 Domain already taken; conflicting sleep/wake transition; lost source/commit race
422 PATCH /v1/apps/{id} body with unknown keys
429 Edge rate limit (+ Retry-After)
500 Runner internal failure (mirror, S3, DB)
503 Fleet shedding load (+ Retry-After) or failed wake

Every error answer is plain text: the runner’s own message (ApiError), never a store internal.

Was this page helpful?