API

Everything the cockpit does goes through this API - there is no private back door - so anything you can click, a script can call.

RESThttps://api.overnite.dev/v1
WebSocketwss://ws.overnite.dev/ws
FormatJSON in, JSON out
IDsprefixed ULIDs: ws_… workspaces, ag_… agents, sch_… schedules

Authentication

There are no personal API tokens yet. A request is authenticated by the session you get from signing in with GitHub:

  • In a browser, the overnite_session cookie on .overnite.dev, sent automatically (credentials: "include").
  • From a script, the same session token as Authorization: Bearer <token>. Sessions last 7 days.

Every route answers only for the signed-in person: a workspace, agent or schedule that is not yours is a 404, exactly like one that does not exist. An account outside the beta gets 403 not_invited.

Errors

{ "error": { "code": "workspace_not_running", "message": "workspace is stopped" } }

The HTTP status says what kind of failure it is; code says which, and is stable enough to branch on. Common ones:

StatusCode
400invalid_requestthe body or query did not validate; message names the field
401unauthorizedno session
403not_invitedthe account is on the waitlist
404workspace_not_found, agent_not_found, …not there, or not yours
409workspace_not_running, runtime_offlinethe machine is asleep or its runtime is not connected
409git_not_connected, git_reauth_requiredsign in with GitHub again
409github_app_not_installedinstall the App on that repository; message has the link
502provider_unavailableGitHub did not answer

Account

MethodPath
GET/me{ user: { id, login, avatarUrl, plan }, git: { provider, login, scopes } | null }
POST/auth/logoutends the session; 204
GET/auth/githubstarts GitHub sign-in (a browser redirect)

Agent credentials

MethodPath
GET/agents/credentialsevery agent kind: { kind, label, env, state: "signed-in" | "key" | "none", key: { last4, updatedAt } | null }
PUT/agents/credentials/:kind{ key } - saves or replaces that kind's key
DELETE/agents/credentials/:kindremoves it

A key is never returned; last4 is all you get back.

Repositories

MethodPath
GET/repos{ repos: [{ id, fullName, defaultBranch, private, cloneUrl, pushedAt }], installUrl } - what you can see and the GitHub App is installed on, newest push first
GET/repos/:owner/:repo/branches{ defaultBranch, branches }, the default first

Workspaces

MethodPath
GET/workspacesyour workspaces
POST/workspaces{ repoFullName?, repoUrl?, branch?, connectors? }201 { workspace }; the machine starts
GET/workspaces/:idthe workspace with its agents
POST/workspaces/:id/startwake it
POST/workspaces/:id/stopput it to sleep; agents are suspended, the disk stays
DELETE/workspaces/:iddelete the machine and its disk; 202
GET/workspaces/:id/changes?agentId= - { branch, ahead, additions, deletions, files } for that agent's worktree, or the checkout
GET/workspaces/:id/activitythe last 100 events, newest first: { activity: [{ id, type, data, at }] }

repoFullName (owner/name) is a repository from GET /repos; repoUrl is any public https clone URL. connectors defaults to ["github", "context7"].

Workspace status: creating · running · stopping · stopped · error.

Agents

MethodPath
GET/workspaces/:id/agentsthe workspace's agents
POST/workspaces/:id/agents{ kind, task?, worktree?, connectors?, autonomy? }201 { agent, warning? }
GET/workspaces/:id/agents/:agentIdone agent, with its prompt when blocked
GET/workspaces/:id/agents/:agentId/read?source=screen|recent|detection&format=text|ansi - its terminal
POST/workspaces/:id/agents/:agentId/input{ text, enter? } - type into it; Enter unless enter: false
POST/workspaces/:id/agents/:agentId/interruptCtrl-C
POST/workspaces/:id/agents/:agentId/resumerestart a suspended or exited agent under the same id
DELETE/workspaces/:id/agents/:agentIdstop it

