Skip to content

Pre-release. v0.1 is not out yet, so there is nothing to install and no public source to clone — the quickstart builds from a checkout.

Errors

Two error shapes, and which one you get depends on the endpoint rather than the failure.

The token, introspection and revocation endpoints answer RFC 6749 §5.2 bodies: a code and a human-readable description. The description is for a developer reading the response — it never carries a token, a key or a stack trace.

400 Bad Request
{
"error": "invalid_scope",
"error_description": "No requested scope is both held by the user and allowed for the agent."
}
Error Status What it means
invalid_request 400 A parameter is missing, repeated, or has an invalid value
invalid_client 401 Client authentication failed: the agent is unknown, or its assertion was rejected
invalid_grant 400 The subject token is expired, malformed, or not from the configured provider
unsupported_grant_type 400 The grant_type is not one this control plane issues
invalid_scope 400 The scope intersection is empty, or a refresh tried to widen
invalid_target 400 The audience is not in the agent’s allowed_audiences
access_denied 400 The agent is disabled, the task is revoked, the sponsor is no longer active at the identity provider, too little of the task is left to issue a token for (task_ending — stop, do not retry), or the delegation depth was exceeded
unsupported_token_type 400 A token_type_hint naming a type this server does not issue
temporarily_unavailable 503 An upstream this decision depends on could not be reached: the identity provider’s keys, the agent’s keys, or the identity provider’s answer about the sponsor. Never read as a yes. Retry
slow_down 429 Too many requests from this source, refused before the request is read. Retry-After says when. Recorded in the ledger as a summary under rate_limited

invalid_client covers several very different things, deliberately: an unknown agent, a bad signature, a replayed jti, an assertion older than five minutes, and an over-long instance. Telling them apart in the response would tell an attacker which agent ids exist. The audit ledger has the reason; that is where to look.

A disabled agent is access_denied, not invalid_client — and only for an assertion that verified. A caller learns that an agent is disabled only by proving it is that agent.

temporarily_unavailable is the server’s problem, not yours. An upstream this decision depends on could not be reached: the identity provider’s keys, the agent’s keys, or the identity provider’s answer about the sponsor. It is never read as a yes. Retrying is correct; re-issuing the credential is not.

The admin API answers 400 with an RFC 9110 problem document naming the fields rather than failing on the first one it meets:

400 Bad Request
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "The request is invalid.",
"status": 400,
"errors": {
"display_name": ["is required."],
"max_delegation_depth": ["is required."]
}
}

The field names are the JSON ones, so the error names the thing you sent, not an internal property. Validation runs in two passes — the request’s own shape, then the registration as a whole — so a request with both kinds of problem shows the second only once the first is fixed:

400 Bad Request
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "The request is invalid.",
"status": 400,
"errors": {
"sponsor_required": [
"must be true: issuing without a human subject token is not supported yet."
]
}
}

Some rules that surprise people:

Field Rejected when
agent_id Not a lowercase slug, or longer than 128 characters
sponsor_required false. v0.1 issues nothing without a human subject token
allowed_scopes Empty, containing duplicates, or containing anything that is not an RFC 6749 scope token
allowed_audiences Empty, containing duplicates, or containing a URL that is not absolute http/https
max_task_ttl Present and outside Onbe:Agents:MinTaskTtlMaxTaskTtl (a minute to a day by default)
max_token_ttl Present and outside Onbe:Agents:MinTokenTtlMaxTokenTtl (30 seconds to an hour by default), or longer than the task lifetime
max_delegation_depth Outside 1 to 5
jwks_uri Not absolute HTTPS
jwks Any key carrying a private member (d, p, q, dp, dq, qi, k, oth), refused per field
jwks and jwks_uri Both set, or neither

Every authorization decision, allow or deny, is a row in the audit ledger, and a denial always carries a machine-readable reason. A denial that names nobody is one row per reason per window plus a summary carrying count; a denial that names an agent, person, task or token is always its own row. When a response says invalid_client and you want to know which of the five causes it was, ask the ledger rather than guessing:

Terminal window
curl -s "https://onbe.internal.example.com/audit?decision=deny&limit=5" \
-H "Authorization: Bearer $ONBE_ADMIN_KEY"

The audit ledger explains the record, and section 7 of the contract defines its shape.

A tool server’s refusals are its own, not the control plane’s. @onbe/server and @onbe/mcp answer 401, 403 or 503 — the last when keys or introspection are unreachable — with a WWW-Authenticate header and a stable machine-readable reason; those are listed in the @onbe/server reference.

OnbePre-release. v0.1 is not out yet.

© 2026 Onbe