HTTP API v1
Programs holding a Dev Bench API key can read your issues, error counts, trends, release impact, affected users and logs, and on V2 resolve, dismiss, reopen and comment on issues. An owner creates keys on the account page. AI assistants can use the same operations as tools through the MCP server.
Host and transport
https://api.developerbench.com./v1/…is the HTTP API;/mcpis the MCP endpoint. Nothing else is served on this host.- HTTPS only. JSON in and out (
Content-Type: application/json); request bodies with unknown fields are 400. Every response isCache-Control: no-store. - No cookies, no CORS headers: callers are programs holding a key, not
browsers. The cookie-session routes on
developerbench.com/api/…are unchanged and do not accept API keys. - All times are UTC, RFC 3339 (
2026-10-08T14:03:11Z); dates areYYYY-MM-DDUTC days. - While the API is switched off for maintenance, every request answers
503
{"error":"The Dev Bench API is temporarily disabled."}.
Authentication: API keys
Authorization: Bearer dbk_5n3k7q2x…- Format:
dbk_+ 32 random bytes in lowercase base32, unpadded (dbk_+ 52 characters). Shown once, at creation. Only its SHA-256 is stored, plus the first 8 characters (dbk_5n3k) for display. - Sent only in the
Authorizationheader. A key in a query string is never read (the request is 401 as if no key were sent). - A key belongs to one tenant, not to a person: it keeps working after the account that created it leaves. Every audited action is recorded against the key's id and name.
- Keys are created, listed and revoked by tenant owners on the account page.
last_used_atis updated at most once a minute per key.
Scopes
| Scope | Grants | Edition |
|---|---|---|
issues:read |
list/read issues, triage text, GitHub links | V1+ |
sensors:read |
fingerprints, counts, trends, release impact | V1+ |
users:read |
affected users' emails / account ids | V1+ |
logs:read |
log slices and evidence bundles | V1+ |
issues:write |
resolve, dismiss, reopen, comment | V2 |
prs:write |
reserved. Review and merge of builder PRs by the tenant's own agents. Its routes and tools are not available yet; until then the scope cannot be granted | V2 |
- Default scopes at creation:
issues:read,sensors:read. - Editions come from the tenant's
edition:v1→ V1;v2andpilot(operator pilots and engagements) → V2. - A V1 tenant cannot create a key with a V2 scope (403 at creation). A key that holds a V2 scope while its tenant is on V1 (the tenant moved from V2 to V1) keeps the scope on record but every V2 route answers 403 and the V2 tools are not listed; they work again if the tenant returns to V2.
Entitlement (billing)
A billed tenant that is not entitled gets 402 on every route except GET /v1/me, and every MCP
tools/call returns the same error. Operator tenants (not billed) are
always entitled.
Errors
Every error body is {"error": "<a sentence a person can act on>"}.
| Status | When |
|---|---|
| 400 | malformed JSON, unknown body field, invalid query parameter or value (the message names it) |
| 401 | no key, malformed key, unknown key, revoked key. Always {"error":"Invalid or revoked API key."}: the response does not say which |
| 402 | billed tenant not entitled ({"error":"Billing for this account has lapsed; an owner can fix it at https://developerbench.com/app/<slug>."}) |
| 403 | the key lacks the route's scope, or the scope needs V2 and the tenant is on V1. The message names the missing scope or edition |
| 404 | no such resource or one belonging to another tenant: the two are indistinguishable, byte for byte |
| 409 | a write on an issue that was never filed to GitHub, or the tenant has no usable GitHub credential |
| 429 | rate limited; Retry-After: <seconds> |
| 502 | GitHub failed or refused a write. Nothing was changed on our side |
| 503 | API disabled |
Order of checks: key (401) → disabled (503) → entitlement (402) → scope and edition (403) → rate limit (429) → the route itself (400/404/409/…).
Rate limits
Per key, sliding one-minute windows:
- 120 requests/minute (all routes and MCP calls, writes included);
- 30 writes/minute (the four
POSTroutes and their tools), on top.
Over either limit: 429 with Retry-After. Each MCP tools/call counts as
one request (a write tool also as one write); initialize, tools/list
and notifications do not count.
Pagination
List routes take limit (1–100, default 25; outside that range is 400) and
cursor (opaque; from the previous response, with the same filters).
They answer:
{"data": [ … ], "next_cursor": "eyJs…" }next_cursor is null on the last page. A cursor from a different route
or different filters is 400.
Counts and time windows
Counts come from per-day buckets (UTC days), so windows are whole days:
last_24h: yesterday and today (UTC), so it covers at least the last 24 hours;last_7d/7d: today and the 7 days before it;30d: today and the 30 days before it;month: the current calendar month (UTC) to date.
Responses that use a window say which days it covered (from, to).
Auditing
These append an api_audit row (tenant, key, action, target, time),
retained while the tenant exists and deleted with it:
| Action | Target |
|---|---|
issue.resolve, issue.dismiss, issue.reopen, issue.comment |
issue:<id> |
users.lookup |
email or account: which kind of lookup, not the value, so deleting a person's data leaves no copy of the address behind |
issue.users, issue.logs, issue.evidence |
issue:<id> |
Reads of other scopes are not audited. A request that fails before the route runs (401–429) is not audited.
Objects
Issue
An issue is a Dev Bench problem (a cluster: one root cause, however many
fingerprints report it). id is ours; github is the GitHub issue it was
filed as, or null while it has not been filed (still triaging, or held
for a human).
{
"id": 412,
"title": "Checkout fails for carts with a gift card",
"summary": "Customers paying with a gift card see \"Something went wrong\" …",
"kind": "error",
"state": "open",
"stage": "needs_human",
"impact": 37,
"confidence": 0.82,
"environments": ["production", "staging"],
"counts": {"total": 1893, "last_24h": 41, "last_7d": 388},
"first_seen_at": "2026-09-30T08:12:40Z",
"last_seen_at": "2026-10-08T13:58:02Z",
"regressions": 1,
"github": {"number": 87, "url": "https://github.com/acme/devbench-issues/issues/87"}
}kind:error,silent_failure,frustration,degradation,log_template,handled_failure.state:open,resolved(closed as done),dismissed(closed as not planned). This is GitHub's state as last synced or written through this API.stage: where the work is:triaging,needs_human,ready_to_fix,fixing,in_review,ready_to_merge,gave_up,resolved,noise. New values may be added; treat unknown ones asopen.summary: the first paragraph of the triage text (≤ 500 characters).impact: triage's estimate of affected users.confidence: triage's confidence in the root cause, 0–1.
Issue detail
The issue, plus:
{
"diagnosis": "Customers paying with a gift card …\n\n**Root cause**\n\nThe gift-card branch …",
"implicated": [
{"repo": "acme/shop", "paths": ["app/checkout/payments.rb"], "confidence": 0.8}
],
"fingerprints": [
{"id": 9031, "kind": "error", "source": "server", "environment": "production",
"total": 1702, "first_seen_at": "2026-09-30T08:12:40Z", "last_seen_at": "2026-10-08T13:58:02Z",
"first_release": "2026.09.30-1", "last_release": "2026.10.07-2"}
],
"affected_users": {"emails": 29, "accounts": 31, "unlisted": 0}
}diagnosis: the full triage text (Markdown, root cause included).affected_users: distinct identities sensors listed;unlistedis how many more they saw but did not list, so the true count is at least the larger ofemails/accountsand may be up to that plusunlisted.
Routes
GET /v1/me (any key, works while billing has lapsed)
{
"tenant": {"slug": "acme", "name": "Acme Shop", "edition": "v2"},
"key": {"id": 7, "name": "Sam's Claude", "prefix": "dbk_5n3k",
"scopes": ["issues:read", "sensors:read", "issues:write"]},
"usable_scopes": ["issues:read", "sensors:read", "issues:write"],
"entitled": true
}usable_scopes is scopes minus what the edition does not allow.
GET /v1/issues (issues:read)
Query: state (open default, resolved, dismissed, all),
environment (issues with any fingerprint in it), kind, since (RFC
3339: last seen at or after), limit, cursor. Newest last_seen_at
first.
{"data": [ <Issue>, … ], "next_cursor": null}GET /v1/issues/{id} (issues:read)
The issue detail. 404 if unknown or another tenant's.
GET /v1/errors/top (sensors:read)
Query: period (24h default, 7d, 30d, month), environment,
kind, limit, cursor. Fingerprints ranked by their count in the
period; ties by most recent last_seen_at. Fingerprints with no count in
the period are left out.
{
"period": "7d", "from": "2026-10-01", "to": "2026-10-08",
"data": [
{"fingerprint_id": 9031, "issue_id": 412, "title": "Checkout fails for carts with a gift card",
"kind": "error", "environment": "production", "count": 371,
"last_seen_at": "2026-10-08T13:58:02Z"}
],
"next_cursor": null
}issue_id is null for a fingerprint not yet triaged into an issue;
title is then the fingerprint's own sample message (≤ 200 characters).
GET /v1/fingerprints/{id}/trend (sensors:read)
Query: days (1–90, default 30). One entry per UTC day, oldest first,
zero-filled, ending today.
{"fingerprint_id": 9031, "issue_id": 412, "environment": "production",
"days": [{"day": "2026-10-07", "count": 52}, {"day": "2026-10-08", "count": 41}]}GET /v1/releases/{release}/impact (sensors:read)
{release} is the release string as the SDK reported it, URL-escaped.
Query: environment (default production). 404 if no sensor in that
environment has reported the release.
{
"release": "2026.10.07-2", "environment": "production",
"first_seen_at": "2026-10-07T16:20:00Z",
"new": [
{"fingerprint_id": 9120, "issue_id": 430, "title": "…", "kind": "error",
"first_seen_at": "2026-10-07T16:41:09Z", "total": 64}
],
"worse": [
{"fingerprint_id": 9031, "issue_id": 412, "title": "…", "kind": "error",
"per_day_before": 6.1, "per_day_after": 31.5}
]
}first_seen_at: when the release was first reported in that environment.new: fingerprints whose first sighting was in this release.worse: older fingerprints whose average daily count over the days from the release's first day (up to 7, including today) is at least 2× the average over the 7 days before it, and higher by at least 5 a day.- Each list holds at most 100 entries, largest effect first (
newbytotal,worsebyper_day_after − per_day_before); not paginated.
GET /v1/users?email= or ?account= (users:read, audited)
Exactly one of the two (400 otherwise). Email matching is case-insensitive. One row per problem the person was affected by, most recent first. Paginated.
{"data": [
{"email": "pat@example.com", "account": "acct_831", "environment": "production",
"kind": "error", "reports": 4, "first_seen_at": "…", "last_seen_at": "…",
"issue_id": 412, "title": "Checkout fails for carts with a gift card",
"state": "open", "issue_url": "https://github.com/acme/devbench-issues/issues/87"}
], "next_cursor": null}issue_id, title, state and issue_url are null when the
fingerprint is not in an issue yet.
GET /v1/issues/{id}/users (users:read, audited)
The issue's affected users, one row per identity across its fingerprints, most recent first. Paginated.
{"data": [
{"email": "pat@example.com", "account": "acct_831", "environment": "production",
"reports": 4, "first_seen_at": "…", "last_seen_at": "…"}
], "unlisted": 0, "next_cursor": null}Either email or account may be "", never both.
GET /v1/issues/{id}/logs (logs:read, audited)
The issue's answered log slices (already redacted by the sidecar and ingest), newest first. Paginated (by slice).
{"data": [
{"fingerprint_id": 9031, "trace_key": "4bf92f3577b34da6", "status": "delivered",
"lines": ["2026-10-08T13:58:01Z INFO POST /checkout …", "…"],
"truncated": false, "answered_at": "2026-10-08T13:58:30Z"}
], "next_cursor": null}status is delivered or empty (the sidecar held no line for that
trace, which itself says the request may not have reached the server).
Pending and expired requests are not listed.
GET /v1/issues/{id}/evidence (logs:read, audited)
Download links for the issue's uploaded evidence bundles (at most one per fingerprint). Not paginated.
{"data": [
{"fingerprint_id": 9031, "uploaded_at": "2026-10-08T14:00:12Z",
"url": "https://…", "expires_at": "2026-10-08T14:05:12Z"}
]}url is a pre-signed GET valid for 5 minutes; ask again for a fresh
one. An empty data means no bundle has been uploaded.
Writes (issues:write, V2; audited; 30/minute)
Writes go through GitHub. Each uses the tenant's GitHub credential (the App installation token, or the PAT of an older pilot) on the issue's GitHub issue; when GitHub accepts it, our issue is updated at once, so an immediate read agrees. The regular issue sync remains the authority.
- 409
{"error":"This issue has not been filed to GitHub yet."}whengithubisnull; 409 also when the tenant has no usable GitHub credential. - 502 when GitHub fails or refuses: nothing changed on our side, nothing audited.
- Idempotent: a write whose effect is already true (resolving a
resolved issue, reopening an open one) makes no GitHub call, answers 200
with
"changed": false, and is still audited. - Every comment Dev Bench posts names the key:
<text>\n\n— via the Dev Bench API, key "Sam's Claude". - Text limits:
comment,reason,body≤ 60,000 characters;fixed_in≤ 255. Empty or whitespace-only required text is 400.
All four answer 200 {"issue": <Issue>, "changed": true|false}.
| Route | Body | On GitHub | Our state after |
|---|---|---|---|
POST /v1/issues/{id}/resolve |
{"comment"?: "…", "fixed_in"?: "<release>"} |
comment (with "Fixed in <release>." when given), then close with state_reason: completed |
resolved / stage resolved |
POST /v1/issues/{id}/dismiss |
{"reason": "…"} (required) |
comment with the reason, then close with state_reason: not_planned |
dismissed / stage noise |
POST /v1/issues/{id}/reopen |
{"comment"?: "…"} |
reopen, then comment if given | open / stage needs_human (from resolved or dismissed) |
POST /v1/issues/{id}/comments |
{"body": "…"} (required) |
comment | unchanged; always changed: true |
Reopen sets the stage itself rather than waiting for the sync because the
sync never revisits a dismissed issue (it would otherwise stay noise
forever). A reopened issue then follows the normal rules: a person or the
builder decides what happens next.
Generated from the API contract when the site is built.