kind is claude, codex, opencode or gemini; autonomy is auto (the default) or ask, and a resume keeps it. The workspace must be running (409 workspace_not_running otherwise). warning says when the agent has no key and no login - it starts anyway, so you can sign in inside it.

Agent status: starting · working · blocked · idle · error · suspended · exited. A blocked agent carries its question:

{
  "status": "blocked",
  "prompt": {
    "text": "Do you want to create hello.txt?",
    "kind": "choice",
    "choices": ["Yes", "Yes, allow all edits during this session", "No"]
  }
}
Field
kindyes_no, choice or free_text
choicesthe options, in screen order; answer with the option's number
choiceKeysthe key that picks each option, when the screen names them (Codex's y, p, Esc) - send that instead of the number
textChoicethe number of an option that is a text box, not an answer: send the number, the text, then Enter

Answering is typing, like at the terminal: POST …/input { "text": "1" }.

Schedules

MethodPath
GET/schedulesevery schedule, with nextRunAt and lastRun
POST/schedules{ workspaceId, cron, timezone, agentKind, task, openPr?, enabled? }
PATCH/schedules/:id{ enabled?, cron?, timezone?, task?, openPr? }
DELETE/schedules/:idthe schedule and its runs
POST/schedules/:id/runrun it now, even when disabled
GET/schedules/:id/runspast runs, newest first: { id, startedAt, endedAt, status, trigger, agentId, prUrl, error }
  • cron is exactly five fields, in timezone (IANA, default UTC), and no more often than every 15 minutes: every run starts an agent.
  • openPr (default true) tells the agent to branch, commit, push and open a pull request - or say so when it changed nothing.
  • A run moves wakingrunningsucceeded · failed · stopped, or is skipped when the previous run is still open.

Connectors

MethodPath
GET/connectorsone row per connector: { kind, label, auth, status, accountName, last4, connectedAt, lastUsedAt, workspaces }
POST/connectors/:kind/connect{ url } to send the browser to, for an OAuth connector
DELETE/connectors/:kinddisconnect an OAuth connector

Settings and notifications

MethodPath
GET/settings{ autoResume }
PUT/settings{ autoResume } - resume suspended agents when their workspace wakes
GET/push/vapid{ publicKey } for pushManager.subscribe(), or null
POST/push/subscribe{ endpoint, keys: { p256dh, auth } } - this browser
DELETE/push/subscribe{ endpoint }
POST/push/testsend yourself a test notification

WebSocket

wss://ws.overnite.dev/ws, authenticated by the same session cookie. Every frame is a JSON object with a t.

From you:

{ "t": "subscribe",   "workspaceId": "ws_…" }
{ "t": "unsubscribe", "workspaceId": "ws_…" }
{ "t": "term.attach", "agentId": "ag_…" }
{ "t": "term.detach", "agentId": "ag_…" }
{ "t": "term.input",  "agentId": "ag_…", "data": "1\r" }
{ "t": "term.resize", "agentId": "ag_…", "cols": 120, "rows": 40 }

To you, for the workspaces you subscribed to:

{ "t": "workspace.status", "workspaceId": "ws_…", "status": "running" }
{ "t": "agent.created",    "agent": { … } }
{ "t": "agent.status",     "agentId": "ag_…", "status": "blocked", "prompt": { … } }
{ "t": "agent.suspended",  "agentId": "ag_…" }
{ "t": "agent.exited",     "agentId": "ag_…", "code": 0 }
{ "t": "term.snapshot",    "agentId": "ag_…", "ansi": "…", "cols": 120, "rows": 40 }
{ "t": "term.output",      "agentId": "ag_…", "data": "…" }

term.attach answers with a term.snapshot - the whole screen, redrawn - and then streams term.output; every byte arrives exactly once. Replay the snapshot into a terminal emulator (the cockpit uses xterm.js) and append the output.

Webhooks

POST /webhooks/github receives the GitHub App's events. It is GitHub's to call, verified by its signature, and not something to call yourself.