Skip to content

Docs / REST API

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, /actions/:jwt, /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.

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 write: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/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.
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/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. Every call 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. Every call 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.

Personal settings

Method Path Capability Scope Description
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 (subscriptionmetered).
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/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).

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.

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.

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 it (OIDC or SAML). 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 backer counts for ONE enabled, public, donations-opted repo. No bearer. Cached, but the eligibility gate is re-run on every positive hit, so a repo that goes private or opts out drops out immediately.
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 two owner switches, enabled and donations_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 stored .maintainer.yml policy snapshot for one enabled repo.
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.

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 /actions/:jwt pr.action_link_confirm no bearer Show the confirmation page for a signed approve/skip link an ask-before-acting email carries. Verifies the link and applies nothing, so a mail scanner or link prefetcher cannot spend it: 400 malformed, 410 expired.
POST /actions/:jwt pr.action_link_redeem no bearer Apply the decision a signed approve/skip link carries. The signed JWT in the path is the whole credential and is also the CSRF token, and the link is single-use: 400 malformed, 410 expired, 409 already redeemed.
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.
POST /v1/prs/:id/skip-pending pr.skip_pending write:prs Decline the PR’s outstanding ask-before-acting action.

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.

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 or terminal task answers reassigned: false.
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.
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.
POST /v1/tasks task.create write:tasks Dispatch one scoped task to a sink (linear | gh | direct) → {task_id, sink, url}, an escalation, or queue_full.

Blockers

Method Path Capability Scope Description
GET /blocker-fix/:jwt blocker.fix_link_form no bearer Show the prefilled secret form a missing_credential blocker hands a human: the exact env-var names the blocked work needs, ready to fill in. Verifies the signed link and stores nothing, so a mail scanner or link prefetcher cannot spend it: 400 malformed, 410 expired. Needs no login: the signed JWT in the path is the whole credential.
POST /blocker-fix/:jwt blocker.fix_link_redeem no bearer Store the supplied values into the repo’s secret vault and close the blocker, which puts the work back into triage. The signed JWT in the path is the whole credential and is also the CSRF token; the link is single-use, expires within the hour, may write into exactly one (repo, environment) and can never read a stored value back. 400 an incomplete form, 409 already redeemed, 410 expired.
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 the env-var NAMES it needs and, when one can be minted, a signed single-use fix_url a human can fill in without the agent plane ever holding the value. 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. 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.

Plans

Method Path Capability Scope Description
GET /v1/plans plan.list read:tasks The account’s plans, newest-activity-first: plan id, target repo, lead-slice title and the merged/total progress fraction.
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; that task’s run_id is null until a box claims and starts it, and it is the id the mailbox steers. 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/:planId plan.get read:tasks One plan’s full projection: the group → task → PR chain.
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/: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. Always 200 when the plan exists: 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.

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 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: its credential stops working. 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). 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/creds/reissue box.reissue_creds write:runners Operator-initiated credential re-issue for a healthy box (#2547): 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: shared (the pool), 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.
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. 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, and 503 means the deployment has no bucket to write to.
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.
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.
DELETE /v1/artifacts/:id artifact.delete write:artifacts Delete the object and its row. Idempotent.

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.
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:sessions Send a message to a LIVE interactive run (a scout or a setup session) and wait for the agent’s reply. Addressed by the fleet run id, the same id GET /v1/fleet/runs/:runId/events streams, not by a session id. A run that holds no live channel is a 409; a delivered message the agent did not answer in time is a 200 carrying delivered: true, reply: null, because the guidance did land.

Audit log

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

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 account-shared 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 (idempotent). A seeded built-in is refused.
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 is scoped to ONE category, so escalations can never be silenced. 400 tampered, 410 expired.
GET /v1/me/notification-preferences notification.prefs_list read:account The resolved kind × channel notification matrix.
PUT /v1/me/notification-preferences/:kind/:channel notification.pref_set write:account Override one kind/channel pair.
DELETE /v1/me/notification-preferences/:kind/:channel notification.pref_clear write:account Clear an override, back to the default.
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.

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.

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: which BYOK key serves which purpose, at org and repo scope.
PUT /v1/llm-routes/org/:purpose llm_route.org_set write:billing Pin an org-wide key for one routing purpose.
DELETE /v1/llm-routes/org/:purpose llm_route.org_clear write:billing Clear the org pin for one purpose.
PUT /v1/llm-routes/repo/:repoId/:purpose llm_route.project_set write:billing Pin a key for one purpose on one repo; the repo pin wins over the org one.
DELETE /v1/llm-routes/repo/:repoId/:purpose llm_route.project_clear write:billing Clear the repo pin for one purpose.

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.
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.
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 (subscriptionmetered).
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.

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.
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.

Onboarding

Method Path Capability Scope Description
GET /v1/onboarding/state onboarding.state per request The resumable onboarding funnel state.
POST /v1/onboarding/advance onboarding.advance per request 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.
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.

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 Liveness/readiness probe for the kubelet. No bearer; the DB and cache are probed with a timeout.
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.

Generated from REST_ROUTES (apps/api/src/catalog.ts). 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: 60 reads and 10 writes per minute, plus a 300-per-minute per-IP bucket applied before your credential is even resolved.

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. Sixty reads a minute IS 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 about 4% of your budget instead of all of it. The MCP task run_tail verb takes the same argument. See watching a run.

Idempotency

Every POST, PUT, PATCH and DELETE under /v1 accepts an Idempotency-Key header, uniformly across every write endpoint, not opt-in per route. 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 and caches the response.
  • A retry with the same key and body replays that cached response (X-Idempotent-Replayed: true) instead of running it again.
  • The same key with a different body is a 409 Conflict, a client bug.
  • Omit the header and the call goes through unconditionally, every time.

Keys are scoped to the caller, so two tokens sharing the same string never alias each other's responses. Routes that hand back a one-time secret (minting a token, enrolling a runner) never cache their response, so a retry always mints a fresh credential rather than replaying the first one.

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.