Skip to main content
Every BeeOS OpenAPI response that isn’t a 2xx (and every SSE event: error / event: done with is_error: true) carries a stable machine-readable code field. This page is the single source of truth for that code; the wire envelopes only differ in shape, not in vocabulary. The complete catalogue is generated from backend/shared/types/apierror/ which is shared across Gateway, Agent Gateway, A2A Gateway, MCP Gateway, OpenAPI Gateway and the internal services. Codes in this table are public contract — they will not change shape during the 1.x line. Adding new codes is a minor version bump; renaming or removing a code is a major version bump.

How errors are encoded

Blocking JSON response (any 4xx / 5xx)

{
  "success": false,
  "error": {
    "type": "api_error",
    "code": "agent_not_found",
    "message": "Agent not found.",
    "details": {}
  }
}
The HTTP status line carries the same information as error.code (see the table below). The type field is the RFC 9457 problem-type (“api_error” / “invalid_request_error” / “authentication_error” / “permission_error” / “rate_limit_error” / “not_found_error” / “conflict_error” / “validation_error”); SDKs typically key on code rather than type.

SSE event: error frame (streaming invoke + task events)

event: error
data: {"type":"error","code":"service_timeout","status_code":504,"message":"agent invocation timed out"}
The code / status_code are exactly what the blocking JSON path would have returned for the same condition. See Streaming (P1-F, planned) for full SSE frame semantics.

SSE event: done terminal frame

When a stream ends in error, done.code mirrors the error frame’s code so a client that only consumes done is still able to dispatch:
event: done
data: {"type":"done","text":"","context_id":"ch-...","is_error":true,"error":"agent invocation timed out","code":"service_timeout"}
There is one streaming-specific code that never appears in blocking JSON:
  • agent_reply_error — the agent itself returned an in-band error message (agent_reply with is_error: true). The HTTP / SSE transport succeeded; the agent reported a domain-level failure. done.text carries the agent’s error text.

Code catalogue

4xx — client errors

codeHTTPWire envelopeCauseRecovery
invalid_json400JSONRequest body isn’t valid JSONFix client serialiser
invalid_body400JSONBody parsed but failed schema validationInspect message for which field
invalid_param400JSON, SSE errorQuery / path / body parameter rejected (e.g. taskId empty, deadline_ms > 7 days)Fix the offending parameter
missing_param400JSONA required parameter is absentAdd the parameter
unauthorized401JSONAuthorization header missing or invalidRefresh / re-issue credential — see Authentication & API Keys
invalid_token401JSONJWT / oag_ failed signature or hash checkSame as unauthorized
missing_token401JSONAuthorization header is emptyAdd the header
forbidden403JSON, SSE errorCaller doesn’t own the agent (and it isn’t public)Use a credential that owns the resource
agent_not_found404JSON, SSE erroragentId (or taskId) unknown to the caller’s ACLVerify ownership / visibility
conflict409JSON, SSE errorAgent rejected the call (busy / refused), or duplicate idempotency key on a changed payload, or task is terminalInspect message: agent rejected the request / task is already closed / duplicate idempotency key. Retry only on transient rejections
payload_too_large413JSONRequest body exceeds the per-route size limitcatalog/instances/webhook routes cap at 64 KiB; invoke / tasks / conversations cap at 1 MiB. For large attachments use POST /api/v1/files/presign-upload (P1-A) instead of inlining bytes
rate_limited429JSONPer-caller × endpoint quota exceededHonour Retry-After; quotas listed in the operator runbook
login_rejected422JSONLogin credentials wrong (NOT a refresh-token failure)Re-prompt user; do not clear existing session

5xx — server errors

codeHTTPWire envelopeCauseRecovery
internal_error500JSON, SSE errorUnexpected server fault (panic, marshalling failure, etc.)Retry with exponential backoff; file a bug if persistent
agent_offline503JSON, SSE errorAgent record exists but no live sessionRetry briefly; if persistent the agent pod is down
agent_service_unavailable503JSON, SSE errorOpenAPI Gateway lost connection to Message Service / chatinvoke (e.g. MESSAGE_SERVICE_URL unset, MS dead)Retry; check platform health
auth_unavailable503JSONAuth Service unreachableRetry; do not force logout
auth_transient503JSONAuth gRPC blip (timeout / replica failover)Retry; do not force logout — wire-level signal “session not killed”
refresh_transient503JSONJWT refresh failed due to transient server faultRetry; do not force logout
session_unavailable503JSONSession store unreachable on cookie-based loginRetry login
service_timeout504JSON, SSE errorOpenAPI Gateway gave up waiting for the agent. timeout_ms defaults to 120000 (legacy) and is server-clamped at 115000 so the response always has budget to flush before the underlying http.Server.WriteTimeout (120 s) firesUse the async tasks API for long work; do not retry the same blocking invoke since the agent may still be running

Streaming-only codes

codeFrameCause
agent_reply_errordone.code onlyAgent returned an agent_reply with is_error: true. Transport succeeded — the agent itself failed. Inspect done.text for the agent’s explanation.

Mapping chatinvoke sentinels → wire codes

The OpenAPI Gateway’s invokeErrToAPIError (handlers_agents.go) collapses the pkg/chatinvoke sentinel errors into wire-level codes:
chatinvoke sentinelWire codeHTTP
ErrInvokerAgentNotFoundagent_not_found404
ErrInvokerTaskNotFoundagent_not_found404
ErrInvokerAgentOfflineagent_service_unavailable (msg = “agent is offline”)503
ErrInvokerMisconfiguredagent_service_unavailable (msg = “message service not configured”)503
ErrInvokerStreamUnavailableagent_service_unavailable (msg = “streaming not configured”)503
ErrInvokerTimeoutservice_timeout504
ErrInvokerTaskRejectedconflict (msg = “agent rejected the request”)409
ErrInvokerTaskClosedconflict (msg = “task is already closed”)409
ErrInvokerDuplicateconflict (msg = “duplicate idempotency key”)409
ErrInvokerPermissionDeniedforbidden403
anything elseinternal_error500
Note: pre-P0-B versions of this contract used distinct code values task_rejected / agent_busy / agent_unavailable in client-facing docs. Those were never the wire codes — the actual wire has always been conflict / agent_service_unavailable with the discriminator in message. SDK clients must key on the code values in this table, not on the older doc values.

TypeScript SDK recipe

try {
  const reply = await agents.invokeAgent({ agentId, invokeAgentRequest: { message } });
  // ... use reply
} catch (e) {
  if (e instanceof ResponseError && e.response.status === 504) {
    // service_timeout — switch to async tasks
  } else if (e instanceof ResponseError) {
    const body = await e.response.json();
    switch (body.error?.code) {
      case "agent_not_found":     /* surface "agent not found" */ break;
      case "forbidden":           /* user lacks permission     */ break;
      case "conflict":            /* agent busy / duplicate    */ break;
      case "agent_service_unavailable":
      case "agent_offline":       /* retry later               */ break;
      default:                    /* generic error             */ break;
    }
  }
}

Go SDK recipe

_, _, err := client.AgentsAPI.InvokeAgent(ctx, agentID).
    InvokeAgentRequest(req).Execute()
if err != nil {
    if apiErr, ok := err.(*beeos.GenericOpenAPIError); ok {
        var env struct {
            Error struct {
                Code    string `json:"code"`
                Message string `json:"message"`
            } `json:"error"`
        }
        _ = json.Unmarshal(apiErr.Body(), &env)
        switch env.Error.Code {
        case "agent_not_found":
            // ...
        case "service_timeout":
            // fall back to async tasks
        }
    }
}

See also