Saltar al contenido

Docs / API REST

REST API

Programmatic access to your audit log, policy, tasks, repos, and billing via the developerz.ai REST API.

The developerz.ai REST API is served at https://api.developerz.ai.

Most of what it does is also reachable over the MCP server, so use whichever fits your client. Claude Code and Claude Desktop connect over MCP; scripts and CI workflows typically use REST. The two surfaces are not yet a mirror image of each other: the Capability column below is the id they share, so comparing it against the MCP verb list tells you which of the two serves what you need.

Authentication

Create a personal access token (PAT) in the dashboard under Settings → API tokens, then pass it as a bearer token:

curl https://api.developerz.ai/v1/me \
  -H "Authorization: Bearer dev_pat_…"

Tokens are scoped at creation time. See Scopes below.

PAT capability asymmetries

Multi-org context: The dashboard UI session can switch organizations, toggling which org a request sees via the X-Dz-Account header. PATs ignore that header: a token minted in one account stays bound to that account for its entire lifetime. The header is membership-fenced before any route runs on UI sessions, never trusted from a PAT request. If you need to work with multiple organizations, issue a separate PAT in each one.

Personal-only surfaces: The subroutes under /v1/me/ (personal keys, preferences, system prompt) and the routes under /v1/notifications* (inbox, preferences) need a human behind the credential, because "me" has to resolve to somebody. What decides that is whether the token names a user, not that it is a token: a PAT you mint while signed in carries your user id and reaches your own personal data as you, while a token with no human behind it (a box connection token, or a PAT minted before tokens recorded their minter) returns 403 Forbidden on all of them. GET /v1/me is not in that set and always answers: it is the self-check, and it reports the calling credential's own scopes.

Base URL

https://api.developerz.ai

Every path below is absolute and carries its own prefix. The capability surface is versioned under /v1; the handful of routes that are not (/join, /unsubscribe, /gh/installed) are the unversioned legs a browser, a mail client or a fresh box hits without a bearer token.

Collections

Every endpoint that returns a list answers one envelope, so you write one pager and it works everywhere:

{ "data": [ ... ], "next_cursor": "b3AxMDA" }

Send next_cursor straight back as ?cursor= and never parse it: it is opaque and its internals will change. ?per_page= sets the page size, and a value above a collection's maximum is refused rather than quietly clamped, so you always know what you asked for is what you got.

Paging is keyset. A cursor names a position in the collection, not a row count to skip, so it carries the order it was minted under and the sort keys of the last row it gave you. Two things follow. A bare ?cursor= is enough: you do not have to repeat the sort or the per_page that produced it, and changing per_page mid walk continues from the same row instead of rebasing. And rows written while you page never shift the ones you have not read yet, which is what an offset could not promise.

There is no ?page=. A page number is an offset by another name, and on a collection anything is appending to it re-serves rows you already have. Sending one is refused by name rather than ignored, and the error points at next_cursor. Re-sending the same ?sort= alongside a cursor is fine, since a generic pager appends its base parameters to every request; sending a different one is a 400, because the keys you are holding address a place in the old order.

next_cursor: null means there is nothing after this page. That is a promise, not a placeholder. Some collections are always served in full (your org roster, the connectors on an account, the notification category matrix) and those answer null every time and take no paging parameters at all. Any collection that could truncate hands you a real cursor instead, so a null never hides rows you have not seen.

Two endpoints are the exception and say so in their own description: GET /v1/sessions and GET /v1/prs/pending-approvals are bounded summary views behind ?limit=, so a full page there can mean more rows exist and no cursor reaches them. Narrow the request rather than paging it. Everything else keeps the promise above.

?per_page= is the page-size parameter. ?limit= was an older spelling of it on the endpoints that published both, and it is gone: send it and the call is a 400 whose detail names per_page as the replacement, rather than being quietly ignored. Three reads spell their page size limit and keep it, because they never had a ?per_page= beside it and it is their own canonical knob: GET /v1/sessions, GET /v1/prs/pending-approvals and GET /v1/digests.

Endpoints

