Errors & Responses
Every public API endpoint returns predictable response shapes. Errors carry a stable machine-readable code in the body so you can branch on it without parsing English.
Response envelope
Successful responses return the resource (or a list of resources) directly. There is no top-level wrapper — clients can deserialize straight into a typed model:
GET /api/public/v1/org
{
"id": "e188fc87-…",
"name": "Acme Corp",
"plan": "growth",
…
}Failure responses use a small uniform shape with an error code as the only required field. Optional fields give context that helps you fix the call:
{
"error": "insufficient_scope",
"required": "assignments:write",
"present": ["assignments:read", "users:read"]
}Status code reference
| Status | When you see it | What to do |
|---|---|---|
200 | Request succeeded. Body holds the resource or a confirmation message. | Process the body. |
400 | Malformed request: missing required field, invalid enum value, body too large, unparsable JSON. | Fix the client request — do not retry the same payload. |
401 | Missing, malformed, revoked, or expired token. | Verify the Authorization header. Rotate the token if it has been revoked. |
403 | Token is valid but lacks the required scope. | Add the scope in the admin UI; the response carries the exact scope name. |
404 | Resource doesn't exist or belongs to a different organization. | Re-fetch the parent collection. Don't follow stale IDs across orgs. |
409 | Conflict — e.g. an email is already in use. | Treat as success if your operation is idempotent. |
413 | Request body exceeds the per-endpoint cap. Returned by Kestrel for hard overflow; the SARIF endpoint also returns this as 400 body_too_large when the declared Content-Length exceeds 10 MB. | Trim the payload. For SARIF, increment-ingest one tool's findings at a time. |
429 | Rate limit hit on this API key. | Honor Retry-After; see Rate Limits. |
5xx | Server-side problem. | Retry with exponential backoff. Capture the response X-Request-Id header and quote it in any support ticket. |
Error codes
The error field is stable across versions — branch on it in client code rather than on the HTTP status, since the same status can carry different codes:
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | No usable bearer token on the request. |
insufficient_scope | 403 | Token is fine, scope is missing. The response includes required and present. |
rate_limited | 429 | Cap exceeded. See Retry-After header. |
user_not_found | 404 | User ID doesn't exist or isn't in your organization. |
team_not_found | 404 | Team ID doesn't exist or isn't in your organization. |
assignment_not_found | 404 | Assignment ID doesn't exist or isn't in your organization. |
empty_body | 400 | POST or PATCH sent with no body where one is required. |
body_too_large | 400 / 413 | Request body exceeded the endpoint cap. SARIF emits this as 400 with maxBytes in the body when the declared Content-Length is too large; Kestrel emits a bare 413 when the body actually overruns. |
Request IDs
Every response carries an X-Request-Id header (UUID v4). Capture it client-side and include it in support requests — it lets us pull the exact server-side trace for the failing call. For 5xx responses, the request ID is the single fastest path to diagnosis.
Validation errors
Validation failures on POST and PATCH bodies return 400 with a flat error describing the first failing rule. Multi-field validation surfacing is not part of v1 — fix the first reported field and retry. Future versions may add per-field detail; the top-level error contract will remain.
Branching on errors — example
const res = await fetch(url, { headers });
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
switch (body.error) {
case 'rate_limited':
return retryWithBackoff(url);
case 'insufficient_scope':
throw new Error(`API key missing scope: ${body.required}`);
case 'unauthorized':
throw new Error('API key revoked or invalid — rotate the secret');
default:
throw new Error(`API error ${res.status}: ${body.error || 'unknown'}`);
}