AgentMingo REST API
The API lets scripts, CI pipelines and other tools do what people do in AgentMingo: file and read tasks, follow runs of a project's workflow, read findings and decide them, and publish workflows kept as YAML in a repository. Starting runs of AgentMingo's own agents is coming soon: where they don't run yet (agentmingo.com today), POST /runs answers 409 invalid_state.
- Base URL:
https://<your AgentMingo address>/api/v1 - JSON in and out (UTF-8). Times are ISO 8601 in UTC. Ids are opaque strings.
- Every response is
{ "data": … }on success or{ "error": { "code", "message", "details"? } }on failure.
Authentication
Create a token in Settings → API (organization admins). Send it on every request:
Authorization: Bearer amk_3f9c0a1b2c4d_…
- A token belongs to one organization and acts with the role chosen when it was made:
viewer(read),member(tasks, runs, finding decisions), oradmin(also projects and publishing workflows). Only owners can create admin tokens. - Tokens expire after the chosen time (30, 90 or 365 days, or never) and can be revoked at any time. The token is shown once, when created; AgentMingo stores only its hash.
- A token works only while the person who created it could still create it: an admin of the organization (an owner, for an admin token). When they leave or lose that role, the token gets
401; it works again if they get the role back. - Tokens can't approve releases: approvals need a person (see Approvals).
- A browser that's signed in to AgentMingo can call the API too. Session callers add
?organization=<slug>(the organization's address, as in/o/<slug>) to list endpoints, and writes must come from AgentMingo's own pages (same origin).
Idempotency
Writes you might retry (starting a run, filing a task, importing a workflow) accept an Idempotency-Key header: any 1–128 visible ASCII characters, unique per logical request (a UUID works).
- The first response for a key is kept for 24 hours and returned again for retries with the same method, path and body, with the header
Idempotent-Replayed: true. - Reusing a key with a different body returns
422 idempotency_key_reused. A retry while the first request is still running returns409 conflict. - Server errors (5xx) aren't kept, so it's always safe to retry with the same key.
Errors
| Status | code |
Meaning |
|---|---|---|
| 400 | invalid_json, invalid_idempotency_key |
The request couldn't be read. |
| 401 | unauthorized |
Missing, invalid, expired or revoked token, or one whose creator left the organization or lost the role that made it. |
| 403 | forbidden |
The token's role (or your role) isn't enough (a connection token whose person is a viewer, too), the organization is being deleted (any token, until an owner cancels), or the organization or the account that made the token is suspended (any token, until the instance's admins lift it). |
| 403 | insufficient_scope |
The wrong kind of token: a connection token (Sessions) used on the API, or an API token used to report sessions. |
| 403 | human_approval_required |
Only a person can do this (release approvals). |
| 404 | not_found |
It doesn't exist, or it's in an organization you can't see. |
| 409 | conflict, invalid_state |
It can't be done in the current state (e.g. canceling a finished run). |
| 413 | payload_too_large |
Bodies are limited to 256 KB. |
| 422 | validation_failed |
A field is wrong; details lists { path, message } per problem. Publishing a workflow with errors returns its issues. |
| 429 | rate_limited |
Slow down; Retry-After says for how long (seconds). |
| 500 | internal_error |
Something broke on our side; retry with the same Idempotency-Key. |
Rate limits
300 requests per minute per token (or per signed-in person). Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Organizations can start up to 120 runs an hour (API, webhooks and people together); how many run at once is set in Settings → General. Each token (and each person) can create 120 things an hour across their organizations, together: workflows (POST /workflows/import without workflowId), published versions (POST /workflows/{id}/publish), projects (POST /projects), and in the app, copied workflows, repositories added by address, project variables, checklist steps and Jira projects followed; past that, 429 rate_limited. Tasks and comments count toward the organization's 600 task writes an hour. A token has 8 live event streams open at once (GET /runs/{id}/events). An organization's computers share a day's session events and an hour's new sessions, and so do one person's computers across their organizations: past those, events are skipped with a 200 that says so (see POST /agent-sessions/events), not refused with 429.
Me
GET /me
Who's calling.
{ "data": { "kind": "api_token", "token": { "id": "…", "name": "CI", "prefix": "amk_3f9c0a1b2c4d", "role": "member", "expiresAt": "2027-01-22T10:00:00.000Z" }, "organization": { "id": "…", "slug": "acme", "name": "Acme Engineering" } } }
Projects
GET /projects
Projects in the organization, with their repository and number of open tasks.
POST /projects (admin)
{ "name": "Web app", "key": "WEB", "description": "Customer-facing site", "repositoryId": "…", "defaultWorkflowId": "…", "threadWorkflowId": "…" }
key is 2–10 letters or digits starting with a letter; tasks are numbered WEB-1, WEB-2, … threadWorkflowId (optional) is the workflow people's own threads follow on the project's tasks: a published workflow whose trigger has runsIn: "thread" (422 otherwise). Left out, the project follows the organization's thread workflow when it has exactly one published (a new organization starts with “Your own thread”); null means none. Returns 201 with the project.
GET /projects/{id}
{ "data": { "id": "…", "key": "WEB", "name": "Web app", "description": null, "repository": { "id": "…", "fullName": "acme/web", "provider": "github", "defaultBranch": "main" }, "repositoryId": "…", "defaultWorkflowId": "…", "createdAt": "…" } }
Tasks
GET /tasks
Query parameters (all optional): projectId, status (comma-separated: backlog,todo,in_progress,in_review,done,canceled), type (feature, bug, chore, follow_up), assigneeUserId, label, q (text, or a task number like WEB-12), limit (up to 500, default 200).
POST /tasks (member)
{
"projectId": "…",
"title": "Add rate limiting to the public API",
"description": "Markdown is fine.",
"acceptanceCriteria": ["Requests over 100/min get 429", "The limit is configurable"],
"type": "feature",
"priority": "high",
"labels": ["api"],
"status": "todo",
"delivery": "pull_request"
}
delivery is how the task is finished: pull_request (the default; Done when its last open pull request merges) or none (someone marks it done; threads on it are told not to open a pull request). Projects that follow Jira don't take new tasks here (409 invalid_state): their tasks are Jira's issues.
Returns 201 with the task, including its key (WEB-13).
GET /tasks/{id}
The task with its comments, subtasks, dependencies (blockedBy, blocks), runs and links to outside trackers.
PATCH /tasks/{id} (member)
Any of the fields above; fields left out stay as they are. On a task from Jira, only status, assigneeUserId, delivery and estimate change here. Returns the updated task.
POST /tasks/{id}/comments (member)
{ "body": "Reproduced on staging." }
Workflows
GET /workflows
Workflows with their latest published version and number of runs.
GET /workflows/{id}
The draft graph (draft), published versions, and issues: what validation reports about the draft ({ level: "error" | "warning", message, nodeId?, edgeId? }).
GET /workflows/{id}/yaml
The draft as YAML (text/yaml), or a published version with ?versionId=…. Keep it in your repository and import it back to change the workflow as code.
POST /workflows/import (member)
Send YAML as the body with Content-Type: text/yaml (or JSON { "yaml": "…" }). Add ?workflowId=… (or "workflowId" in JSON) to replace that workflow's draft instead of creating a new workflow. Returns the workflow id and the draft's validation issues; publish separately.
curl -X POST "$BASE/workflows/import?workflowId=$WORKFLOW" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: text/yaml" \
--data-binary @.agentmingo/standard.agentmingo.yml
POST /workflows/{id}/publish (admin)
{ "note": "Run E2E tests only when UI files change" }
Publishes the draft as the next version (201 { "data": { "workflowId", "version" } }). Runs that already started keep the version they started with. If the draft has errors, the answer is 422 validation_failed with details.issues. Each version counts toward the token's 120 things an hour (429 rate_limited past it).
Runs
GET /runs
Newest first. Query parameters: projectId, taskId, status (comma-separated: queued,running,waiting,succeeded,failed,canceled), limit (up to 200, default 50).
POST /runs (member)
Starts the project's workflow (or workflowId's published version) with AgentMingo's own agents. Coming soon: where they don't run yet (agentmingo.com today), it answers 409 invalid_state, and a run of a thread's workflow starts from the thread instead. Send an Idempotency-Key.
{ "projectId": "…", "taskId": "…", "title": "Optional title", "baseBranch": "main" }
Returns 201 with the run (same shape as below). Runs wait in the queue when the organization is at its limit of runs at once; a task blocked by an unfinished task can't start (409 invalid_state). A workflow done by people's own Claude Code threads (runsIn: "thread") can't be started here either (409 invalid_state): it starts when a thread is tied to the task, or from the thread's page. Runs have executor: agentmingo, or thread (with threadSessionId, the thread doing its steps).
GET /runs/{id}
{
"data": {
"id": "…",
"number": 12,
"title": "WEB-13 Add rate limiting to the public API",
"status": "waiting",
"statusReason": null,
"canceling": false,
"trigger": "api",
"baseBranch": "main",
"headBranch": "agentmingo/web-13-add-rate-limiting-r12",
"pullRequestNumber": null,
"url": "https://agentmingo.com/o/acme/runs/…",
"usage": { "costUsd": 1.84, "inputTokens": 412000, "outputTokens": 38000, "cacheReadTokens": 290000, "cacheWriteTokens": 52000, "runnerSeconds": 611, "humanWaitSeconds": 0, "repairIterations": 1 },
"findings": { "block": 0, "fix_now": 0, "ticket": 2, "addressed": 3, "noise": 1, "undecided": 0 },
"nodes": [
{ "id": "…", "nodeId": "code", "type": "code", "iteration": 0, "item": 0, "status": "succeeded", "ports": ["out"], "error": null, "attempts": 1, "branch": "…", "commitSha": "…", "costUsd": 1.2, "startedAt": "…", "finishedAt": "…" }
],
"pullRequests": []
}
}
status is queued, running, waiting (a person needs to approve), succeeded, failed or canceled. Each node is one step instance: iteration counts repair rounds, item is the plan item for parallel agents (or null), ports are what it fired (out, error, blockers, clear, exhausted, approved, …).
POST /runs/{id}/cancel (member)
Stops the run: running steps' sandboxes are removed. Returns 202 with the run (canceling: true until it settles).
POST /runs/{id}/rerun (member)
Re-runs a finished run from one step (and everything after it):
{ "nodeId": "tests" }
GET /runs/{id}/events — live events (server-sent events)
A text/event-stream of the run's activity: status changes, step starts and finishes, log lines, agent messages and tool calls, findings, approvals. Each event carries an id (a cursor that only grows); reconnect with the Last-Event-ID header (browsers' EventSource does this for you) to continue where you left off without gaps or repeats. Event data is JSON: { "id", "kind", "message", "data", "nodeRunId", "createdAt" }, where kind is one of status, node, log, agent, tool, artifact, finding, approval, system. Secrets are redacted before anything is stored or streamed. A connection lasts under a minute. One token (or one signed-in person) can have 8 streams open at once; another gets 429 rate_limited with Retry-After: 15, so close one first or reconnect later (with ?after=<last id> to resume).
curl -N "$BASE/runs/$RUN/events" -H "Authorization: Bearer $TOKEN"
Findings
GET /findings?runId=…
A run's findings, most severe first. Optional: decision (comma-separated: block,fix_now,ticket,addressed,noise,undecided), severity (critical,high,medium,low,info), limit (up to 1000).
{
"data": [
{
"id": "…",
"source": "coderabbit",
"category": "correctness",
"severity": "medium",
"title": "Retry loop never gives up",
"body": "…",
"location": { "file": "src/api/client.ts", "startLine": 40, "endLine": 52 },
"suggestion": "Cap the attempts at 5 …",
"decision": "fix_now",
"decisionReason": "Policy: medium findings are fixed in this change",
"decidedBy": "rules",
"status": "fixed",
"firstIteration": 0,
"lastIteration": 0,
"followUpTaskId": null
}
]
}
decision is the platform's triage: block (must be fixed before release), fix_now (fixed in this change), ticket (a follow-up task), addressed (gone after a repair), noise (not acted on, or a duplicate). decidedBy is rules, ai or human.
POST /findings/{id}/decision (member)
Overrides the decision; it's kept in later repair rounds and re-runs.
{ "decision": "noise", "reason": "False positive: the input is validated upstream" }
Approvals
GET /approvals
Release approvals in the organization: ?status=pending (default), approved, changes_requested, rejected, expired or canceled. Each has its run, who may decide (approvers: { roles, userIds }), minApprovals, and the decisions so far.
POST /approvals/{id}/decision
{ "decision": "approve", "comment": "Ship it" }
decision is approve, changes (send it back to repair with your comment) or reject. Only people decide releases: API tokens get 403 human_approval_required. A signed-in approver's browser session can call this endpoint.
The answer is { "data": { "id", "status", "criteriaSaved" } }. criteriaSaved lists the criteria that approving a thread's plan saved to its task: the plan's "Done when" list, saved only when the task had no acceptance criteria and its details don't come from Jira. Otherwise it's []. Only the plan's own approval saves them. The approval's page (/o/{org}/approvals/{id}) shows the list before you decide.
Agent sessions
POST /agent-sessions/events (connection token)
What the Claude Code reporter (/reporter.mjs) sends. It authenticates with a connection token, which install gets by trading the setup command's one-time code (POST /agent-sessions/connection/exchange below) and which can do nothing else (API tokens get 403 insufficient_scope here, and connection tokens get the same everywhere else). Members and up make setup commands; a connection whose person is now a viewer gets 403 forbidden on every connection-token endpoint (its reports would move tasks), until they're a member again. Only threads in a repository that reports here are recorded: one the organization has connected, or one the connection's person added (POST /agent-sessions/repositories). Up to 100 events per call:
{
"events": [
{ "id": "e58cf3f6-7631-414d-b279-6ed5f3c8fbf7", "session": "c3c0361e-…", "kind": "needs_you", "at": "2026-10-24T19:08:37.419Z", "detail": "Permission to use Bash" },
{ "id": "85c8fe50-b44c-4cc7-b71d-b7b42af311dd", "session": "c3c0361e-…", "kind": "stop", "at": "2026-10-24T19:08:51.443Z",
"title": "Saved carts", "repo": "acme/web", "branch": "saved-carts", "client": "vscode",
"usage": { "claude-opus-5-5": { "input": 18, "output": 194, "cacheRead": 27932, "cacheWrite5m": 0, "cacheWrite1h": 28207 } } }
]
}
kindisstart,prompt,activity(a tool call finished;busyMsis how long it ran),needs_you,answered,stoporend.idmakes each event count once: resending a batch is safe. Events older than a week are skipped; a clock ahead of ours is treated as now.- A session whose repository (its latest event's
repo, else the one stored for it) is neither connected here nor added by the connection's person, or that has none, is skipped: nothing of it is stored, and nothing is looked up for it. Its events count inskippedandoutside. Reporter 10 never sends those; reporters before it send every thread. titleanddetailare stored with emails, phone numbers, long numbers and secrets masked ([email],[phone],[number],[secret]): keys and tokens by their shapes (Anthropic, OpenAI, Stripe, Slack, npm, AWS, Google, GitHub, JWTs, AgentMingo's), PEM and PGP private keys, credentials in addresses (user:pass@), afterBearer/Basicand on a command line (--password=…, mysql's-p…), and values given to a password, secret, token or key that are shaped like one (quoted, or 8 characters or more with a digit or symbol; not a variable, call or member access, so code likeconst token = getToken()stays). Reporter 9 masks the older set before sending, reporter 10 all of them.usageis cumulative by model since the reporter first saw the session; each report adds only its growth. So aretools(tool calls by kind:git,github,tests,build,install,shell,read,edit,web,agents,mcp,other, each{ calls, outputChars, ms }) andgit(git commands by subcommand, each{ calls, outputChars }).- Optional:
reviews(whether the session's computer takes review results back; a branch's results go to a thread that does),replies(reporter 15: whether it takes replies sent from the thread's page; only then does the page offer one),leanGit(the session has the lean git rules, in its CLAUDE.md or given by the reporter),leanGitArm(rulesorcontrol: its group in a lean git test, picked when it started; the first one reported stays),hasUpstreamandunpushed(the branch's push state),uncommitted(how many files in its folder have changes not yet committed: a count, never their names; reporter 14 and newer), andfiles([{ path, at }], recently edited files relative to the repository, sent only by computers that share them). shareFiles(reporter 11 and newer): whether the session's computer lets sessions know about each other.falsekeeps the thread out of every teammate's notes: it isn't in anyone else'speers, no "same task" notice names it (one made before is held back while it staysfalse), the Sessions page doesn't offer its branch for combining with another person's, and the paths it sent are cleared. Its person still sees it among their own. Without the field (older reporters) the thread is named as before.counts(reporter 11 and newer, when it changes, and twice a day while it isn'tok): what the session's transcript stopped giving after its hooks saw a reply's worth of messages and tool calls:no_usage(no token counts),no_tools(no tool calls),no_transcript(none to read), orokagain. It's kept for the computer's connection, and/setupcounts the computers that said so in the last week.- A
needs_youevent for a session that has already waited 45 seconds or more (the reporter repeats it after a minute) sends its person a notice, once per wait, linking to the thread's page (where a prompt its computer opened withPOST /agent-sessions/permissionscan be answered; the notice then says so, and names only the tool; otherwise it says "Answer this one in the thread" and why: answering from AgentMingo off there, a reporter older than 17, a subagent's prompt, the prompt not on screen, an answer not applied, the computer not checking, or a dialog Claude Code doesn't share with hooks). - The response's
settingscarries the organization's settings the reporter acts on:{ "leanGit": "off" | "test" | "on", "teamRules": { "text", "by" } | null }, whether new sessions get the rules for every thread (the built-in git habits and the team's own rules, with who wrote them).reporteris{ "version": "11" }, the reporter version this server serves: a reporter installed by the setup command updates itself to it. WithREPORTER_MAX_VERSIONset, it's that older version instead (reporters don't update past it); withREPORTER_ROLLBACK_TO(10 or newer), it's{ "version": "10", "rollback": true }, and reporters 10 and newer on a newer version go back to that one.repositorieslists the repositories ("owner/name") whose threads report here from this person's computers: the organization's connected ones, their earlier names (a GitHub repository renamed or transferred keeps the old name working, so a clone made before may still have it as its origin), then the ones this person added, whichaddedalso lists. A thread reported under an earlier name is stored under the repository's name now. Reporter 10 sends nothing about a thread in any other repository, and a computer connected to several organizations sends each thread to the one that lists its repository. - The response lists the live sessions in the reported repositories, so the reporter can tell each session about the others:
"peers": { "acme/web": [{ "session", "title", "person", "isMe", "branch", "state", "files" }] }(sessionis the reporter's own id for its person's sessions,nullfor other people's), for sessions whose events include astart,promptorstopfrom a computer that shares. A repository is listed under the name the batch used too, when that's an earlier one. checkedlists the sessions (the reporter's ids) whosefeedback,tasksandstepsthe response worked out. For reporter 11 and newer, those are the sessions whose events include astart,promptorstop: a batch of heartbeats and waits looks nothing up, and the reporter keeps what it has for the others. Older reporters, which take a missing entry as none, get every session checked.- An event's
deliveredlists review results (feedback ids) the reporter passed to the session with that message; they're recorded as told (the same onesdeliveron the feedback endpoint below records). ItsdeliveredStepis the workflow step (a step id) it passed on with it. start(reporter 19), only on a session'sstartevent: the id of the start from AgentMingo the thread came from ("start": "clx…", matching^c[a-z0-9]{20,32}$; anything else is left out, not refused with the batch), which the reporter takes from the environment variableAGENTMINGO_STARTthatrun-startsets. It links that start to the thread, once: only a start of this same connection and person, claimed, opened or started (or failed with the server's ownno_wordornot_run), not linked yet, made in the last 24 hours. A start that wasn't marked started yet (the computer's own word was late or lost) is then, with its notice (seePOST /agent-sessions/starts/resultbelow).startTrusted(reporter 21), only besidestart: the repositories whose clones that start marked trusted in Claude Code (at most 10owner/name, never a path; anything else is left out, not refused with the batch), which the reporter takes fromAGENTMINGO_START_TRUSTED(comma-separated) thatrun-startsets besideAGENTMINGO_START. When this report is what marks the start started (itsstartedresult was late or lost), the start's history line names them as the result'strustedwould (checked against the thread's repository); when the result came first, it named them already, and this changes nothing: one line either way.- A
stopwith"keepAlive": true(reporter 18) ends a short turn the thread's watcher woke it for only to keep listening (the feedback endpoint'skeepListening, below). On a thread that waits for its person it's kept askeep_alive: it starts no new turn (the thread'sstateSincestays, so a reply on its way still answers that wait), sends no notice, isn't counted as work, and its usage and cost count like any turn's. A flaggedstopafter the thread went on in that turn (working, a prompt) is an ordinary one. - The response's
feedbackholds, per session in the batch, the blocking and fix-now review results waiting for it:{ "s1": { "ids": […], "note": "…" } }. When notices (main changed files it edited, its task changed, another thread on its task) travel with them, it also hasparts: each part'sidsandnote, in the note's order, so reporter 12, when the whole note doesn't fit the 10,000 characters Claude Code takes with a message, passes on the parts that fit and marks only those as told. - A session whose first report ties it to a task whose project has a workflow for threads starts that workflow's run, or takes over the one going when the thread doing it has ended, been quiet a day or moved on to another task. Such a session (or one a person tied to the task) also takes it over on a later report once that happens, never starting another run; one that only moved onto the task's branch later never does. A task whose pull requests have all merged starts nothing by itself. The response's
stepsholds, per session in the batch that follows one:{ "s1": { "following": true, "runId": "…", "step": { "id", "note", "delivered" } | null, "waiting": "…" | null } }: the step to do now (itsnotecarries{{agentmingo}}where the reporter puts its own command), or what the workflow waits for. - The reporter's version comes from its
User-Agent(agentmingo-reporter/5); workflow steps need 5 or newer. - A
branchwith a task's key in its name (web-12-saved-carts,tommy/WEB-12) ties the session to that task: the first session on a task in Backlog or To do moves it to In progress. The response'stasksholds, per session in the batch that's on a task, its key and the brief the reporter passes to Claude once:{ "s1": { "key": "WEB-12", "note": "…" } }. A session whose branch names no task is left out (the reporter drops the brief it had). - An empty batch checks the connection, and says which repositories report (reporter 10 sends one before anything about a thread when it hasn't heard the list yet, and every 15 minutes or so while a thread outside it runs):
{ "data": { "accepted": 0, "duplicates": 0, "skipped": 0, "outside": 0, "limited": null, "connection": { "organization", "person", "sessionsUrl" }, "peers": {}, "feedback": {}, "settings": { "leanGit": "off", "teamRules": null }, "reporter": { "version": "10" }, "repositories": ["acme/web"], "added": [], "tasks": {} } }. - An organization's computers share two limits, and so do one person's computers across all their organizations, set by the instance's admins (by default 5,000 events a UTC day and 60 new sessions an hour per organization, 2,000 events a day and 20 new sessions an hour per person). Once one is reached, what it covers is skipped, and the answer is still
200(so reporters keep updating, and the kill switch keeps working, since both follow a send that went): past a new-sessions limit, the events of sessions not stored yet (the others are taken); past an events limit, all of them.limitedthen says why and for how long:{ "reason", "window": "day" | "hour", "per": "organization" | "person", "limit", "retryAfterSeconds" }(until 00:00 UTC, or the next hour), and those events count inskipped. Reporter 10 drops them and sends nothing more until then (reporter 9 drops them too). Events that turn out to be already recorded, or that weren't recorded because the batch failed partway, don't count. Each connection token also has its own 240 calls a minute, past which it gets429 rate_limitedwithRetry-After. 403 forbiddenwhile the organization, or the account that made the connection, is suspended by the instance's admins (as while the organization is being deleted): the connection stays, and works again once the suspension is lifted. Reporter 10 then leaves that connection alone (no reports, lookups or review watcher) for an hour, doubling to a day, or until install runs again.- One person can have 5 computers connected at a time (an instance setting), across their organizations: making a sixth setup command says to disconnect one first, and so does running one when five are connected.
POST /agent-sessions/connection/exchange (no token: the setup code)
Install trades the setup command's one-time code for the computer's connection token: { "code": "amc_…", "org": "acme" } → { "data": { "token": "amk_…", "report": { … } | null, "approvals": false, "startFromWeb": false } }. startFromWeb is true when the code was made with the box "Start threads on this computer from AgentMingo" ticked (kept with the code): reporter 19's install turns the start helper on only then, on a Mac, with --start-from-web in the command (a command made with the box unticked has no flag for it, so the computer keeps what it has). Reporter 19's User-Agent (agentmingo-reporter/19 (darwin)) also gives the new connection its platform from the start. A code made with the box "Answer prompts and reply to threads from AgentMingo" ticked starts ama_ instead, and its answer has "approvals": true: reporter 16's install turns answering prompts and replies on only then, with --approvals in the command. Only the code's hash is stored, so a code with its prefix changed is unknown (401). report is what an empty batch to /agent-sessions/events answers for the new connection (whose it is, which repositories report), and counts as its first report (the computer shows as connected), so install doesn't call again once the code is spent; null when that failed (install then sends the empty batch itself). The setup command carries the code instead of the token, so the token never sits in a command line, a process list or a shell history. The command also carries --share-files or --no-share-files, --reviews or --no-reviews, and --approvals or --no-approvals, as the setup form's boxes say. A code works once, for 15 minutes, for the person and organization it was made for: 401 once it's used or expired (or unknown), 403 when org names another organization (the code still works for its own) or its person is now a viewer, 409 when its person has 5 computers connected, 422 for something that isn't a code. The connection (and its audit entry) is made now. 30 trades an hour per network. A setup command with --token from before codes still works.
POST /agent-sessions/repositories (connection token)
Which repositories' threads report here, for the reporter's repos commands: {} lists them, { "add": "acme/side" } adds one for the connection's person (their threads in it report here, though the organization hasn't connected it; only theirs), { "remove": "acme/side" } takes it out → { "data": { "connected": ["acme/web"], "added": ["acme/side"], "repositories": ["acme/web", "acme/side"], "result": "added" | "already" | "connected" | "removed" | "not_added" | null } }. Names are owner/name as the folder's origin remote has them, matched without case (422 otherwise); one already connected isn't added (connected). A plain Git remote connected under a name of its own is listed under that name and the owner/name in its clone URL, which is what its clones' origin says. At most 50 per person and organization (409). The Sessions page lists them and removes them too, and each is audited.
{ "check": "acme/web-old" } (reporter 11 and newer) asks whether a name the computer doesn't find listed, which shares its owner or its name with a listed repository, is a connected GitHub repository under an earlier name: result is connected (it's listed now, under repositories), checking (AgentMingo asks GitHub, as the organization's installation, in a job after this response; ask for the list again in a minute or two), or outside (it shares neither with a connected GitHub repository, it was asked about today already, or the organization's 4 lookups this hour are used). A name GitHub leads to a connected repository's id becomes one of that repository's earlier names; any other isn't kept (a day's rate-limit bucket holds only its hash, and the job that asked holds it encrypted until it's pruned, an hour after it finished, however it ended). Until then its threads stay outside. A name another connected repository has now (in any organization) is no repository's earlier name, and each earlier name is asked about again once a day: one GitHub leads elsewhere now, or nowhere the installation can see, stops being one.
POST /agent-sessions/task (connection token)
The task a branch names, which the reporter asks for when a session starts on a branch with something like a task key in its name, so Claude has the task from the first message: { "branch": "web-12-saved-carts", "repo": "acme/web" } → { "data": { "task": { "key": "WEB-12", "note": "…" } } }, or { "data": { "task": null } } when it names no task in the organization. With repo (reporter 10 sends it) for a repository that doesn't report here: { "data": { "task": null, "outside": true } }, and nothing is looked up. 422 without a branch.
POST /agent-sessions/start (connection token)
For the reporter's start command (reporter 6 or newer), which opens a thread on a task from any folder: { "task": "WEB-12", "repo": "web" } → { "data": { "key": "WEB-12", "title": "…", "branch": "WEB-12-saved-carts", "repository": { "fullName": "acme/web", "defaultBranch": "main" }, "others": [{ "fullName": "acme/api", "defaultBranch": "main" }] } }. others are the organization's other repositories: a workflow's plan can take the task into them, so the reporter lets Claude work beside their clones (--add-dir), and a thread gets another one's folder for the task with start <key> --repo <name> --no-launch. repo (a full name, or a name that's unique among the connected repositories) picks where Claude opens; without it, it's the project's repository, else the one this person last worked in on the project's tasks (or anywhere), else the first connected. The repositories are the organization's, then the ones the connection's person added (repos add) that aren't connected; an added one has "defaultBranch": "HEAD", since AgentMingo doesn't know its main branch, and the task's branch is made from the clone's origin/HEAD. 404 for a task that isn't in the organization (a Jira issue that isn't here yet is fetched, and shows up within a couple of minutes); 409 when repo isn't one of them (the message names the ones that are) or there's none (the message says to run repos add in the clone's folder); 422 for a malformed key. A repository with earlier names (renamed or transferred on GitHub) also carries "formerNames": ["acme/shop"], so reporter 11 finds a clone whose origin still has one.
POST /agent-sessions/feedback (connection token)
Review results for a session's pull request (review bots' inline comments, such as CodeRabbit's, Copilot code review's or Greptile's, and failed CI checks, arriving through the GitHub App), as the reporter's background watcher asks after each reply:
{ "session": "c3c0361e-…", "deliver": ["clx…", "clx…"] }
The answer: { "data": { "pullRequest": { "number", "url", "headBranch" } | null, "pending": [{ "id", "source", "decision", "decisionReason", "severity", "title", "body", "suggestion", "file", "line", "url" }], "later", "rounds", "maxRounds", "wake", "note", "following", "step" } }. Results go to one session: the thread of the workflow run working on the pull request while it's still at it (on the run's task, or on its branch within the last day) and taking review results, else the first session to come onto the branch that's still going and takes review results; other sessions on the branch get pullRequest: null for it. A failure whose check was skipped on the pull request's latest commit isn't pending: it has cause: "not_run", and every note names it as unchecked. pending is the blocking and fix-now results the session hasn't been told about, or the next step of the repository's pull request checklist (source: "checklist"), handed out once reviews and checks are clear; note is the text for Claude; wake says whether the reporter may wake the session for it (review fixes count toward maxRounds automatic rounds per pull request; checklist steps don't). With deliver, those results are recorded as told first (only results that go to one of the connection's person's threads, as above, or that are on a pull request of a workflow run one of them is doing; others are left as they are). Only the repository's own pull requests have results here: a fork's pull request is ignored, whatever its branch is called. For a session following a workflow, following is true and step is the step to do now ({ "id", "note", "delivered" }, or null); the watcher wakes the session for a step it hasn't been given, and records it as given with deliverStep. A session whose repository doesn't report here (or that was never stored) gets nothing, with "outside": true, and nothing is recorded for it.
- With
"forMessage": true(reporter 12, just before each message reaches Claude, with review results on), the answer is only{ "data": { "message": { "ids", "note", "parts"? } | null } }: what that message would carry, exactly as a report'sfeedbackhas it for the session, so results that arrived after the watcher stopped go with that message. It records nothing: the message's event marks them told (delivered). - Reporter 11's watcher asks every 30 seconds for the first three minutes after a reply, then every minute until ten, then every two minutes until 18 (17 asks; reporter 10 asks every 30 seconds, 36 times; reporter 18 asks once more when its time is up). Reporter 15's listens up to 8 hours instead in a thread following a workflow, and, on a computer that takes replies, in any thread, asking every five minutes after the first half hour (about 113 asks), and stops within 5 seconds of its person typing in the thread or the thread ending (it looks at the thread's state on the computer, without a request); the thread's next reply starts a watcher of its own, which takes over. A poll's answer is about 12 database round trips; a listening ask (
listen, replies on) about 17, and reporter 18's last two of a watch, which also read whether to keep the thread listening, about 20. A request that queues no work doesn't hold its function open to drain the job queue (the minute heartbeat does). - Reporter 15, on a computer that takes replies (answering prompts and replies from AgentMingo on there; each report says so with
"replies": true): its watcher adds"listen": <seconds until it asks again>(the thread's page offers a reply until then, and a minute more), and the answer carries"reply": { "id", "text", "from" } | null, a reply the thread's own person sent from its page (Continue, Yes, No, or up to 1,000 characters). The watcher takes it with{ "session", "deliverReply": "<id>" }→{ "data": { "replyTaken": true | false } }(a compare-and-set: one call takes it, audited), then wakes the thread with it as its person's words, and with the step and review results its next message would have carried. A reply not taken within 2 hours expires. A reply answers the turn it was sent in: it's offered and taken only while the thread still waits in that turn, and a message typed in the thread, its end, or the thread going on without it (woken for something else) cancels one still waiting, as AgentMingo applies those events, whatever the computer's clock says. No route sends a reply: only its person, on the thread's page, signed in. - Reporter 18 (keep listening): when its watcher's time is up (any limit), it asks once more with
"listen": 0(it stops listening now), so what came in its last minutes isn't missed; from 18, anylistenup to its slowest pace (300) counts as listening until then and a minute more, and 0 until now (before 18, only its paces counted, its last word, the time left, until now). To reporter 18's last two asks of a watch (the time left, alistenthat isn't one of its paces, and"listen": 0), the answer also carries"keepListening": true | false; its paced asks, and older reporters' asks, don't get the field (and nothing is read for it). It's true only while the thread waits for its person, its computer takes replies (reporter 18, replies on), its person's setting "Keep threads listening while their work is open" is on (on by default), it's tied to work still open (its task isn't done or canceled, an open or draft pull request on its branch, or a workflow run that's going), its wait began less than 7 days ago, and the server'sKEEP_LISTENINGisn'toff. Each watch's end decides again. When a long watch with replies on has listened its whole time and the latest answer that said it was true (on a watch hook that gives it the whole 8 hours, at most 21 times in one wait), the watcher wakes the thread for one short turn ("Reply with only: Still listening for AgentMingo"), whose Stop starts a fresh watcher, and that Stop's event says"keepAlive": true(above). A"listen": 0told true, 8 hours or more (less 5 minutes) after the thread's last event, counts as listening for 180 seconds and a minute more, so the page's reply box stays open through that short turn and the fresh watcher picks up what's sent in it. 401when the connection was removed or its person left the organization (the reporter then stops sending, and never uses that token again; from reporter 20 its watcher doesn't stop: it goes on through the organization's new connection once the computer has one, at once after a new setup command, asking nothing until then, and stops at its next look after uninstall);403while its person is a viewer (reporter 10 then stops its watcher, and waits an hour before asking again). Limit: 240 calls a minute per connection.
POST /agent-sessions/steps (connection token)
A thread following a workflow reports a step, from the reporter's step command or its MCP tool complete_step:
{ "step": "clx…", "outcome": "done", "summary": "1. Add a saved_carts table 2. Save on sign-out", "repos": ["acme/api", "acme/web"], "e2e": { "needed": false, "reason": "No screen changes" } }
outcome is done or failed (the step fails with summary as why; with nothing connected to its error output, the run stops). summary and e2e.reason are stored with secrets and personal details masked, as titles are (a plan in an approval email is masked again). For a plan step, repos (full names, or names only one connected repository has) says which repositories the work goes in, and e2e whether it needs an end-to-end test: the steps after it name those repositories, the pull request step waits for a pull request on the run's branch in each, Reviews and checks for all of them, and an end-to-end step is passed over with the reason when needed is false. 422 for a repository that isn't connected (the message names the ones that are). The answer: { "data": { "recorded", "message", "next": { "following", "runId", "step", "waiting" } } }. message is what Claude reads: the next step's instructions (recorded as given), or what the workflow waits for (a person's decision, review bots and CI), or how it ended. Reporting a step that's already recorded is safe (recorded: false). An "open the pull request" step finishes when GitHub reports the pull requests on the branch, not on the thread's word: until then the answer says so (recorded: false). 404 for a step that isn't a thread run's of this connection's person; 409 for approvals and Reviews and checks (people and GitHub settle those).
POST /agent-sessions/steps/files?step=<id>&name=<file name> (connection token)
A file a thread attaches to one of its run's steps, as the raw request body (up to 4 MB): an end-to-end test's video (.webm, .mp4), screenshots (.png, .jpg, .gif, .webp), reports (.json, .html, .xml, .txt, .log, .md) or traces (.zip). The reporter sends them for complete_step's files and the step command's --attach (files or folders). 201 → { "data": { "id", "name", "kind", "size" } }; the thread's page shows them (videos play, screenshots show, GIF and WebP ones too), each step's latest copy of a name once. Screenshots whose name has a folder named before or after (evidence/before/WEB-12-cart-375-light.png: what it shows, the width, light or dark) are before and after pairs: the task's page shows the latest of each side by side, across its thread runs. The reporter keeps a path's folders only for a file inside the folder its command runs in, so attach them from the folder that holds evidence/. 422 for another kind of file or an empty one; 413 over 4 MB; 404 for a step that isn't a thread run's of this connection's person; 409 when the server has nowhere lasting to keep files (a serverless host without an S3 bucket) or the step has 20 already.
POST /agent-sessions/steps/current (connection token)
The step a thread is on: { "session": "c3c0361e-…" }, or { "repo": "acme/web", "branch": "web-12-saved-carts" } (the person's latest thread there that follows a workflow; what the MCP server sends, from its folder), with "deliver": true to record it as given → { "data": { "following", "runId", "step": { "id", "note", "delivered" } | null, "waiting" } }. The reporter asks before passing a message to Claude in a session that follows a workflow, so a step that became ready since its last report goes with that message. For a session or repository that doesn't report here: { "following": false, …, "outside": true }.
POST /agent-sessions/permissions (connection token)
Reporter 14, only on a computer whose person turned answering prompts from AgentMingo on there (approvals on in their own terminal, or from reporter 15 the setup command with its box ticked, --approvals, which install takes only with the command's one-time code from their signed-in Sessions page, made with the box ticked; from reporter 16 wherever the command runs, a coding agent's thread included; off unless they do, and no answer turns it on). When a thread's PermissionRequest hook waits for an answer, it opens the prompt here so its person can answer it on the thread's page too; Claude Code's own prompt stays on screen, and the first answer counts. From reporter 17 that's every prompt the hook sees in a thread's main conversation (a permission for any tool, a question, a plan to approve), with the terminal's own answers:
{ "session": "c3c0361e-…", "requestId": "6f1c2a9e-…", "tool": "Bash", "kind": "command", "preview": "npm test", "added": null, "removed": null,
"canAllow": true, "why": null, "details": { "badges": ["command"], "summary": "Run the tests", "session": [{ "type": "addRules", "rules": ["Bash(npm test:*)"] }] },
"bind": "<64 hex>", "previewHash": "<64 hex>", "waitS": 3585 }
kindisfile_edit,file_create,file_write,notebook,command,mcp,other,questionorplan.previewis a file's path (from the repository's top, or as given outside it; never the change, only how many lines it adds and removes), a command (20 lines and 1,000 characters at most), or an MCP or other tool's input in one line (300 characters at most: the field that says what it does when it's the only one filled, else every filled field as compact JSON, that field first), masked on the computer and masked again here;nullfor a question or a plan.details(reporter 17):badges(outside_repo,settings,runs_code,hidden_chars,masked,indirect,command,cut,mcp: what the card says it is),summary(a command's description),serverandtool(an MCP tool's),questions([{ "header", "question", "multiSelect", "options": [{ "label", "description" }] }], at most 4),plan(at most 20,000 characters), andsession: what Claude Code's own suggestions for this prompt (the hook'spermission_suggestions) would allow if the person picks "Yes, and don't ask again" (addRuleswithrules,setModewithmode,addDirectorieswithdirectories; only allow rules, folders, and the modes acceptEdits, default, plan and auto; at most 10 rules, folders and modes in all, and sent only when each shows exactly as it's applied, not masked, cut or with characters that don't show; the card lists every one). Each text is masked again here; details that don't parse, or over 60,000 characters, are dropped.canAllowis the computer's word that it can apply a yes: reporter 17 says true for every prompt. Before it, only for some file changes, andwhysaid why not (outside_repo,protected,hidden_chars,masked,indirect,commands_off,command,tool): those requests offer No only. This server offers No only, too, for a kind its tool can't have (a file kind for a tool other than Edit, MultiEdit, Write or NotebookEdit; a question for a tool other than AskUserQuestion, a plan for one other than ExitPlanMode, without their details).bindis an HMAC the computer makes, with a key that never leaves it, of the session,requestId, the tool and its whole input;previewHashis the sha256 of what the request shows (tool, kind, preview, line counts, and from reporter 17 the details as canonical JSON). Both come back with an answer, and the reporter applies one only when they match its own.- A short label of what it asked (a command's first line, a path, an MCP server's tool, a question's first question, "the plan"), masked, at most 120 characters, is kept with the request for 30 days: the thread's history shows it to its person.
waitSis how long the hook will still wait: the request closes here 35 seconds before (at most 8 hours).- →
{ "data": { "status": "pending", "expiresAt" } }, the same again for the samerequestIdandbind(409for anotherbind), or{ "status": "refused", "reason": "outside" | "too_many" | "too_late" }(a thread whose repository doesn't report here; 5 waiting per thread or 20 per person, counting only requests their computer checked in the last 90 seconds; under 30 seconds left).404while the thread's first report hasn't arrived (the reporter asks again for up to 15 seconds). - Its own limit: 30 opens a minute per connection.
POST /agent-sessions/permissions/check (connection token)
The waiting hook asks for an answer, every 3 seconds for two minutes, then every 10: { "session", "requestId" } → { "data": { "status": "pending" } } while none, and each check records that the computer is still waiting (an answer is taken on the page only while it checked within 90 seconds). The first check after an answer picks it up, once: { "status": "allowed" | "denied", "bind", "shown", "note", "stop", "ageS", "choice", "answers" } (shown: the hash of what the page showed; ageS: how old the answer is, and the reporter refuses one over two minutes old; choice: once or session for an allow, session being "Yes, and don't ask again", which the reporter applies as Claude Code's own suggestions with destination session, and only where it got some; answers: a question's, [{ "picks": [<option numbers>], "other": "<typed>" | null }], one per question, which the reporter turns into AskUserQuestion's answers as the tool's input). Its preview, details, note and answers are then deleted here. Later checks say { "status": "allowed", "delivered": true } with nothing to apply. A request past its time answers { "status": "expired" }. Its own limit: 120 checks a minute per connection, apart from reports. A request belongs to the thread's person, not to a connection: after a 401 (the connection removed), reporter 20's hook goes on checking through the organization's new connection once the computer has one (at once after a new setup command), and asks nothing until then, not even to close it (after uninstall, which leaves no connection to wait for, it stops at its next look). One it stopped checking for 10 minutes meanwhile is closed by the sweep (below): its next check reads { "status": "expired" } when it wasn't answered, or, when it was answered on the page and not picked up, { "status": "allowed" | "denied" } with no bind and no delivered, nothing to apply. The hook stops waiting at either, and Claude Code's own prompt stays for its person (before reporter 20, the hook went on checking an answer closed that way until its time was up).
No endpoint answers a prompt: only the thread's own person can, on its page, signed in (a server action; a valid session is enough, with no recent-sign-in rule from reporter 17's release). API tokens can't, and POST /approvals/{id}/decision never takes a prompt's id. Each answer is single-use, tied to that one request, rate limited (30 a minute), audited (session.permission_allowed with its choice, or session.permission_denied; never what it asked), and shown in the thread's history to its person.
POST /agent-sessions/permissions/close (connection token)
The hook stops waiting: { "session", "requestId", "outcome" } with outcome answered_locally (answered in the terminal, or the turn moved on), expired, no_dialog (in a terminal, Claude Code didn't say the prompt was on screen within 20 seconds, so the hook stopped waiting and left it to the terminal), refused_locally or too_late (an answer it didn't apply), or stopped (Claude Code stopped the hook: the prompt answered or dismissed there, the turn interrupted, or Claude Code closing; the request reads as expired). An answer given on the page that the computer never applied keeps its status, with an audit line saying it went unused. Idempotent. → { "data": { "status" } }. Its own limit: 60 closes a minute per connection, apart from opens. A request its computer stopped checking 10 minutes ago is closed by the minute sweep (closeReason stopped_checking).
Starting threads from AgentMingo (connection token, reporter 19 on a Mac)
A person presses "Start on my computer" on a task's page; the start helper on their own Mac (a LaunchAgent that start-from-web on installs) checks for it, claims it, opens Terminal on a file it writes itself, and reports how it went. No route creates a start: only the computer's own person, on the task's page, signed in (a server action). The server sends only a start's id and a task's key; the command, the folder and the prompt are built on the computer.
Common to the three routes below:
POST, JSON body,Authorization: Bearer amk_…(a connection token) andUser-Agent: agentmingo-reporter/19 (<process.platform>). From reporter 19 every request's user agent carries the platform, and the connection keeps it (darwin,linux,win32, orother).- Their own limit: 30 calls a minute per connection, the three together (
429withRetry-After). They don't mark the connection used, so a computer's "Last report" stays its last report. - A start is visible only to the connection that it was asked of, and its person.
- Errors use the usual envelope
{ "error": { "code", "message" } }:400for a body that doesn't read (the helper logs it and doesn't retry),401and403as for every connection-token endpoint,404for a start that isn't this connection's,409for a move the start can't make,413over 256 KB,429rate limited.
POST /agent-sessions/starts/check
{ "helper": "running" | "turned_on" | "turned_off", "answers": true } → { "data": { "off": false, "starts": [{ "id": "clx…", "task": "WEB-12" }], "nextPollMs": 10000 } }.
answers: whether answering prompts from AgentMingo is on there (approvals); the task page says so when it's off.turned_off(start-from-web off): starting threads is off for the connection, its waiting starts are canceled (reasonturned_off), auditedsession_connection.starts_off{ by: "computer" }when it was on;off: true.turned_on(start-from-web on): on, and a turn-off from the Account page is lifted; auditedsession_connection.starts_on{ by: "computer" }when it was off. Then asrunning.running: a turn-off from the Account page answersoff: truewith no starts; a connection that never said it's on is on from now.- A platform other than
darwinanswersoff: truewith no starts. starts: up to 3, oldest first, waiting and unexpired. A check changes no start.- The time of the check is kept at most every 30 seconds (the page says "last checked in"), and
answerswhen it changes. nextPollMs:START_FROM_WEB_POLL_MS(10,000 by default, kept within 5,000 and 30,000), 2,000 when it handed out a start, and 60,000 withoff: falseand no starts whileSTART_FROM_WEB=off. The helper keeps it within 5 and 300 seconds itself.- While
START_FROM_WEB=off, what the computer says is still kept (turned_onandturned_off, the check's time,answers); a turn-off from the Account page still answersoff: true.
POST /agent-sessions/starts/claim
{ "id": "clx…" } → { "data": { "claimed": true } }, or { "data": { "claimed": false, "status": "expired" | "canceled" | "failed" } }. A compare-and-set: a waiting, unexpired start of this connection and person becomes claimed. Claiming one that's claimed, opened or started already by this same connection answers claimed: true again, so a lost reply is harmless; a waiting one past its time answers expired. 404 for a start that isn't this connection's.
POST /agent-sessions/starts/result
{ "id": "clx…", "outcome": "opened" | "started" | "failed", "reason": "no_clone", "detail": "…", "repo": "acme/web", "trusted": ["acme/web"] } → { "data": { "status": "started" } }.
trusted(reporter 21, withstarted; at most 10owner/name, never a path): the repositories whose clonesrun-startmarked trusted in Claude Code just before it launched it (projects["<clone>"].hasTrustDialogAcceptedin the computer's~/.claude.json, or the file Claude Code keeps its state in); left out when it marked none (all trusted already, or the file couldn't be read). The start's line in its task's history names them, kept only as the start's own repository, when it's one the organization connected or, failing that, one the start's person added.reasonis required withfailed, and one ofrefused,busy,terminal_failed,no_clone,fetch_failed,no_main_branch,folder_taken,worktree_failed,folder_blocked,task_refused,claude_missing,turned_off,error. The server's own reasons (no_word,not_run) are refused (400).detail: at most 300 characters, the computer's home folder shown as~; control characters are dropped.repo:owner/name.- Moves:
claimed→opened;claimedoropened→started(the task's history, the audit logsession.start_started, and a notice to its person, "Sam's MacBook started a thread on WEB-12"), and alsofailedwith the server's own reason (no_word,not_run: the computer's word came late, or was lost) →started;claimedoropened→failed(the same, with the reason: "WEB-12 didn't start on Sam's MacBook: …"). The same outcome again changes nothing (200); any other move is refused,409with the start's status. busyis the computer's own hourly limit (6 opens); itsdetailsays when the next can open ("The next one can open in 38 minutes."). Within 15 seconds of its last open, reporter 19 leaves a start untaken instead, and opens it at a check a few seconds later.- A start nobody claims in 2 minutes expires; one claimed 2 minutes without opening fails (
no_word); one opened 15 minutes without starting fails (not_run): the minute sweep, each told once. Starts are deleted 30 days after they're made.
DELETE /agent-sessions/connection (connection token)
Disconnects the calling connection. The reporter's install calls it with the old token when a new setup command replaces the connection already on that computer, so the Sessions page doesn't keep listing it, and its uninstall (reporter 10 and newer) calls it for each of the computer's connections. Answers { "data": { "removed": true } }; 401 if it was already disconnected; 403 insufficient_scope for an API token.
Notifications
GET /notifications/latest (signed-in person)
The person's newest notification, { "data": { "id", "title", "body", "url", "approve" } | null }: what the service worker (/sw.js) shows when a browser push wakes it. Pushes carry no payload. approve is a workflow approval's id while it's still waiting (the notice then gets an Approve button), and always null for a thread waiting on its person: a permission prompt is answered only on the thread's page, once its person has seen what it asks. 403 insufficient_scope for an API token.
Health
GET /api/health (no auth)
{
"ok": true,
"db": "up",
"heartbeat": { "status": "fresh", "lastTickSecondsAgo": 21, "paths": [{ "source": "vercel", "lastTickSecondsAgo": 21, "stopped": false }, { "source": "pg_cron", "lastTickSecondsAgo": 34, "stopped": false }] },
"queue": { "due": 0, "oldestDueSeconds": null, "stuck": false },
"version": "0.2.0"
}
For uptime monitors. 503 with "ok": false when the database can't be reached ("db": "down"), when no heartbeat tick came for 10 minutes from any path ("status": "stale"; "never" before the first one), or when a quick job has waited 15 minutes ("stuck": true). An answer is reused for 10 seconds. GET /api/health?live=1 answers only { "ok", "db", "version" }: whether the server runs and reaches the database.
GET /api/unsubscribe?token=… and POST /api/unsubscribe?token=… (no auth: the token)
The address in a notice email's List-Unsubscribe header. A POST (a mail app's one click, RFC 8058, with List-Unsubscribe=One-Click) turns off that kind of notice email for that person and answers { "ok": true, "unsubscribed": "pull_requests" }; 400 invalid_token for a token that's cut, altered or over 90 days old; 409 conflict in the rare case the account kept changing while it tried (it never overwrites a Notifications page save made at the same moment): try again. A GET changes nothing: it goes to the page that asks first (303 to /unsubscribe?token=…).