API
Everything the cockpit does goes through this API - there is no private back door - so anything you can click, a script can call.
| REST | https://api.overnite.dev/v1 |
| WebSocket | wss://ws.overnite.dev/ws |
| Format | JSON in, JSON out |
| IDs | prefixed 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_sessioncookie 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:
| Status | Code | |
|---|---|---|
| 400 | invalid_request | the body or query did not validate; message names the field |
| 401 | unauthorized | no session |
| 403 | not_invited | the account is on the waitlist |
| 404 | workspace_not_found, agent_not_found, … | not there, or not yours |
| 409 | workspace_not_running, runtime_offline | the machine is asleep or its runtime is not connected |
| 409 | git_not_connected, git_reauth_required | sign in with GitHub again |
| 409 | github_app_not_installed | install the App on that repository; message has the link |
| 502 | provider_unavailable | GitHub did not answer |
Account
| Method | Path | |
|---|---|---|
GET | /me | { user: { id, login, avatarUrl, plan }, git: { provider, login, scopes } | null } |
POST | /auth/logout | ends the session; 204 |
GET | /auth/github | starts GitHub sign-in (a browser redirect) |
Agent credentials
| Method | Path | |
|---|---|---|
GET | /agents/credentials | every 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/:kind | removes it |
A key is never returned; last4 is all you get back.
Repositories
| Method | Path | |
|---|---|---|
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
| Method | Path | |
|---|---|---|
GET | /workspaces | your workspaces |
POST | /workspaces | { repoFullName?, repoUrl?, branch?, connectors? } → 201 { workspace }; the machine starts |
GET | /workspaces/:id | the workspace with its agents |
POST | /workspaces/:id/start | wake it |
POST | /workspaces/:id/stop | put it to sleep; agents are suspended, the disk stays |
DELETE | /workspaces/:id | delete 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/activity | the 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
| Method | Path | |
|---|---|---|
GET | /workspaces/:id/agents | the workspace's agents |
POST | /workspaces/:id/agents | { kind, task?, worktree?, connectors?, autonomy? } → 201 { agent, warning? } |
GET | /workspaces/:id/agents/:agentId | one 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/interrupt | Ctrl-C |
POST | /workspaces/:id/agents/:agentId/resume | restart a suspended or exited agent under the same id |
DELETE | /workspaces/:id/agents/:agentId | stop 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 | |
|---|---|
kind | yes_no, choice or free_text |
choices | the options, in screen order; answer with the option's number |
choiceKeys | the key that picks each option, when the screen names them (Codex's y, p, Esc) - send that instead of the number |
textChoice | the 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
| Method | Path | |
|---|---|---|
GET | /schedules | every schedule, with nextRunAt and lastRun |
POST | /schedules | { workspaceId, cron, timezone, agentKind, task, openPr?, enabled? } |
PATCH | /schedules/:id | { enabled?, cron?, timezone?, task?, openPr? } |
DELETE | /schedules/:id | the schedule and its runs |
POST | /schedules/:id/run | run it now, even when disabled |
GET | /schedules/:id/runs | past runs, newest first: { id, startedAt, endedAt, status, trigger, agentId, prUrl, error } |
cronis exactly five fields, intimezone(IANA, defaultUTC), and no more often than every 15 minutes: every run starts an agent.openPr(defaulttrue) tells the agent to branch, commit, push and open a pull request - or say so when it changed nothing.- A run moves
waking→running→succeeded·failed·stopped, or isskippedwhen the previous run is still open.
Connectors
| Method | Path | |
|---|---|---|
GET | /connectors | one 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/:kind | disconnect an OAuth connector |
Settings and notifications
| Method | Path | |
|---|---|---|
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/test | send 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.