Errors
Every error returns the same envelope. code is stable and safe to branch on;
message is written for a person and may be reworded at any time.
{
"error": {
"message": "Warrant application not found.",
"type": "invalid_request_error",
"code": "not_found",
"param": null,
"request_id": "req_01HZY5R2K3Q9"
}
}
| Field | Meaning |
|---|---|
message | Human-readable description. Do not parse it |
type | Broad class: invalid_request_error, authentication_error, permission_error, conflict_error, rate_limit_error, server_error |
code | Stable machine-readable code, listed below |
param | The request field at fault, in bracket form (form_data[subject_dob]), or null |
request_id | This request's identifier, also in the X-Request-ID header |
Registry
Authentication
missing_api_key
401. No x-api-key header. Send your
API key on every authenticated call.
invalid_api_key
401. The key is unknown, expired, or revoked. Do not retry with the same key;
it will not start working. Check that you are sending the key for the right
environment and agency.
Permission
insufficient_scope
403. The key is valid but does not carry the
scope this operation requires. Ask for a key with
the scope, rather than routing the call through a broader key.
insufficient_permission
403. The key has the scope, but the person named in X-On-Behalf-Of may not
take this action in this agency. Signing without signing rights is the common
case. See permissions and scopes.
missing_actor
422. A write arrived without X-On-Behalf-Of. Every write must name the
person it is made for: actor attribution.
unknown_actor
403. X-On-Behalf-Of names somebody who is not an active member of this
agency. Check for a typo, a deactivated member, or a key pointed at the wrong
agency.
Request
invalid_request
400. The request was malformed: bad JSON, an unparseable identifier, or a
query parameter outside its allowed values. param names the field when the
API can tell.
validation_failed
422. The body was well-formed but failed validation. param points at the
field, including inside form_data:
{
"error": {
"message": "physician_license is required by this template.",
"type": "invalid_request_error",
"code": "validation_failed",
"param": "form_data[physician_license]",
"request_id": "req_01HZY5R2K3Q9"
}
}
Read the template's elements and validate before sending, so a person is told about a missing field while they are still looking at it.
not_found
404. No such resource in this agency. It is also what you get for a resource
that exists in a different agency: the API does not distinguish, because
confirming existence across agencies would itself be a leak.
already_exists
409. A record with the same unique value already exists, such as a member
with that email address.
Workflow
invalid_transition
409. The application is not in a state that allows the transition: signing
something that is not SUBMITTED, submitting something that is not DRAFT,
reopening something that is not REJECTED, or signing a warrant twice. The
lifecycle has the full table.
warrant_locked
409. The application is SIGNED or REJECTED and is immutable. A signed
warrant is a court record; correct a rejected one by
reopening it first.
lock_held
409. Another caller holds the edit lock. The body names the
holder so your client can say who is editing. Retry when they release it, or
after their lock goes stale.
Limits and failures
rate_limit_exceeded
429. Too many requests. The Retry-After header says how long to wait. See
rate limits.
server_error
500 or 503. Something failed on our side. Retry idempotent calls with
backoff, and quote request_id if it persists: it maps to the exact
server-side record of the call.
Handling errors
# Example client: branch on code, never on message.
def handle(response):
if response.ok:
return response.json()
error = response.json()["error"]
code = error["code"]
if code in ("missing_api_key", "invalid_api_key"):
raise ConfigurationError(error["message"])
if code == "rate_limit_exceeded":
sleep(int(response.headers.get("Retry-After", "30")))
return retry()
if code in ("validation_failed", "invalid_request"):
raise UserFacingError(error["message"], field=error.get("param"))
if code == "lock_held":
return show_who_is_editing(error["message"])
if code == "server_error":
return retry_with_backoff()
raise ApiError(code, error["message"], error.get("request_id"))
Rules that keep a client sane:
- Branch on
code. Messages are prose and will change. - Retry only
rate_limit_exceededandserver_error, with backoff. - Never retry a
4xxunchanged. It will fail identically. - Log
request_idon every failure. It is the one thing support can act on. - Treat an unknown
codeas fatal for that call rather than as success.