Generated from the route table the app mounts, so this list cannot drift from what is running. It covers every capability endpoint; the browser login legs (/auth/* and the OAuth callbacks) are not here because they mint a credential rather than spend one.

The Scope column carries what a caller must present. A scope in backticks is asserted by the route. no bearer marks a route mounted before the token guard, whose description names what authorizes it instead (a signed link, a one-time enrollment token, a webhook signature). per request marks a route that authorizes each call inside its handler, so it publishes no single floor.

POST /v1/sessions/:id/replay (under Sessions) has a page of its own, with a runnable example, the diff it returns and when it refuses: Policy replay.

Account

Method Path Capability Scope Description
GET /v1/me account.me read:account Identity + self-check: the scopes this credential holds, plus user, memberships, active account and ToS state. approved is the closed-beta gate, and it is a TRI-STATE: true cleared it, false is waitlisted, null means this credential names no human to approve (a machine token). Absent or null is never a refusal, so read the field rather than treating "not true" as denied.
POST /v1/me/active-account account.active_account_switch read:account Repoint the live user session at a member account.
POST /v1/me/accept-tos account.accept_tos read:account Record the signed-in user's Terms of Service consent.
GET /v1/admin/overview account.platform_overview admin:read:platform Platform radar rollup (staff read).
GET /v1/admin/hive account.platform_hive admin:read:platform What every fan-out lane reports about itself over the trailing window (staff read): how often a lead dispatched more than one member, how much of the round it then spent idle, how many members it could steer and how many it did, and what it falsified. A graded rate carries its own n / d, and a rate over an empty denominator carries the same counts with d at zero rather than reading 0%; a metric the lane payload does not carry is not_on_contract, which carries no counts at all, and a row an older runner wrote lands in unparseable with the keys it could not satisfy and the time it was last written.
GET /v1/admin/audit account.platform_audit admin:read:platform Platform-wide audit feed (staff read). The account-scoped feed is /v1/audit/events.
GET /v1/admin/platform-audit account.platform_trail admin:read:platform The platform-admin trail: every operator read of a tenant’s data, naming who looked, whose data, which run or account, and when. The other half of account.platform_audit, which records what the platform did FOR a tenant; this records what it did TO one. Keyset-paged, newest-first.
GET /v1/admin/goals account.platform_goals admin:read:platform The platform's own goal board (staff read): every milestone and type/epic issue, graded from live GitHub state with a status, an item-weighted percent, and days since anything last closed. Answers 503 with a named reason rather than an empty board when it cannot be read.
PATCH /v1/admin/goals/:id account.platform_goal_rename admin:goals Rename one goal, writing THROUGH to GitHub: the milestone or type/epic issue is retitled at the source, and nothing is stored here. Its own scope, so a staff radar session can read the board without being able to retitle it. 422 on an id this board never mints, 404 on one GitHub does not have, 503 when GitHub cannot be reached.
GET /v1/admin/accounts account.orgs_list admin:read:orgs All org accounts (staff read), newest-first. q searches the GitHub login; filter=billing:paying|comped|paused narrows to the paying, comped or paused accounts, and omitting it reads every org.
GET /v1/admin/accounts/:id account.org_get admin:read:orgs One account in detail (staff read).
GET /v1/admin/users account.users_list admin:read:users All users (staff read), newest-first. q searches the login; filter=status:approved|waitlisted narrows by approval state, and omitting it reads the whole roster.
GET /v1/admin/oss-verifications account.oss_verify_list admin:read:orgs The queued OSS-verified applications (staff read). Defaults to filter=status:pending (the queue proper); filter=status:all reads decided history, and an unknown value is a 400 rather than a silent fallback. Ruling on one is operator-only, over MCP.
GET /v1/admin/users/:id account.user_get admin:read:users One person in detail (staff read): the registry row, when they were last active (the later of their newest sign-in and newest token use), every org they belong to with their role and the org’s kind, and the first page of their grant history with each grant’s resolved active. Trailed as a tenant read.
PATCH /v1/admin/accounts/:id/members/:userId account.set_member_role admin:accounts Operator only: set one member’s role in any org. Staff need no membership there, but an org never loses its last owner: demoting it answers 409 with reason: last_owner. 404 when the account does not exist or the user is not a member of it. Audited on the org’s own feed as org_member.role_changed, naming the operator and the previous role.
PUT /v1/admin/accounts/:id/tier account.set_tier admin:accounts Operator only: set any account’s tier. oss_verified stamps the OSS review date (an existing one is kept); any other tier clears it. Requires confirm: true. 404 when no account has the id. Audited on the account’s own feed as account.set_tier, naming the operator.
PUT /v1/admin/accounts/:id/seats account.set_seats admin:accounts Operator only: set any account’s paid AI-dev seat count (an integer, 0 or more). Requires confirm: true. 404 when no account has the id. Audited on the account’s own feed as account.set_seats, naming the operator.
PUT /v1/admin/accounts/:id/spend-cap account.set_spend_cap admin:accounts Operator only: set any account’s monthly spend cap in USD. Positive, or null to reset to the default; 0 is refused. Requires confirm: true. 404 when no account has the id. Audited on the account’s own feed as account.set_spend_cap, naming the operator.
POST /v1/admin/oss-verifications/:id/decide account.oss_verify_decide admin:accounts Operator only: rule on a pending OSS-verified application. approve also moves the account to oss_verified in the same transaction and mails the applicant; reject requires a note, which the applicant is shown. Requires confirm: true. 404 when no PENDING application has the id: unknown and already-decided are one answer. Audited on the applicant’s feed as account.oss_verify_decide, naming the operator.
POST /v1/admin/accounts/:id/repos/:owner/:repo/backlog-import account.backlog_import admin:accounts Operator only: run the GitHub backlog import again for a repository the account has ENABLED. Each open issue not already filed becomes an ask for triage, under the same bounds and opt-in as the first-enable import. Requires confirm: true. 202 with job_id. 409 with reason: import_in_flight when a run is already queued or running at the time of the call; two calls racing in the same instant can both answer 202 with the same job_id, and one job runs. 404 when the account holds no repository by that name; 409 with reason: repo_disabled when it is switched off. Audited on the account’s own feed as backlog.import_requested, naming the operator, before the job is queued.
GET /v1/admin/fleet/runs/:runId account.platform_run_get admin:read:runs One agent run’s pulse, whichever account owns it; the owning org is resolved server-side and returned. The cross-tenant debugging read: an ordinary run read is fenced to your own account, and platform staff need to diagnose a run belonging to a customer they are not a member of. A run this platform does not hold is a 404 that discloses nothing and trails nothing; every call that RESOLVES an owner appends a trail row before it answers.
GET /v1/admin/fleet/runs/:runId/events account.platform_run_tail admin:read:runs SSE: one agent run’s durable event transcript, whichever account owns it. Backfill, tail, terminate, the same stream task.run_tail serves a tenant, with the owner resolved server-side. Not resumable: Last-Event-ID is refused rather than ignored, and that 400 is answered before the run is resolved. Every call that RESOLVES an owner appends a trail row before the first frame.
GET /v1/next-steps account.next_steps per request What still stands between this credential and a first scout call, as an ordered ladder of rungs. Each rung carries the fact it was decided from and WHERE it clears, so a rung no verb can move is never retried. Pass ?repo=owner/repo to ask about ONE repo: the same ladder narrowed to it, plus whether it is onboarded and whether its stored secrets sit where a lane is handed them. Every rung is reported every time, passing ones included; a rung whose fact sits behind a scope you do not hold reports unknown naming that scope rather than passing. The same answer the next_steps MCP verb serves, from the same implementation.
GET /v1/oss/verification account.oss_verification_get read:billing This account's latest OSS-verified application, or null.
POST /v1/oss/verification account.oss_verification_apply write:billing Apply for OSS-verified. Amends the open ask while pending, 409 once approved. It grants nothing by itself: the verdict is operator-only.

Overview

Method Path Capability Scope Description
GET /v1/overview/counts overview.counts read:prs Account-wide headline counts: enabled repositories, open pull requests, pull requests merged in the last 24 hours, and pending ask-before-acting approvals. Takes no query parameters; any is a 400.
GET /v1/overview/burn overview.burn read:sessions Recorded agent-session cost over the last hour, account-wide: a decimal USD string summing only prices that may be shown as a cost (null when no displayable cost exists in the window; a client may show that as zero only when the session count is 0, never when sessions ran), the session count, and how many of those sessions spent without such a price. A recorded estimate on your own keys, not a bill. Takes no query parameters; any is a 400.

Personal settings

Method Path Capability Scope Description
PATCH /v1/me/keys/:id/capacity self.key_capacity_set write:self:keys Declare a personal key’s concurrency capacity: model streams it sustains at once, an integer in [1, 256]; null clears it (the default for its kind applies).
GET /v1/me/keys self.keys_list write:self:keys List the caller’s PERSONAL BYOK keys, masked. The org pool is /v1/billing/keys.
POST /v1/me/keys self.key_add write:self:keys Add a personal BYOK key, sealed on write and never echoed back.
POST /v1/me/keys/models self.provider_models write:self:keys List the models a provider key can address, before a PERSONAL key is captured. The key rides in the body and is never stored; the org pool’s door is /v1/billing/providers/models.
POST /v1/me/keys/:id/verify self.key_verify write:self:keys Verify a personal key against its provider (live probe).
POST /v1/me/keys/:id/rotate self.key_rotate write:self:keys Replace a personal key’s secret in place, keeping its id.
DELETE /v1/me/keys/:id self.key_delete write:self:keys Delete a personal BYOK key.
PATCH /v1/me/keys/:id/kind self.key_kind_set write:self:keys Re-stamp a personal key’s billing nature (subscription ⇄ metered).
PATCH /v1/me/keys/:id/priority self.key_priority_set write:self:keys Set a personal key’s pool priority: an integer in [0, 10000], LOWER is tried first (default 100).
GET /v1/me/llm-pref self.llm_pref_get write:self:keys The caller’s personal model preference.
PUT /v1/me/llm-pref self.llm_pref_set write:self:keys Set the caller’s personal model preference.
DELETE /v1/me/llm-pref self.llm_pref_clear write:self:keys Clear it, back to the account default.
GET /v1/me/github-token self.github_token_get write:self:keys Whether a personal GitHub token is connected, its expiry and repo grants. Status only; the token is never echoed.
POST /v1/me/github-token self.github_token_set write:self:keys Connect a personal GitHub token, sealed on write.
DELETE /v1/me/github-token self.github_token_clear write:self:keys Disconnect it.
GET /v1/me/repo-bindings self.repo_binding_list write:self:keys The repos on this account you drive with your own GitHub token (the user-token lane).
POST /v1/me/repo-bindings self.repo_binding_set write:self:keys Drive a repo with your own connected GitHub token instead of the GitHub App. The repo must be in the token’s allowlist and writable by it.
DELETE /v1/me/repo-bindings/:owner/:repo self.repo_binding_clear write:self:keys Stop driving a repo with your own GitHub token.
GET /v1/me/system-prompt self.system_prompt_get write:self:prompt The caller’s personal system prompt.
PUT /v1/me/system-prompt self.system_prompt_set write:self:prompt Set it.
DELETE /v1/me/system-prompt self.system_prompt_clear write:self:prompt Clear it.

API tokens

Method Path Capability Scope Description
POST /v1/tokens token.mint write:account Mint a dev_pat_… PAT with scopes ⊆ mintable_scopes. The plaintext is returned exactly ONCE; only its hash is stored. Body-less mints the caller’s whole mintable set. name is an optional human label: every PAT carries the same prefix, so it is the only field that CAN tell two apart afterwards. Absent leaves the token unnamed; names need not be unique.
GET /v1/tokens token.list read:account PATs, masked, revoked rows included, each with its expires_iso (null ⇒ permanent), plus mintable_scopes: the exact vocabulary THIS caller may mint. Empty ⇒ the caller can neither mint nor revoke.
PATCH /v1/tokens/:id token.rename write:account Relabel a PAT. Moves the human name and nothing else (no scope, no expiry, no revocation state), and a revoked row is renameable so a roster stays readable.
DELETE /v1/tokens/:id token.revoke write:account Revoke a PAT (idempotent, account-scoped).

Operator credentials

Method Path Capability Scope Description
POST /v1/admin/operator-tokens operator_token.issue admin:read:platform Issue a short-lived platform-operator credential carrying one or more of the five admin:* WRITE scopes, or admin:bugs (the staff bug-report scope a staff session already holds, issued here so a CLI can use it). The only door to them on a token: no role ceiling grants one and POST /v1/tokens mints none. Requires a signed-in platform admin, because a token cannot issue another, which is what makes the TTL a bound rather than a formality. The session must have signed in within the last hour (step-up): an older one is refused 403 reauth-required and must sign in again, because a stolen cookie is replayed later than its login. scopes and name are both REQUIRED (there is no "give me the operator token" request, and a credential nobody can account for is worse than none). The plaintext is returned exactly ONCE; the row lands on the issuer’s own token roster, so GET /v1/tokens lists it and DELETE /v1/tokens/:id revokes it.

Org members

Method Path Capability Scope Description
GET /v1/org/members member.list read:account The org’s members and their roles.
POST /v1/org/members member.invite write:members Invite someone to the org by login (an already-registered user, added to the roster at once) or by email (anyone: a one-time invitation link, also mailed to them).
PATCH /v1/org/members/:userId member.set_role write:members Change one member’s role.
DELETE /v1/org/members/:userId member.remove write:members Remove a member from the org.
GET /v1/org/members/invitations member.list_invitations read:account The org’s org_member invitations, every status, newest first.
DELETE /v1/org/members/invitations/:invitationId member.revoke_invitation write:members Withdraw one PENDING invitation (idempotent). Refuses an already-accepted one; remove the member instead.

Access requests

Method Path Capability Scope Description
POST /v1/access-requests access_request.create no bearer Record a waitlist application (the marketing form’s write path, over the shared service bearer). The row is written first, then the applicant is mailed a confirmation; a mail failure never loses the application. A live address applies once: a re-submission folds onto its pending row and answers 200 with the same id.
GET /v1/admin/access-requests access_request.list admin:read:users The waitlist applications, newest first (staff read). filter=status:pending is the queue and the default; invited, declined or all read the decided history. q searches email, name and org.
GET /v1/admin/access-requests/:id access_request.get admin:read:users One waitlist application by id (staff read), with its derived status.
POST /v1/admin/access-requests/:id/invite access_request.invite admin:grants Operator only (admin:grants AND admin:users): let one applicant in. Mints or re-arms their platform invitation and mails it, approves them if they already signed in, and books the starter bundle: ai_devs free AI developers for expires_in_days days (the box budget follows from that count). Every knob defaults to the standard bundle, so {} is the one-click path. Answers the provision result: the accept link is shown ONCE, here.
POST /v1/admin/access-requests/:id/decline access_request.decline admin:users Operator only: decline one pending application. Stamps the decision; an optional reason lands on the audit row only. Nothing is sent to the applicant. An already-decided request answers 409 naming the standing decision.

Users (platform operator)

Method Path Capability Scope Description
POST /v1/admin/users/:userId/approve user.approve_by_id admin:users Operator only: lift the closed-beta waitlist cap on one user by id. Idempotent: an already-approved user answers already_approved; a fresh approval mails them.
POST /v1/admin/users/:userId/suspend user.suspend_by_id admin:users Operator only: drop one user back to the closed-beta waitlist cap by id (clears their approval; their sessions read read:account on the next request). Idempotent: an already-waitlisted user answers already_waitlisted.

Invitations

Method Path Capability Scope Description
POST /v1/invitations/accept invitation.accept read:account Redeem an invitation token for the signed-in identity. Grants platform access or the invited org membership, and answers a named reason when it grants nothing.
GET /v1/admin/invitations invitation.list admin:read:users Platform invitations, newest first, with their derived status and never a link (staff read). Keyset-paged (per_page, cursor); filter=status:<pending|accepted|revoked|expired> narrows it before the page is cut, all (the default) reads every status, and q searches the address.
POST /v1/admin/invitations/:invitationId/revoke invitation.revoke admin:users Operator only: withdraw one platform invitation. An already-revoked one answers 200 revoked: false with the earlier stamp; an accepted one is a 409, since revoking it would not take the access back.
POST /v1/admin/invitations/:invitationId/resend invitation.resend admin:users Operator only: a fresh link and a fresh mail for a pending or expired platform invitation: the same address and the same promise, re-armed, so the old link dies. The accept link is shown ONCE, here. An accepted or revoked invitation is a 409.

Org SSO

Method Path Capability Scope Description
GET /v1/org-sso org_sso.get read:account The org’s SSO configuration.
PUT /v1/org-sso org_sso.set write:account Set the org’s OIDC configuration. A SAML body is refused with a 400: those connections are seed-provisioned for now. The browser login legs are separate, unversioned routes.
DELETE /v1/org-sso org_sso.delete write:account Delete it; the org falls back to platform login.

Repositories

Method Path Capability Scope Description
GET /gh/installed project.install_landing no bearer The GitHub App Setup URL, where GitHub drops the human after an install. No bearer, and it provisions nothing: the install webhook stays the only writer of accounts and repos.
GET /v1/public/oss/:owner/:repo/stats project.public_stats no bearer Public activity and agent-authored counts for ONE enabled, public repo with donations on and/or public_stats_enabled (the owner consent). No bearer. The donation fields need the donations opt-in; agent_authored needs the public-stats consent. The gate is re-run on every positive hit of the in-process cache, so the API stops serving a repo that goes private or loses its last gate flag at once; the response carries Cache-Control max-age 60, so a browser or shared cache can hold one body that long. agent_authored counts merged and reverted pull requests opened by a platform task or by the plan-files documents lane over the last 30 days; release-bump pull requests are not counted. human_approved_30d counts those merged pull requests whose merge ask was approved on the approvals plane (any write:prs credential). days_since_human_commit is the age of the newest merged pull request with a non-bot author flag that no platform task authored, so a direct push to the default branch is not seen.
GET /v1/repos project.list read:repos The account’s repositories with enabled state; rows carry policy_valid and last activity.
POST /v1/repos project.create write:repos Mint a brand-new repo under the account org from the versioned gold-standard template, then dispatch the setup session that scaffolds it. Answers the created repo, the artifact manifest and a scaffold_task_id which may be null: the repo exists either way, so a failed dispatch is reported rather than thrown. Present only where the deployment holds GitHub App credentials.
GET /v1/repos/:owner/:repo project.get read:repos One repository by owner/repo.
PATCH /v1/repos/:owner/:repo project.update write:repos Flip a repo’s owner switches: enabled, donations_enabled, ai_first, canary_enabled, public_stats_enabled. At least one is required and an absent field is untouched state. Enabling a private repo on a plan that does not cover it is a 402 naming the way out, never a 403.
GET /v1/repos/:owner/:repo/policy project.policy_get read:repos The effective maintainer.yml policy, plus fallback: null, or why it fell back.
GET /v1/repos/:owner/:repo/overview project.overview read:repos Activity rollup for one enabled repo (the repo-detail Overview tab).
GET /v1/repos/:owner/:repo/settings project.settings read:repos Settings view for one enabled repo (the repo-detail Settings tab).
POST /v1/repos/:owner/:repo/policy/validate project.policy_validate write:repos Check a maintainer.yml body: {yml} in, ok or errors out. Stores nothing.
POST /v1/repos/:owner/:repo/setup project.setup write:tasks Point a box at a repo that already exists and have it audit the project and establish its base (stack detect, the one-command DX scripts, .mcp.json, a project brain). 202 with the dispatched task_id. It rides write:tasks, not write:repos: a credential that may enable a repo is not thereby allowed to spend the account’s inference budget on it.
POST /v1/repos/:owner/:repo/ask project.ask read:repos Ask a question about one enabled repo, answered from grounded evidence with citations. A body of {question}; the answer is never uncited, so {refused: true, reason} at 200 is the capability working rather than a failure.
GET /v1/repos/:owner/:repo/health project.health read:tasks One repo’s graded health over a window: every SLO ok / warn / bad / no_data with its numerator, denominator and window, and the single worst bottleneck. Also needs read:prs; the live key pool rides with read:runners.

Issues

Method Path Capability Scope Description
GET /v1/repos/:owner/:repo/issues issue.list read:issues One enabled repo’s tracked issues newest-first, with triage state.
GET /v1/issues/:id issue.get read:issues One issue by id.
POST /v1/issues/:id/handoff issue.handoff write:issues Force-fire the issue to the coding-agent webhook (queued delivery; reason plus an optional webhook_ref).
POST /v1/issues/:id/escalate issue.escalate write:issues Force-pull a human in: category + reason → an escalation row and a notification.

Pull requests

Method Path Capability Scope Description
GET /v1/approvals pr.pending_approvals read:prs The account-wide queue of outstanding ask-before-acting approvals.
GET /v1/repos/:owner/:repo/prs pr.list read:prs One enabled repo’s PRs newest-first.
GET /v1/prs/:id pr.get read:prs One PR by id.
POST /v1/prs/:id/approve-pending pr.approve_pending write:prs Approve the PR’s outstanding ask-before-acting action, OF the head the caller saw: body {head_sha}. A push since answers 409 with reason: head_moved and current_head_sha; re-read and decide again.
POST /v1/prs/:id/skip-pending pr.skip_pending write:prs Decline the PR’s outstanding ask-before-acting action, OF the head the caller saw: body {head_sha}. A push since answers 409 with reason: head_moved and current_head_sha.
POST /v1/prs/:id/rearm-babysit pr.rearm_babysit write:prs Re-arm the PR’s spent babysit CI self-fix budget, body {reason}: the ladders count only rounds after this, the act is audited with what it forgave, and a babysit pass is queued now. 202 with forgiven and sweep; a merged or closed PR is a 409 with reason: pr_not_open, a disabled repo a 409 with reason: repo_disabled.

Code review

Method Path Capability Scope Description
GET /v1/prs/:id/review-runs review.status read:prs The PR’s AI code-review runs, newest-first.
POST /v1/prs/:id/review-runs review.rerun write:prs Re-run the PR’s latest code review on the same commit (a fresh push already auto-reviews). 202 with the revived task; rerun: false means one was already queued, which is an idempotent no-op. A PR with no review run yet is a 409, not a 404: the PR is fine, there is simply nothing to re-run. Also refused as a 409 when the run’s OWN plan is already done, since reviving it would restart a slice plan cancel can no longer stop.
GET /v1/repos/:owner/:repo/reviews review.list read:prs Every AI code review run on one enabled repo, newest-first: which pull request, the run’s status, the findings it KEPT (the number it actually posted), the verdict it sent GitHub, and whether a model ran. brain is a three-way verdict, never a boolean: model (a credential resolved, with the provider and model id when the run recorded them), blind (the brain reported no model, with gap naming which one, no_credential or unresolved; that review settles the check neutral and says so), or unknown (the run never reached the brain, which is not evidence either way). provenance is the separate, honest answer to "what did the box actually drive", and is null on every run whose box did not record it. findings_kept: null means the run never reached the findings step, never zero findings. A fixed head of the newest runs, ordered by creation time and then by task id so two identical requests cannot disagree, with no page size to ask for and no cursor to follow, so any query parameter is a 400. 404 when the repo is not enabled for this account.

Asks (work for triage to decide)

Method Path Capability Scope Description
POST /v1/asks ask.file write:tasks File an ask the platform decides on: the object triage judges, not a task. Triage reads it against the LIVE fleet and either mints executable tasks (it decides their order, their file sets and which box runs each), drops it as noise, cross-links it to work already in motion, or escalates it to a human; GET /v1/asks/:askId carries that verdict. Idempotent on dedupe_key per account: filing again with the same key answers kind: "existing" with the ask already filed, never a second one. A caller that has already decided the schedule wants POST /v1/tasks instead, which mints one executable task and runs no triage.
GET /v1/asks/:askId ask.get read:tasks One filed ask and what triage decided about it: pending (nobody has looked yet), triaged with a schedule verdict naming the task_ids it minted (poll each with GET /v1/tasks/:id), dropped (noise, or a duplicate naming the task that covers it) or escalated (a human decides; not a failure). 404 for an id this account does not own, which is also the answer for one that does not exist.
POST /v1/asks/:askId/redrive ask.redrive write:tasks Re-drive one ask the triage judge never answered: an ask escalated with reason judge_unavailable goes back to pending and the admission drain is nudged for a fresh pass. That escalation was an infrastructure failure, not a decision, so re-deciding it is safe; any other escalation reason (or a non-escalated ask) is refused 409 in the verb’s own words, and GET /v1/asks/:askId shows what the row actually is. 404 for an id this account does not own, which is also the answer for one that does not exist. The body carries nudged: false means the row IS pending but the hand-off was refused, and the stale-pending sweep re-nudges it.

Tasks

Method Path Capability Scope Description
GET /v1/fleet/runs/:runId/events task.run_tail read:tasks SSE: one BYOVM run’s durable event timeline (the NATS→SSE bridge). Backfill, tail, terminate. Resumable: each frame’s id: is the row’s seq, so a reconnect carrying Last-Event-ID continues from it instead of replaying the run.
GET /v1/tasks task.list read:tasks The account’s task queue, newest-first.
GET /v1/queue/status task.stalls read:tasks Why is nothing moving: every reason holding a pending task, how many it holds, how long it has been true, and what clears it.
GET /v1/tasks/:id task.run_status read:tasks One task: the queue row, its scoped-plan detail and its run pulse. 404 when unknown or foreign.
GET /v1/tasks/:id/attempts task.attempts read:tasks Every attempt the task has burned, oldest first: one rung per run, each with its own outcome and its own re-derived failure kind. The task row records only the LATEST attempt’s classification, so this is the only surface that says what went wrong on attempts 1 and 2 of 3. 404 when the task is unknown or foreign.
POST /v1/tasks/:id/reassign task.reassign write:tasks Pull one in-flight task off its box back to pending so the fleet assigner re-claims it elsewhere. Idempotent: a pending task answers reassigned: false. A terminal task is refused (409 task_terminal): it holds no box, and the verb that runs it again is redispatch.
POST /v1/tasks/:id/cancel task.cancel write:tasks STOP one task: it moves to cancelled and the box holding it is interrupted. The destructive sibling of reassign, which only moves the work elsewhere. Idempotent: an already-terminal task answers kind: "already_terminal", not an error. One kind of task is refused (409 not_cancellable_review): a PR review whose pull request is still OPEN, because terminating it would release the merge gate’s review hold and let the pull request merge with no review verdict. Once that pull request is merged or closed the hold is moot and the stop is accepted; the platform also retires such reviews itself.
POST /v1/tasks/:id/redispatch task.redispatch write:tasks Re-drive one task back to pending so the fleet assigner claims it fresh (the operator trigger for a task whose retry ladder exhausted at failed, or one otherwise stuck). Resets attempt to 0 (an operator redispatch is a deliberate fresh try; the erased value is prior_attempt and rides the audit row) and keeps failure_kind. The next run resumes the diff the task saved as the tip commit of its own branch (dz/<slug>--<first 8 hex of the task id>); saved_wip names it (found, sha, paths, failures, ending) or says there is none. The tasks that failed BECAUSE this one did go back to pending with it (released_dependents). An optional { guidance } body (at most 2,000 chars, secrets scrubbed) rides the next run’s brief as an operator note; guidance_chars (present only then) is its scrubbed length. Refused as a 409: a task already pending has nothing to redispatch, one an operator cancelled stays cancelled, since that was a decision, not a failure, and one whose PR already shipped (merged, or completed/awaiting_ci/awaiting_review/mergeable under the babysit watch) is not run a second time against that PR.
GET /v1/tasks/:id/events task.run_events read:tasks The same run timeline as plain JSON, paged on ?since_seq= for a client that cannot hold the SSE tail open. An unstarted task answers run_id: null and an empty page, never a 404. Send ?wait_ms= to WATCH it: the request is held open until the first new event lands (or the wait expires), so one call covers what 25 polls would, and the wait also spans the gap before a box starts the task. An empty page after a wait is not an error, it means nothing happened in that window: re-send with the same cursor. A run that has already ended settles the wait at once, so an instant empty page means the run is over rather than idle.
GET /v1/tasks/:id/report task.result read:tasks What the run DELIVERED, folded off the task’s durable events: the report markdown with the source it came from (report_md = the file the brief asked for, final_turn = the run’s closing prose taken in its place, none = neither), the artifacts it attached by reference, every raw task any attempt filed, the access blocker any attempt declared, the devops lane’s own summary, and scout_context, a return_context scout’s survey (its repo, its one-line summary, the files it grounded with their citations, its split hints, the handoffs it noticed beside the question, and the access it needs or the blocker it hit), which is the ONLY deliverable that lane leaves. runs_seen > 1 means the task was retried: the report and the survey are the LATEST run’s, while filings and blockers span every attempt. A task that has delivered nothing yet answers 200 with report: null, not a 404: the 404 is reserved for a task this account does not have. report_missing: true is the run saying it produced no deliverable and failed for it.
POST /v1/tasks task.create write:tasks Dispatch one scoped task to a sink (linear | gh | direct | queued). A fleet enqueue (direct/queued, one admitted sink) answers {task_id, sink, queue_position, queue_depth, queue_depth_cap}; refusals name the REST problem type: a full backlog is a 429 queue-full with Retry-After, an account with no AI devs a 403 no-capacity, a problem that is only GitHub issue or pull-request links a 422 problem-is-only-link (a run may have no way to open them: paste their text); below the confidence threshold → an escalation.

Blockers

Method Path Capability Scope Description
GET /blocker-fix/:jwt blocker.fix_link_form no bearer Show a blocker’s form to a human: each field it asks for (env secrets, service URLs, MCP servers, OAuth connections), with the fields that already landed marked and closed to edits. Reads the blocker and stores nothing, so a mail scanner or link prefetcher changes nothing: 400 malformed, 410 expired or no longer matching the blocker. Needs no login: the signed JWT in the path is the whole credential.
POST /blocker-fix/:jwt blocker.fix_link_redeem no bearer Land the fields that were filled in: vault values are sealed into the repo’s vault, an MCP server’s token into the account vault, and the page hands back the .maintainer.yml stanza for each MCP server. Fill any subset; each field is write-once, and the link stays valid for its hour while fields remain. A live run that asked is woken, and the submit that lands the last field closes the blocker. The JWT is the whole credential and the CSRF token; it can never read a stored value back. 400 nothing filled in, 409 already landed or closed, 410 expired, 413 a form over 1 MiB.
GET /v1/blockers blocker.list read:tasks The answerable holds: the questions the platform is waiting on a human for. Oldest-first while open, because the oldest unanswered question is the one holding work up longest; open=false widens to the closed record, newest-first. A missing_credential row carries its form (the field NAMES it asks for, and which have landed) and, for a caller holding write:secrets, a signed hour-long fix_url a human can fill in without the agent plane ever holding a value. The link fills the form as a bearer, so a caller who may not write the vault reads null. It never carries the answer text.
POST /v1/blockers/:blockerId/answer blocker.answer write:tasks Supply what was missing. Closes the blocker and files a raw task carrying both the ask and the answer, so the answer is durable and admitted rather than lost with the stopped run. It does NOT restart that run: triage decides the raw task on the account’s own key and may schedule, drop, cross-link or escalate it. A held task’s blocker is the exception: the answer releases that task with fresh attempts, as task redispatch does, and files nothing. Answering twice is refused with a 409, never doubled: one blocker mints at most one raw task. Needs a credential that names a human, since the answer is attributed to the person who gave it. Send the LOCATION of a credential, never the credential itself.
POST /v1/blockers/:blockerId/withdraw blocker.withdraw write:tasks Retire an ask that went away, closing it with no answer and minting no work. 404 when this account has no OPEN blocker with that id, which is also the answer for one that is already resolved.
GET /v1/blockers/:blockerId/spec blocker.spec read:tasks The form a missing_credential blocker asks a human or an agent to fill: every field NAME, its kind (env_secret, service_url, mcp_server, oauth_connection), its environment where it has one, whether it has landed, and whether a value can be submitted for it. Never a value. Also accepts the blocker’s own fix-link token as the bearer, for this blocker only. 404 for an id this account does not own.
POST /v1/blockers/:blockerId/submit blocker.submit write:secrets Fill some of a blocker’s open fields: {fields: {NAME: "value", "mcp:<server>": {url, transport, token}}}. Each vault value is sealed into the repo’s vault (sealed, never readable back); an MCP server’s token goes to the account vault under its token_ref, and its url and transport come back as the .maintainer.yml stanza to add. Each field is write-once, so a landed field is a 409. A live run that raised the ask is woken; the submit that lands the last field closes the blocker and files the follow-up work. Also accepts the blocker’s own fix-link token as the bearer. 404 for an id this account does not own, 409 once the blocker is closed, 422 for a name the form does not have or a value it cannot store.

Built-in dashboards

Method Path Capability Scope Description
POST /v1/admin/builtin-dashboard-sync builtin_dashboard_sync.enqueue admin:accounts Operator only: enqueue one cron.builtin_dashboard_sync pass on demand, audited as a platform op. The lane's daily 04:10 UTC tick is the only other producer; nothing in the bot's normal path can reach this queue. Requires confirm: true and a why the maintainer typed. 202 with job_id. 409 with reason: import_in_flight (sic, code-mis-stem) when a manual run is queued or running at the time of the call; two calls in the same instant both answer 202 with the same job_id, and one job runs. Audited on the platform trail as builtin_dashboard_sync.enqueue_requested, naming the operator.

Plans

Method Path Capability Scope Description
GET /v1/plans plan.list read:tasks The account’s plans, newest-activity-first, in numbered pages: plan id, target repo, lead-slice title, the merged/total progress fraction and the held count. The envelope carries total, counts per status bucket and the repos vocabulary; ?bucket=, ?repo= and ?q= filter, ?page= walks.
POST /v1/plans plan.start write:tasks Start a scout planning session over one enabled repo: hand it a raw ask and a mailbox-capable box authors the plan (groups then tasks). 202 with the dispatched task_id plus queue_position (1-based, in claim order) and queue_depth_cap (-1 = uncapped); that task’s run_id is null until a box claims and starts it, and it is the id the mailbox steers. Name SEVERAL repos for one change across them: with return_context: true one sub-scout surveys each and the 202 adds fanout_id; without it the ask is filed for triage, which authors ONE plan with a slice per repository ordered by its dependencies, and the 202 carries raw_task_id INSTEAD of task_id: the same id GET /v1/plans/{planId} takes once triage settles. require_approval on that arm is a 400 naming the field: the triage lane mints unconditionally and has no held state to write. An account with no AI devs is a 403 no-capacity, a full backlog a 429 queue-full with Retry-After. A fleet with no online box able to hold a live session is a 409 naming the mode a box needs, not a 404: the repo is fine.
GET /v1/plans/file-switch plan.file_switch_get read:account The org’s plan-file switch: whether scouts may file plan documents into its repos. Default on; an owner flips it with PUT /v1/plans/file-switch.
PUT /v1/plans/file-switch plan.file_switch_set write:account Flip the org’s plan-file switch: {"enabled": false} opts the org OUT of scouts filing plan documents into its repos; true restores the default. Answers the flip it made: changed is false on a no-op set, which is still audited.
GET /v1/plans/:planId plan.get read:tasks One plan’s full projection: the group → task → PR chain.
GET /v1/plans/fanouts/:fanoutId plan.context read:tasks The merged answer of a multi-repo scout fan-out, addressed by the fanout_id POST /v1/plans returned when several repos were named. One sub-scout surveyed each repo; this folds their findings into one answer with per-repo attribution, citations repo-qualified (repo:path:line) and a claim two repos both established collapsed once. A fan-out still running answers 200 too, naming the repos still surveying: poll again rather than reading a short answer as complete. 404 when this account owns no such fan-out, the same answer an unknown id gets.
POST /v1/plans/:planId/cancel plan.cancel write:tasks STOP a whole plan: every non-terminal child of its groups moves to cancelled and the boxes holding them are interrupted. Always 200 when the plan exists, even when nothing moved. The answer is three id lists: what was stopped, what had already finished, and the PR-review children that were not stopped (the same merge-gate carve-out task.cancel refuses on).
POST /v1/plans/execute plan.execute write:tasks Execute a COMMITTED plan directory (the /planx layout: overview.md, NN-*.md slices, status.yml) on an enabled repo. The platform reads it at the default branch, cuts one unit per pull request with the dependencies and file sets the plan states, and records ONE plan: each unit starts when every unit it depends on has MERGED, work units never touch the plan directory, and only a final unit updates its status file. 202 with the plan_id to poll at GET /v1/plans/{plan_id}. dry_run: true records it held for review (POST /v1/plans/{plan_id}/approve starts it). The same directory is always the same plan; revision re-reads a corrected one as a new plan. 404 for a repo this account has not enabled, 422 for a path that is not a plain repo-relative directory.
POST /v1/plans/file plan.file write:tasks File a plan DIRECTORY inline (the /planx layout: overview.md, NN-*.md slices, status.yml): validate every document, check each scout citation against GitHub at the default branch, write plan_documents, then dispatch onto the SAME plan.execute lane POST /v1/plans/execute uses. 202 with the plan_id to poll at GET /v1/plans/{plan_id}. 409 when the org has the plan-file switch off; 404 for a repo this account has not enabled; 422 for every other refusal (a done or cancelled plan, a read of it running, a stale re-file conflict, or every collected problem, slug, documents, scouts or GitHub, named in the body). dry_run: true records the filing held for review (POST /v1/plans/{plan_id}/approve starts it).
POST /v1/plans/:planId/regroup plan.regroup write:tasks Replace unfinished group(s) of an open plan with the tasks that replace them: the tasks a SCHEDULED ask was cut into (ask_id), or task_ids. One new group per task takes the old slot; every group and task that waited on the old one waits on ALL the new ones, and a dependent the old one killed is revived. Nothing is minted and the superseded task is not stopped (the answer names it). 200 with the new groups, 404 for no such plan, 422 for an unknown group, ask or task, 409 for a plan, group or ask whose state forbids it.
POST /v1/plans/:planId/unlink plan.unlink write:tasks Remove named upstream edges (upstream_group_ids) from ONE group (group_id) of an open plan, with a stated reason: the group's depends_on loses them and its task's depends_on loses their tasks, unless an edge that remains still names that task. For edges that are ordering only, not code dependencies; never adds an edge or touches another group. Audited with both edges before and after. 200 with the before/after, 404 for no such plan, 422 for an unknown group or an id that is not an edge, 409 for a plan that is not open or a group already settled.
POST /v1/plans/:planId/approve plan.approve write:tasks Release a held plan stamped when POST /v1/plans carried require_approval: true. Flips every held group back to dispatched and mints its tasks in one transaction. 200 on every plan still releasable: kind: "approved" carries the freshly-minted task ids, kind: "already_approved" is the idempotent re-approve answer (and carries the ids the FIRST call minted). 404 when no such plan, 409 when every group was already stopped by plan cancel (that plan exists and can never be released), and 409 repo_not_enabled when a held group targets a repo no longer enabled for the account: nothing is released until it is re-enabled.

Runners (fleet boxes)

Method Path Capability Scope Description
POST /v1/runners/enroll box.enroll no bearer The box’s own enrollment leg: a runner redeems its one-time enrollment token for durable credentials.
POST /v1/runners/creds/reissue box.creds_reissue no bearer The box rolls its own fleet credential before it expires, authenticating with its durable runner token rather than the credential being replaced. Machine leg. A revoked box is refused, which is what bounds its remaining fleet access to the lifetime of the credential it already holds.
POST /v1/runners/hardening box.hardening_report no bearer The box’s post-harden report (firewall state, exposed-port scan). Machine leg.
GET /join box.join_script no bearer The one-curl join script a fresh VM runs. Its credential is the one-time enrollment token, which rides the X-Dz-Enroll-Token HEADER and never the query string; this leg validates the shape but does not consume it.
POST /v1/runners/enrollments box.add write:runners Mint a one-time enrollment token. Returned exactly once.
POST /v1/runners/enrollments/ssh box.add_ssh write:runners Enroll over SSH instead: hand us the box’s coordinates and its coding model, and the mothership dials in and runs the join. 503 when the deployment has no platform SSH key.
GET /v1/runners box.list read:runners The fleet boxes enrolled under this account, each with live presence.
POST /v1/runners/:id/revoke box.revoke write:runners Cut a box off: the box is told to halt over its control channel (its in-flight run is released back to the queue as it drains), its presence key is dropped, its credential is refused at the next dial and its gateway tokens are cascaded dead. The reversible half is POST /v1/runners/:id/drain, which evacuates a box and leaves it enrolled.
GET /v1/runners/versions box.versions read:runners The fleet’s dz-runner build spread right now: how many boxes run each version, live beat first and the last recorded version as fallback.
GET /v1/runners/releases box.releases read:runners The dz-runner release ledger per build target. A tenant reads the installable builds their own boxes may be pointed at (view: installable); a platform operator holding admin:releases reads the whole ledger, halted rows and build provenance included (view: ledger).
POST /v1/runners/releases/:channel/:version/unyank box.unyank admin:releases Restore a halted release so it re-enters the installable read. Platform-wide, so it rides the operator scope no account role grants; reports the artifact count it actually cleared.
POST /v1/runners/update box.update write:runners Start a staged self-update rollout of THIS account’s fleet (canary, soak, bounded waves, halting itself on failure). Reports what it started, not an outcome. A second trigger joins the running rollout rather than racing it.
GET /v1/runners/:id box.get read:runners One box in full: its row plus live presence (online, active sessions, reported version, telemetry); presence: 'unknown' + presence_error when the presence KV could not be read, never an offline box. 404 when unknown or foreign.
POST /v1/runners/:id/drain box.drain write:runners Evacuate a box: flip it out of the assignable pool and re-queue its in-flight work. Reversible, and the box stays enrolled. Killing one is POST /v1/runners/:id/revoke.
POST /v1/runners/:id/decommission box.decommission write:runners Take a box OUT of the fleet in one act: drain (in-flight work re-queued now) → revoke (row, gateway tokens, the halt over its control channel, presence dropped) → audit runner.decommissioned. 404 when unknown or foreign; a cascade_failed token kill is a 200 carrying the detail (the box is gone, one idempotent retry owed). The box-side half is sudo dz-decommission on the machine.
POST /v1/runners/:id/creds/reissue box.reissue_creds write:runners Operator-initiated credential re-issue for a healthy box: mint a fresh fleet credential on demand. Convenience, not recovery. The verb rides the NATS control plane, which is exactly what a box with a lapsed credential has lost, so a locked-out box is systemctl restart dz-runner. A foreign id, an unknown id, and a revoked box all answer 404 (under a MEMORY resolver, refusing the roll-forward is the mechanism that makes revocation converge, and distinguishing it would tell a caller a box exists when it does not); a deployment without a NATS account signing key answers 503, the same posture the box-initiated leg carries.
PUT /v1/runners/:id/owner box.set_owner write:runners Retarget which work a box claims: org (this account) or user (one member, across their orgs). 422 when the target cannot own a box here.
PUT /v1/runners/:id/prompt box.set_prompt write:runners Set or clear the box’s concise operator briefing (what this VPS runs, how to dispatch on it). Max 2000 characters; send null to clear.
PATCH /v1/runners/:id box.rename write:runners Rename a box, or send display_name: null to restore its generated name. 1-64 characters after trimming, one line; answers the GET /v1/runners/:id shape. 404 when unknown or foreign; 409 for a revoked box.
GET /v1/fleet/utilization box.utilization read:runners The one read the command center polls: per-box load, BYOK key pool heat, per-lane backlog depth and per-repo KB freshness, in one call. presence: 'unknown' + presence_error on the body (and presence: 'unknown' on every box) when the presence KV could not be read or none is wired (a deployment without NATS), never an offline fleet. Writes nothing.

Agent templates

Method Path Capability Scope Description
GET /v1/templates template.list read:runners Every agent template on this account.
GET /v1/templates/:name template.get read:runners One agent template by name.
PUT /v1/templates/:name template.set write:runners Create or REPLACE a template by name. A full replace: an omitted field resets to its default, and a compatible backend must carry a base_url.
DELETE /v1/templates/:name template.delete write:runners Delete a template by name. Idempotent.

Prompt variables

Method Path Capability Scope Description
GET /v1/prompt-vars config.list read:runners The account prompt variables, optionally narrowed to one scope and ref.
GET /v1/prompt-vars/:scope/:key config.get read:runners One prompt variable at (scope, ref, key). The ref is a repo id or template name, and only repo/template scope takes one.
PUT /v1/prompt-vars/:scope/:key config.set write:runners Set one prompt variable. Plaintext, so NEVER a credential: a secret-shaped value is refused, and sealed secrets belong in /v1/repos/:owner/:repo/secrets.
DELETE /v1/prompt-vars/:scope/:key config.delete write:runners Clear one prompt variable. Idempotent.

CI runs

Method Path Capability Scope Description
GET /v1/ci/runs ci.runs read:runners Self-hosted CI runs, newest-first. Filter by repo and status through the shared list grammar (?filter=status:failed), not bare query params.
GET /v1/ci/runs/:id/log-url ci.log_url read:runners A short-lived presigned URL for one run’s archived log. One 404 covers unknown/foreign, log not landed, log expired (3-day retention) and storage unconfigured, none of which a caller may distinguish.

Artifacts

Method Path Capability Scope Description
GET /v1/artifacts artifact.list read:artifacts Artifacts this account produced, newest-first. Narrow with run_id, task_id or repo_id; per_page caps the head (default 50, max 200) and there is no cursor, so a full page may mean more remain.
POST /v1/artifacts artifact.put write:artifacts Record an artifact and mint a presigned PUT for its bytes. You upload to upload_url yourself; our bucket credentials never leave the control plane. Visibility is run or account only, a repo_id, run_id or task_id outside your account is a 404, and 503 means there is no bucket to write to: none on the deployment, or the account’s connected S3 integration cannot be opened.
GET /v1/artifacts/:id/url artifact.get read:artifacts A short-lived presigned GET for one artifact, alongside its metadata. One 404 covers unknown/foreign, bytes that never landed and an object already reaped; 410 means it existed and expired; 503 means the bucket it was stored in cannot be opened.
POST /v1/artifacts/:id/share artifact.share write:artifacts Flip an artifact to public-link and hand back a URL. Audited and outward-facing: it notifies the account, and it refuses an artifact whose bytes are missing, because a share must produce a live link. 410 means it expired and will not be published; 503 means the bucket it was stored in cannot be opened.
DELETE /v1/artifacts/:id artifact.delete write:artifacts Delete the object and its row. Idempotent. 503 when the bucket it was stored in cannot be opened; the row is kept.

Sessions

Method Path Capability Scope Description
GET /v1/sessions session.list read:sessions Recent agent sessions, account-wide.
GET /v1/repos/:owner/:repo/sessions session.list_by_project read:sessions One repo’s sessions.
GET /v1/sessions/:id session.get read:sessions One session: repo, trigger, outcome, model.
GET /v1/sessions/:id/events session.events read:sessions SSE: one session’s audit events. Backfill, tail, terminate. Resumable: each frame’s id: is that row’s keyset cursor, so a reconnect carrying Last-Event-ID continues from it instead of replaying the session.
GET /v1/sessions/:id/events.jsonl session.events_export read:sessions The same events as a JSONL download, one event per line. An export cut short by the connection’s duration cap, or by a read failure after rows were sent, ends with a trailer line {"truncated":true,"next_after":"<cursor>"} instead of an event; send that cursor back as ?after= for the rest. A complete export has no trailer.
POST /v1/sessions/:id/replay session.replay read:sessions Dry-run a candidate policy against a finished session. Read-only: it changes nothing.
POST /v1/fleet/runs/:runId/message session.message write:tasks Send a message to a LIVE run and wait for the agent’s reply: an interactive session (scout, setup) or an autonomous run (coding, team, babysit or devops; not review), whose agent reads it at its next turn boundary. Addressed by the fleet run id, the same id GET /v1/fleet/runs/:runId/events streams, not by a session id. Without write:sessions, only a run whose task you dispatched: any other is a 403 with reason: not_run_dispatcher (user_required for a credential with no human). A run that holds no live channel is a 409. A 200 carries status: replied with the agent’s reply; or, with reply: null and a note, queued (the box holds the message for its next turn, do not resend), read (the agent read it and has not replied yet, do not resend), unconfirmed (no reply, and the box did not confirm it holds the message) or dropped (the run ended before reading it).

Audit log

Method Path Capability Scope Description
GET /v1/audit/events audit.tail read:sessions The account-wide audit feed, newest-first.

Org console

Method Path Capability Scope Description
GET /v1/terminal/events terminal.tail read:sessions The org console backfill: rendered audit lines, newest first, keyset-paged.
GET /v1/terminal/events/stream terminal.stream read:sessions SSE: the org console’s live tail. 1s poll, resumable via Last-Event-ID, recycles every 10 minutes.
GET /v1/terminal/status terminal.status read:sessions The org console’s status line: worker/key/queue health, from the same reads Fleet’s utilization card and the overview strip use.

Live streams

Method Path Capability Scope Description
GET /v1/streams stream.subscribe per request ONE SSE connection multiplexing several topics (?topics=tasks,ci,…), each patch frame upserting or removing one row by id. Every topic is authorized in-handler, so the opening ready frame names which were subscribed, ignored (unknown) and unauthorized; the request 403s only when nothing survives. Not resumable: it patches derived rows rather than appending, so it issues no id: and a reconnect takes a fresh snapshot.

Knowledge base

Method Path Capability Scope Description
GET /v1/kb/:repoId/pages kb.page_list read:kb One repo’s knowledge-base pages, the agent-maintained wiki.
GET /v1/kb/:repoId/search kb.search read:kb Trigram-rank the repo wiki (?q=, ?limit= 1..25). Each hit carries slug, score, cross-links and a preview excerpt; pull a full body with the page read.
GET /v1/kb/:repoId/pages/:slug{.+} kb.page_get read:kb One KB page verbatim by slug: body, cross-links, provenance.
PUT /v1/kb/:repoId/pages/:slug{.+} kb.page_upsert write:kb Create or replace one page by slug: a FULL replace, so an omitted links clears the page’s cross-links. Idempotent by (repo, slug). The repo must be enabled: writing into an offboarded repo’s wiki is a 409 naming the fix, not a silent no-op.

Dashboards

Method Path Capability Scope Description
POST /v1/dashboards dashboard.create write:dashboards Create a named board (201). A name already in use is a 409 naming the board it would have destroyed; replace: true is the only consent to rebuild one, and a seeded built-in is never replaceable.
GET /v1/dashboards dashboard.list read:dashboards The boards this caller may see: the account-shared ones plus their own personal boards.
GET /v1/dashboards/:id dashboard.get read:dashboards One board with all its widget specs.
PATCH /v1/dashboards/:id dashboard.update write:dashboards Rename a board and/or change its layout. Widgets are untouched.
DELETE /v1/dashboards/:id dashboard.delete write:dashboards Delete a board and cascade its widgets. An id this account does not own, or one already deleted, is a 404; a seeded built-in is a 409 (duplicate it to customize).
POST /v1/dashboards/:id/widgets/:wid/page dashboard.widget_page read:dashboards Page one widget’s data without re-running the whole board.

Digests

Method Path Capability Scope Description
GET /v1/digests digest.list read:digests Digest header rows, newest-first.
GET /v1/digests/:id digest.get read:digests One digest, with its rendered HTML.

Notifications

Method Path Capability Scope Description
GET /unsubscribe notification.unsubscribe_page no bearer The confirm page behind an email footer link, authorized by the signed token in that link. It NEVER mutates, because mail scanners and link prefetchers issue GETs.
POST /unsubscribe notification.unsubscribe no bearer Perform the opt-out, on the same signed token. Serves both the confirm form and RFC 8058 one-click, and changes ONE person's own settings, so escalations can never be silenced. 400 tampered, 410 expired, 413 a body over 16 KiB.
GET /v1/me/notification-preferences notification.prefs_list read:account Your whole notification settings page: the resolved kind × channel matrix with tiers and locked/available flags, your email cadence, the preset your overrides equal, and your delivery address.
PUT /v1/me/notification-preferences notification.prefs_replace write:self:notifications Replace your whole settings page in one atomic write: a preset, your email cadence (instant or hourly; daily is not served), and cell overrides on top. One locked or unavailable cell anywhere refuses the whole request with nothing written.
PUT /v1/me/notification-preferences/:kind/:channel notification.pref_set write:self:notifications Override one kind/channel pair.
DELETE /v1/me/notification-preferences/:kind/:channel notification.pref_clear write:self:notifications Clear an override, back to the default.
POST /v1/me/notification-test-email notification.test_email write:self:notifications Send one test email to your own address.
GET /v1/notifications notification.inbox_list read:account The in-app notification inbox.
POST /v1/notifications/:id/read notification.mark_read read:account Mark one notification read.
POST /v1/notifications/read-all notification.mark_all_read read:account Mark the whole inbox read.
GET /v1/me/notification-estimate notification.estimate read:account About how many emails a week your current settings mean, replayed from your own last 7 days, with the total each preset would give. truncated means the 7 days held more events than one read replays.
GET /v1/me/notification-deliveries notification.deliveries_list read:account Your own email notifications from the last 7 days, newest first, each with a why code saying why it was or was not sent.

Web push

Method Path Capability Scope Description
GET /v1/push/public-key push.public_key read:account The VAPID public key a browser needs before it can subscribe.
GET /v1/push/subscriptions push.subscriptions_list read:account The Web Push endpoints registered for this user.
POST /v1/push/subscriptions push.subscribe read:account Register a Web Push endpoint.
DELETE /v1/push/subscriptions push.unsubscribe read:account Forget one Web Push endpoint.

Email routes

Method Path Capability Scope Description
GET /v1/email-routes email_route.list read:account The resolved notification category → address matrix.
PUT /v1/email-routes/:category email_route.set write:account Override one category’s route.
DELETE /v1/email-routes/:category email_route.delete write:account Clear an override, back to the default.

Email bounce status

Method Path Capability Scope Description
GET /v1/me/email-status email_status.get read:account Whether notification email to the signed-in user's current address is suppressed after a hard bounce: bouncing, and when true the address and the ISO-8601 since.
POST /v1/me/email-status/retry email_status.retry read:account Lift the hard-bounce suppression on the signed-in user's current address so the next notification is mailed again. Idempotent: cleared is false when nothing was suppressed. A further bounce suppresses it anew.

Outbound webhooks

Method Path Capability Scope Description
GET /v1/webhooks webhook.list read:webhooks The outbound webhook subscriptions.
GET /v1/webhooks/deliveries webhook.deliveries read:webhooks The delivery ledger: what we sent and what answered, newest first. Narrow it to one endpoint with ?filter=webhook_id:<uuid> rather than paging the whole account.
POST /v1/webhooks webhook.create write:webhooks Subscribe a URL to a set of events. The signing secret is minted here and returned exactly ONCE; it is sealed at rest and never re-read.
DELETE /v1/webhooks/:id webhook.delete write:webhooks Delete a subscription.
POST /v1/webhooks/:id/test webhook.test write:webhooks Send a test delivery to one subscription.

Integrations

Method Path Capability Scope Description
GET /v1/integrations integration.list read:integrations The account’s connected integrations. Status only; credentials never cross the boundary.
POST /v1/integrations integration.connect write:integrations Connect a service by pasting its credential, sealed on write and never echoed.
DELETE /v1/integrations/:kind integration.disconnect write:integrations Disconnect a connector: drops the row and its vault secret.

Account secrets

Method Path Capability Scope Description
GET /v1/secrets secret.list read:secrets The account’s named secrets: names and timestamps only, never a value.
POST /v1/secrets secret.set write:secrets Store a named secret, sealed before anything else touches the value.
DELETE /v1/secrets/:name secret.delete write:secrets Delete one named secret. Only the dashboard: namespace is deletable here; an integration’s secret is refused at the edge.

Repository secrets

Method Path Capability Scope Description
GET /v1/repos/:owner/:repo/secrets project_secret.list read:secrets One repo’s stored secrets: name, exposure, environment and timestamps ONLY. A read never yields a value, because the ciphertext column is projected by exactly one query and this is not it.
PUT /v1/repos/:owner/:repo/secrets project_secret.set write:secrets Store one repo’s sealed .env for one environment, as a whole pasted .env (parsed server-side) or as explicit rows. environment is required and never defaulted, and a malformed line refuses the WHOLE paste naming only its line number.
DELETE /v1/repos/:owner/:repo/secrets/:name project_secret.delete write:secrets Delete one repo secret by name, optionally scoped to one environment.

System prompts

Method Path Capability Scope Description
GET /v1/system-prompts/org system_prompt.org_get write:prompts The org-wide system prompt.
PUT /v1/system-prompts/org system_prompt.org_set write:prompts Set it.
DELETE /v1/system-prompts/org system_prompt.org_clear write:prompts Clear it.
GET /v1/system-prompts/repo/:repoId system_prompt.project_get write:prompts One repo’s system prompt.
PUT /v1/system-prompts/repo/:repoId system_prompt.project_set write:prompts Set it; the repo layer wins over the org one.
DELETE /v1/system-prompts/repo/:repoId system_prompt.project_clear write:prompts Clear it.

LLM routing

Method Path Capability Scope Description
GET /v1/llm-routes llm_route.list read:billing The model-routing pins: the routing override each purpose carries (key_label, provider, model, any combination, plus its reasoning_effort), at org and repo scope.
GET /v1/llm-routes/preview llm_route.preview read:billing What each purpose resolves to right now, over the claim's own resolver code: one entry per task lane plus gardener, each with the pick (provider, model, key label, source pin|pool) a task arriving now would get, a wait (reason saturated|cooling|credits_exhausted, the key it waits on, an optional retry_at_iso) when the lane cannot start now (its selected key is full or cooling, or higher-ranked work holds every key), a fallback when a pin exists but something else serves (pin_unserved: the selection names no working key), or a blocker with stalls / box_env. Coding lanes also report their online template boxes. Read-only; cached 15 s per account and repo. 404 for a repo_id outside the account.
PUT /v1/llm-routes/org/:purpose llm_route.org_set write:billing Pin one org-wide routing override for one purpose: key_label, provider and model are each optional and at least one is required. Optional reasoning_effort (low|medium|high|xhigh|max) needs provider and model; 422 when that model cannot take it. Optional expect ({updated_at_iso} you read, or {absent: true}) makes the save conditional: 409 naming who saved first when the route changed.
DELETE /v1/llm-routes/org/:purpose llm_route.org_clear write:billing Clear the org pin for one purpose. Optional ?expect_updated_at= (the updated_at_iso you read) refuses a clear of a route that changed since, 409 naming who saved it.
PUT /v1/llm-routes/repo/:repoId/:purpose llm_route.project_set write:billing Pin one routing override for one purpose on one repo (same key_label / provider / model body as the org door); the repo pin wins over the org one. Optional reasoning_effort (low|medium|high|xhigh|max) needs provider and model; 422 when that model cannot take it. Optional expect makes the save conditional: 409 naming who saved first when the route changed.
DELETE /v1/llm-routes/repo/:repoId/:purpose llm_route.project_clear write:billing Clear the repo pin for one purpose. Optional ?expect_updated_at= refuses a clear of a route that changed since you read it, 409 naming who saved it.

Agent modes

Method Path Capability Scope Description
GET /v1/agent-modes agent_mode.org_list write:prompts The org's effective agent modes (scout, worker, reviewer, devops): enabled, the code_tools allowlist (null = the full ceiling), floor, and which row won (source org | default) at which revision.
PUT /v1/agent-modes agent_mode.org_set write:prompts Set the org override for one mode (mode_key): enabled (false holds its tasks until re-enabled) and an optional code_tools allowlist inside the mode ceiling. Replaces the row; 422 on a widening, a list below the floor, or tools on reviewer. Optional expect ({revision} you read, or {absent: true}) makes the save conditional: 409 naming who saved first when the override changed.
DELETE /v1/agent-modes/:modeKey agent_mode.org_clear write:prompts Reset one mode at org scope back to its code default (enabled, full ceiling). Optional ?expect_revision= refuses a reset of an override that changed since you read it, 409 naming who saved it.
GET /v1/agent-modes/repo/:repoId agent_mode.project_list write:prompts One repo's effective agent modes: the repo override beats the org one, which beats the default (source repo | org | default), with each mode's ceiling, floor and revision.
PUT /v1/agent-modes/repo/:repoId agent_mode.project_set write:prompts Set one repo's override for one mode (same body as the org door); it beats the org override. 422 on a widening, a list below the floor, or tools on reviewer. Optional expect makes the save conditional: 409 naming who saved first.
DELETE /v1/agent-modes/repo/:repoId/:modeKey agent_mode.project_clear write:prompts Reset one repo's override for one mode, so the org override (or default) applies. Optional ?expect_revision= refuses a reset of an override that changed since you read it, 409 naming who saved it.

Agents

Method Path Capability Scope Description
GET /v1/agents/activity agent.activity read:tasks Per platform agent (coder, reviewer, ci_fixer, planner, scout, devops, gardener, escalation, default): tasks running now, when it last ran, the last 7 days' worked and failed counts (cancelled counts neither way), and, on the agents a mode governs, whether that mode is switched on at org scope. null means the agent has no such record, never zero. Writes nothing.
GET /v1/agents/history agent.history read:billing Who last changed one agent's model pin or on/off switch, and its timeline: the agent's llm_route and agent_mode changes newest first (5 a page by default, cursor pages older), each with the member who made it (actor: id, login, avatar), the credential kind (via: session | pat), and for a pin the value before and after. A mode change carries only enabled, never its tool lists. Writes nothing.
GET /v1/agents/queue agent.queue read:tasks The pull requests and tasks one agent holds right now: the account's tasks in a live status whose origin is that agent's (for escalation, the stuck-PR rescue rounds), newest update first (25 a page by default, cursor pages older). Each row carries its state (queued: pending, with hold_reason and hold_until_iso when held; running: on a box, with runner_id and run_id; watching: its PR open under the babysit watch), the repo, the PR number and the title. For escalation the first page (no cursor) leads with its STUCK PRs, longest idle first and at most 100, outside the page size: open AI-managed PRs parked on what holds them, each stuck with park_class, park_reason, blocker, its idle clocks (idle_since_iso, idle_basis), last_action, rescue_due_at_iso and the newest escalation's delivery_state. An agent that runs no task of its own (planner, gardener, default) is refused with agent-has-no-queue. Writes nothing.

Billing

Method Path Capability Scope Description
POST /v1/billing/stripe/webhook billing.stripe_webhook no bearer Stripe’s own callback. No bearer: the raw body is HMAC-verified against the endpoint secret, so nothing else can call it. A body over 1 MiB is a 413, refused before the HMAC runs.
GET /v1/billing/keys billing.keys_list read:billing The ORG pool of BYOK keys, masked. The caller’s personal pool is /v1/me/keys.
GET /v1/billing/usage billing.usage read:billing Usage rollup for the current billing window, plus the agent-hours band: used/cap in HOURS and the UTC instant they reset. null means no ceiling applies.
GET /v1/billing/entitlements billing.entitlements read:billing The account’s tier entitlements.
POST /v1/billing/portal billing.portal write:billing Mint a Stripe billing-portal URL.
POST /v1/billing/checkout billing.checkout write:billing Buy seats: {tier, quantity} → a hosted Stripe Checkout URL. This is the FIRST subscription, which the portal cannot create; 503 when the tier’s price or Stripe is unconfigured. solo is one AI dev on a personal account, so an org account asking for it is a 409 billing-solo-requires-personal-account and a solo cart above one seat a 409 billing-solo-single-seat; both name the tier to buy instead.
PATCH /v1/billing/cap billing.set_cap write:billing Set the monthly spend cap.
POST /v1/billing/keys billing.key_add write:billing Add an org BYOK key, sealed and captured, never echoed.
DELETE /v1/billing/keys/:id billing.key_delete write:billing Delete an org BYOK key.
POST /v1/billing/keys/:id/verify billing.key_verify write:billing Verify an org key against its provider (live probe).
POST /v1/billing/keys/:id/rotate billing.key_rotate write:billing Replace an org key’s secret in place, keeping its id.
PATCH /v1/billing/keys/:id/kind billing.key_kind_set write:billing Re-stamp an org key’s billing nature (subscription ⇄ metered).
PATCH /v1/billing/keys/:id/priority billing.key_priority_set write:billing Set an org key’s pool priority: an integer in [0, 10000], LOWER is tried first (default 100).
POST /v1/billing/providers/models billing.provider_models write:billing List the models a provider key can address, before it is captured. The key rides in the body and is never stored; the answer is model ids, so a maintainer picks one instead of typing it.
GET /v1/billing/keys/:id/models billing.key_models read:billing The same list for an org key already in the pool.
GET /v1/billing/log-sink billing.log_sink_get read:billing The LLM-log sink config.
PUT /v1/billing/log-sink billing.log_sink_set write:billing Set it, validated and probed and sealed before it is stored.
DELETE /v1/billing/log-sink billing.log_sink_clear write:billing Clear it.
PATCH /v1/billing/keys/:id/capacity billing.key_capacity_set write:billing Declare an org key’s concurrency capacity: model streams it sustains at once, an integer in [1, 256]; null clears it (the default for its kind applies).
PATCH /v1/billing/keys/:id/label billing.key_label_set write:billing Rename an org key’s route-pin label (1-64 chars; null clears it). Every pin in the account naming the old label moves with it in one transaction. 409 label_taken when another key in the account holds the label, label_in_use while pending work names the old one, label_pinned when clearing a label a pin names.
GET /v1/key-outage billing.outage_get read:billing The account’s open key outage, or null. The dashboard banner and the status block read this same projection, and MCP reads it through the status key_outage block. outage is null when the account has no open outage, which is the empty projection the banner reads to hide itself.
POST /v1/key-outage/pick billing.outage_pick write:billing Pin every still-held task/ask of the account’s open outage onto the named key and close it picked. Body {key_label}. 200 with tasks/asks counts on picked; 404 no_open_outage when there is none; 404 unknown_key_label when no non-revoked ORG key carries that label; 422 key_not_usable (with unserved_steps) when the named key serves some but not every held step (a partial pick would break the always-pick-closes invariant); 409 outage_moved when the outage closed between this read and this write; 409 draining when the snapshot is empty and the outage will close drained on its own; 503 service_unavailable when the cooldown Redis read faulted (a pick blind to cooldowns would admit a cooling key).

Donations

Method Path Capability Scope Description
GET /v1/donations donation.list read:donations The caller’s own gifts, newest-first. A project gift carries target_repo_full_name, derived, because a donor funds repos they do not own.
GET /v1/donations/received donation.received read:donations The MAINTAINER’s inbox: gifts pinned to repos this account owns, each naming its giver by login (never an email) and, for an llm gift, the resolved ceiling the picker may offer. Beside the page envelope the body carries model_options, the platform model catalogue as { provider, model } objects, so a picker has a vocabulary without importing the server registry.
POST /v1/donations donation.create write:donations Give money | compute | llm, pinned to a project or to the pool, earmarking the resource oss-only. 402 without an active oss_donor subscription on a compute or llm gift; an optional models list is the ceiling the recipient picks within.
DELETE /v1/donations/:id donation.cancel write:donations Detach a gift: parks the row and clears the earmark. The DONOR’s end.
POST /v1/donations/:id/decline donation.decline write:donations Refuse a gift you were given: parks the row and releases the donor’s box or key. Authorized against the RECIPIENT, so the same id 404s for whichever side the caller is not.
PATCH /v1/donations/:id/model donation.set_model write:donations The maintainer’s model pick on a received llm gift, inside the donor’s ceiling (null clears it).

Entitlement grants

Method Path Capability Scope Description
GET /v1/admin/grants grant.list admin:read:orgs A subject’s grant history, newest-first. Revoked and expired rows are included, each with its resolved active (staff read).
POST /v1/admin/grants grant.issue admin:grants Operator only: give an account or a user free capacity, with a required reason and an optional expiry.
DELETE /v1/admin/grants/:id grant.revoke admin:grants Operator only: stop a grant. It PARKS the row rather than deleting it, so an already-stopped grant answers 200 revoked: false.
POST /v1/admin/provision grant.provision admin:grants Operator only: provision a friend in one call: invite them if they are new, approve them off the closed-beta waitlist, and grant the free devs. A deliberate re-run reads the live grants first and issues no duplicate, reporting which effects it caused. Send an Idempotency-Key header to make a retry safe: without one, two concurrent calls can both pass that read and both grant.

Onboarding

Method Path Capability Scope Description
GET /v1/onboarding/state onboarding.state read:account The resumable onboarding funnel state.
POST /v1/onboarding/advance onboarding.advance write:account Advance the funnel by one CALLER-witnessed event. A platform-witnessed one (key_set, gh_app_installed, yml_validated, yml_invalid) is 422 from every state: those are observations, and their subject does not assert them.

MCP

Method Path Capability Scope Description
POST /v1/mcp mcp.rpc per request The MCP server itself: one JSON-RPC endpoint for the whole verb catalog. Every verb asserts its OWN scope inside the server, which is also what buckets it for rate limiting. A body over 4 MiB is a 413.
GET /v1/mcp mcp.stream_refused per request Answers 405 + Allow: POST to the GET a Streamable HTTP client opens for the server-to-client SSE leg. This server is stateless single-request JSON-RPC and pushes nothing, and the transport spec requires exactly this status to say so. The official SDK client reads 405 as "POST-only, carry on" and treats every other status, a 404 included, as a fatal transport error.

Client error reports

Method Path Capability Scope Description
POST /v1/client-errors client_error.report no bearer Report a dashboard crash (a render failure, an uncaught error or an unhandled rejection). No bearer: a signed-in session only tags the report with its user. The body is capped at 32 KiB (413), each field has its own cap (400), and each IP gets 30 reports a minute, with a cap of 600 a minute across all callers (429). Answers 204.

Service

Method Path Capability Scope Description
GET / service.info no bearer Service card: name, status, docs URL. No bearer, and no account state.
GET /healthz service.health no bearer Readiness probe for the kubelet. No bearer; the DB and cache are probed with a timeout, 503 while either is down or the pod is draining, and the response names the running build's git sha (sha, null on an unreleased build).
GET /livez service.liveness no bearer Liveness probe for the kubelet. No bearer, and no dependency is consulted: it answers 200 whenever the process can serve, including while draining, so a Postgres or cache outage never restarts the pod.
GET /v1/openapi.json service.openapi no bearer This document: the OpenAPI 3.1 description of every /v1 capability, generated from the same catalog the routes are mounted from. No bearer, so a client can be generated before a token exists; per-IP rate limited, and cacheable.
GET /.well-known/oauth-protected-resource service.oauth_protected_resource no bearer OAuth 2.1 discovery (RFC 9728): names the MCP endpoint as a protected resource, which authorization server guards it, and the scope vocabulary a grant may carry. No bearer, since it is what a hosted client reads before it has one. The /v1/mcp suffixed twin is the URL the 401 challenge points at; both serve the same document.
GET /.well-known/oauth-protected-resource/v1/mcp service.oauth_protected_resource_mcp no bearer The same RFC 9728 document at the path-inserted URL the spec derives from the MCP endpoint (/.well-known/oauth-protected-resource + /v1/mcp). This is the URL named by WWW-Authenticate on a 401 from POST /v1/mcp.
GET /.well-known/oauth-authorization-server service.oauth_authorization_server no bearer OAuth 2.1 authorization-server metadata (RFC 8414): the authorize, token and dynamic client registration endpoints, plus the grant types and PKCE methods accepted. Authorization code with S256 only, and public clients with no secret.
GET /oauth/authorize service.oauth_authorize no bearer The consent screen. A browser navigation, not an API call: it resolves the dashboard session cookie, lists the exact scopes the grant would carry and the account it would be bound to, and sends a signed-out visitor to the dashboard sign-in with this request as the return target, so a finished sign-in lands back on this screen rather than on a page asking them to open a second tab. PKCE with S256 is required, and a redirect_uri that was not registered is refused on the page instead of redirected to.
POST /oauth/authorize service.oauth_consent no bearer The Authorize button's target. Takes one sealed consent ticket bound to the session that was shown the screen, re-checks the scopes against that session's live ceiling, and redirects to the registered redirect_uri with a single-use authorization code (or error=access_denied on Cancel).
POST /oauth/token service.oauth_token no bearer Exchange an authorization code plus its PKCE verifier for a token. The code is single-use and short-lived, and the result is an ordinary personal access token carrying the consented scopes, so it lists and revokes on the same Account tokens screen as one minted by hand.
POST /oauth/register service.oauth_register no bearer Dynamic client registration (RFC 7591): hand in a client name and its redirect targets, get back a client_id. Anonymous by design, because a hosted connector registers before any human has proved anything, and it stores nothing: the registration is sealed into the identifier. Public clients only, so no client_secret is ever issued and PKCE is the whole client authentication. Requested grant and response types are narrowed to what this server implements and reported back in the response. A body over 16 KiB is a 413.

Generated from REST_ROUTES, the route catalog the API mounts. Run bun run api:doc to regenerate.

Scopes

Scope Grants
read:account / write:account Account metadata + tokens
read:repos / write:repos Repos + policy + KB
read:issues / write:issues Issue triage + handoff
read:prs / write:prs PR review + approvals
read:tasks / write:tasks Fleet task queue
read:runners / write:runners Runner enrollment + revoke
read:sessions / write:sessions Audit log + interactive sessions
read:billing / write:billing Billing portal + spend cap
read:usage Token usage counters
read:digests Weekly digest data

Which scopes does my token hold?

Ask it. GET /v1/me answers a scopes array holding exactly what the credential you called with may do, so a client reads its own limits once instead of discovering them by collecting 403s. MCP callers get the same list from whoami.

That is not the same question as what a new token could carry: GET /v1/tokens answers mintable_scopes, which is your own list minus the classes a self-minted token may never hold, and empty when you lack write:account. Offer that one in a scope picker, and use /v1/me to decide which calls to make.

Error format

Every error is an RFC 9457 problem document, served as application/problem+json:

{
  "type": "https://developerz.ai/errors/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "repo not found",
  "instance": "/v1/repos/acme/widgets",
  "request_id": "4f0c2b8e-2b3a-4d1f-9a6e-6f6a0c2b8e4f"
}

Switch on type, never on title: the slug is a stable contract, the title is prose. detail explains this particular occurrence, instance is the path, and request_id is the correlation id described below. Some errors add extension members: errors carries flattened field errors on a validation failure, and retry_after carries seconds on a 429.

HTTP status codes follow REST conventions: 200 OK, 201 Created, 400 Bad Request (validation), 401 Unauthorized, 403 Forbidden (scope), 404 Not Found, 429 Too Many Requests, 500 Internal Server Error.

Request ids

Every response carries X-Request-Id, and the same value is the request_id member of any problem body. It is the id your call was logged under, so it is the one string worth quoting when something goes wrong. There is no answer without one: successes, refusals, the unauthenticated endpoints and a 404 for a path that matches no route all carry it.

Send your own and it is adopted verbatim:

curl -i https://api.developerz.ai/v1/repos \
  -H "Authorization: Bearer dev_pat_…" \
  -H "X-Request-Id: ci-run-4711"

A value that is not printable ASCII within 200 characters is not adopted, and one is minted instead. That is deliberately not a 400: this header is often stamped by a proxy you do not control, and a diagnostic must never fail the call it is diagnosing. Nothing is hidden by that, because the id actually used comes back on the same response. So the rule to build on is the narrow one: read the response header to learn which id was used. Log it next to your own call, and a support request becomes one lookup instead of a guess at a timestamp.

Rate limiting

Limits are per token or session, not per account: 600 reads a minute (READ_CAPACITY) and 600 writes a minute (TOKEN_WRITE_CAPACITY), with writes additionally fair-shared at 120 a minute per conversation (WRITE_CAPACITY), plus a 1200-per-minute per-IP bucket applied before your credential is even resolved (PREAUTH_CAPACITY). All four are on rate limits.

Every /v1 response the guard produces carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds, not a timestamp) for the bucket that request was charged against, so a client budgets itself instead of discovering the cap by being throttled. The two publicly cacheable /v1 documents that sit ahead of the guard, GET /v1/openapi.json and the public OSS stats badge, send no such headers: a per-caller counter behind a shared cache would be a wrong number for everyone reading it. Absent headers on those two are the contract, not a protocol failure. A 429 is an RFC 9457 problem document and adds retry-after, the same seconds as RateLimit-Reset. The full contract, including how MCP calls are classified, is on rate limits.

Do not poll a run at 1 Hz. That is 60 reads a minute on one run, a tenth of the read budget, and an agent run can last hours. GET /v1/tasks/:id/events takes ?wait_ms= (up to 25000) and holds the request open until the first new event lands, so watching a run costs well under 1% of your budget instead. The MCP task run_tail verb takes the same argument. See watching a run.

Idempotency

Every authenticated POST, PUT, PATCH and DELETE under /v1 accepts an Idempotency-Key header. Accepting it is not opt-in per route; what a keyed retry gets back is. Most calls replay the first answer, and a call that hands back a credential runs again (below). Send one on a call your client might retry (a phone app, a CI script, a flaky connection):

curl -X POST https://api.developerz.ai/v1/tasks \
  -H "Authorization: Bearer dev_pat_…" \
  -H "Idempotency-Key: 8e6a6c0e-1f2e-4b6a-9c3a-2e7a6c0e1f2e" \
  -H "Content-Type: application/json" \
  -d '{"title": "Fix flaky test"}'
  • The first request with a key runs once. A call that succeeds (a 2xx) is stored; a call that fails releases the key, so the retry runs it again. The one exception is a refusal that arrives after the call already created work (a plan that admitted some of its repos): it is stored too, so the retry replays it rather than creating that work twice.
  • A retry of the same request with the same key replays the stored response (X-Idempotent-Replayed: true) instead of running it again.
  • The same key on a different request is a 409 Conflict. A key is bound to the credential, method, path and body of the call it first ran, so reusing it for anything else is a client bug, not only a changed body.
  • Omit the header and the call goes through unconditionally, every time.

Two calls carrying the same key never run at once: the second waits for the first. If the first is still running when that wait runs out, the second answers a 409 with retry-after; send it again once that has passed. A key belongs to the credential that sent it, so another token or session presenting the same string never receives your stored response.

A call that answers a credential shown once never replays. Its response is never stored, so there is nothing to replay. The key still makes same-key calls take turns, but a retry runs the call again: it answers a fresh credential, or the call's own "already done" refusal, never the first answer. Minting a token, enrolling a runner or re-issuing its credentials, creating a webhook (its signing secret), an invitation link and a billing link all work this way.

The rule follows the outcome, not the door and not the status code. The MCP endpoint POST /v1/mcp takes the same header and behaves the same way, verb by verb: a keyed retry of a call that succeeded replays the stored result, so two identical keyed dispatches are one task, and a verb that answers a one-time credential runs again exactly as its REST route does. A failed MCP call arrives inside an HTTP 200, with the failure in the JSON-RPC body, and is still treated as a failure: its key is released and the retry runs, with the same one exception as above.

Treat those calls as non-replayable. When the outcome is unknown (a timeout, a dropped connection), look the resource up before you retry: GET /v1/tokens, GET /v1/webhooks, GET /v1/org/members/invitations, and GET /v1/runners for a box that has already joined. Retry only if it is not there. A blind retry can leave you holding a second token or webhook while the first one's secret is gone for good, so revoke the one you cannot account for.

SDK

The OpenAPI 3.1 document is published at https://api.developerz.ai/v1/openapi.json and is accessible without authentication. Generate a client in any language that supports OpenAPI tooling. For an agent, the MCP interface (/docs/mcp-verbs) is the better target because it carries schema validation and disclosures by design. A REST client is appropriate for scripts and CI workflows.