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.
OAuth errors
Section titled “OAuth errors”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.
{ "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 |
The ones that are easy to misread
Section titled “The ones that are easy to misread”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.
Validation errors
Section titled “Validation errors”The admin API answers 400 with an RFC 9110 problem document naming the fields rather than
failing on the first one it meets:
{ "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:
{ "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:MinTaskTtl…MaxTaskTtl (a minute to a day by default) |
max_token_ttl |
Present and outside Onbe:Agents:MinTokenTtl…MaxTokenTtl (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 |
Where the reason actually is
Section titled “Where the reason actually is”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:
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.
On the other side of the token
Section titled “On the other side of the token”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.
© 2026 Onbe