API errors
Every error code the Agent Authority management API returns, the HTTP status it arrives with, what causes it, and how to fix it.
Every failed request returns the same JSON body. Read code rather than the status, because the mapping is not one to one. Two codes share 409, and message is written for a person and may change.
{
"code": "bad_request",
"message": "request validation failed"
}A request that asks for text/html gets an HTML error page instead, which is how the browser-facing surfaces render a failure. A client sending the default Accept always receives JSON.
Error codes
code | Status | Cause | What to do |
|---|---|---|---|
bad_request | 400 | The request is malformed or fails schema validation. | Read validation_errors, which names the offending field. See below. |
unauthorized | 401 | You presented no usable credential. | Check the Authorization header and the key itself. See below. |
forbidden | 403 | You are authenticated but lack the permission. | The message names the missing permission. See below. |
not_found | 404 | The entity does not exist, or you cannot see it. | Confirm the identifier. An entity belonging to another organization also reads as absent. |
method_not_allowed | 405 | The path exists but not with that HTTP method. | Rarely seen: an unsupported method is caught during contract validation and returns 400 instead. |
already_exists | 409 | An entity with the same unique identifier is already present. | Read the existing entity instead of creating it, or choose a different name or slug. |
conflict | 409 | The request conflicts with the entity's current state. | Re-read the entity and retry against its current state. |
payload_too_large | 413 | The request body exceeds the size limit. | Send less in one call. For bulk changes, iterate rather than batching. |
unsupported_media_type | 415 | The request's media type is not supported. | Also rarely seen here: a wrong Content-Type fails contract validation and returns 400 instead. |
not_implemented | 501 | The operation is in the contract but not implemented yet. | Not a client fault. Treat the operation as unavailable. |
unknown_error | 500 | An unexpected server-side failure. | Safe to retry once. If it persists, contact SecureAuth. |
already_exists and conflict both arrive as 409, which is the clearest reason to branch on code rather than on the status.
When authentication fails
A 401 carries the WWW-Authenticate: Bearer header and one of two messages:
message | Meaning |
|---|---|
missing or unsupported credential | No Authorization header, or one the gateway does not recognize as a credential. |
invalid api key | The key was read but did not authenticate. |
invalid api key is deliberately the same answer for a key that never existed, one that was revoked, and one that expired. You cannot tell those apart. Check all three: confirm the key was copied whole, that nobody revoked it, and that its expiry has not passed. Rotating a key invalidates the old secret immediately, so a rotation someone else performed reads exactly like a bad key.
A 403 means the credential is good but does not hold the permission the operation requires. The message names it, for example permission denied: view_mcps. An API key carries the permissions it was created with, frozen at that moment. Granting yourself more access later does not widen an existing key. Rotating does not widen it either: a rotation replaces the secret and leaves the permissions alone. Create a new key with the permission and retire the old one. Each operation's page lists the permission it requires, and Authentication maps every permission to the endpoints behind it.
Validation failures
A bad_request carries a validation_errors array with one entry per offending field. Contract validation runs before authentication and before the operation runs, so these arrive whatever else is wrong with the call. Only the request timeout, the body size limit, and the member name check run ahead of it. An oversized body is therefore a 413 with no validation_errors at all.
Body fields
For a field in the request body, field is a JSON Pointer, so a nested field reads as /scope/mcp_ids/0. rule names the schema keyword that failed, such as required, minLength, or pattern.
{
"code": "bad_request",
"message": "request validation failed",
"validation_errors": [
{
"field": "/tool_patterns",
"message": "property \"tool_patterns\" is missing",
"rule": "required"
},
{
"field": "/effect",
"message": "property \"effect\" is missing",
"rule": "required"
}
]
}Member names
The API returns 400 for a JSON body with an ambiguous member name (a key in a JSON object). A name is ambiguous when it repeats in one object. It is also ambiguous when it differs from a schema field only in letter case, such as TYPE for type. JSON parsers can disagree on such a body, so the API refuses it instead of guessing. This check runs before contract validation. The single validation_errors entry points field at the offending member.
Query and path parameters
For a parameter, field is not a JSON Pointer. It is the parameter's location and name joined by a slash, such as query/limit or path/mcp_id:
{
"code": "bad_request",
"message": "request validation failed",
"validation_errors": [
{
"field": "query/limit",
"message": "number must be at most 256",
"rule": "maximum"
}
]
}rule is present when the value broke a schema constraint. It is absent when the gateway could not read the value at all, which covers three cases:
| Problem | message |
|---|---|
| The value is the wrong type | an invalid integer |
| A required parameter is missing | value is required but missing |
| The server cannot parse an identifier | value has an invalid format |
Whole-request failures
Some failures are about the request rather than one of its fields, and those carry an empty field. An unknown path, an unsupported method, and a Content-Type the operation does not accept all report this way.
Branch on field's shape if you present these to a user. A leading / means a body pointer. query/ or path/ means a parameter. An empty string means the entry describes the whole request, so show its message on its own.
Validation happens first
Contract validation runs ahead of the credential check. A request that violates the schema returns 400 even when its key is invalid or absent. A 400 therefore tells you nothing about whether your credential is good, and a request with two problems reports the schema one first.
Two consequences worth knowing. The router rejects an unknown path and an unsupported method before any handler runs. An unknown path therefore returns 400 rather than 404, and an unsupported method returns 400 rather than 405.
The top-level message stays the usual request validation failed. The specific reason, no matching operation was found or method not allowed, is the message of the single validation_errors entry.
Retrying safely
GET operations are side-effect free and always safe to retry. The API has no idempotency keys, so a retried POST that timed out in flight may create a second entity. Where that matters, re-read the collection before retrying rather than assuming the first attempt failed.
Next steps
- Authentication: the cause of most
401and403responses. - Conventions: the parameter limits behind many
400responses.