Skip to main content

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"
}
}
FieldMeaning
messageHuman-readable description. Do not parse it
typeBroad class: invalid_request_error, authentication_error, permission_error, conflict_error, rate_limit_error, server_error
codeStable machine-readable code, listed below
paramThe request field at fault, in bracket form (form_data[subject_dob]), or null
request_idThis 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_exceeded and server_error, with backoff.
  • Never retry a 4xx unchanged. It will fail identically.
  • Log request_id on every failure. It is the one thing support can act on.
  • Treat an unknown code as fatal for that call rather than as success.