Errors on plugboard's own routes
Plugboard's own routes live under /plugboard/: /plugboard/usage, /plugboard/admin/keys, /plugboard/status, /plugboard/metrics, /plugboard/dashboard, /plugboard/health and /plugboard/ready. Their errors are RFC 9457 problem details, sent as application/problem+json:
{
"type": "https://jjjermiah.github.io/opencode-plugboard/errors#app-forbidden",
"title": "Forbidden",
"status": 403,
"detail": "This key cannot read the usage of another app",
"instance": "/plugboard/usage?app=sparkyfitness",
"code": "app_forbidden",
"param": "app",
"requestId": "3f9c1a0d2b7e"
}typelinks to the anchor for thecodebelow. A code with no anchor here isabout:blank.titleis the HTTP reason phrase forstatus.detailis the message for this request: safe to show. It never holds a key or provider text.instanceis the request target as sent (path and query).codeis the machine-readable reason,paramthe request field it is about (when there is one), andrequestIdthe value of thex-plugboard-request-idresponse header, which the server log also carries.- Headers keep their meaning:
Retry-Afteron a refusal that can be retried.
The model routes are different. /v1/chat/completions, /v1/responses, /v1/completions, /v1/models, /v1/messages and /v1beta/* answer errors in their provider's shape (OpenAI, Anthropic or Gemini), so SDKs keep their own exception types.
The root probes /healthz and /readyz keep their own bodies ({"ok":true}, {"ready":...}).
Codes
invalid_request_target
The request target is not a valid URL. Fix the client's URL.
invalid_host
The Host header is not the listen address. Plugboard refuses DNS rebinding on loopback. Call the address in listen (127.0.0.1:4010 by default).
cross_origin_forbidden
A browser Origin header reached a keyless endpoint. Configure keys, or call plugboard from a server or a command line tool.
unsupported_media_type
The request body must be Content-Type: application/json.
invalid_api_key
The key is missing or does not match any configured or runtime key. Send Authorization: Bearer <key> with a key from keys, or a runtime key from the admin routes.
admin_key_required
/plugboard/metrics with adminKey set reads the admin key only, because its series name every app. Send the admin key.
not_found
No plugboard route at this path, or the option that serves it is off (for example /plugboard/usage with usage: { enabled: false }). The discovery page at / lists the routes that are served. Plugboard's own routes all live under /plugboard/; /v1/usage, /v1/status and /metrics are not served.
method_not_allowed
The path exists but not for this method (for example PUT /plugboard/admin/keys). Use the method the discovery page lists.
invalid_date
A from or to value is not a UTC date in YYYY-MM-DD form, or is not a real date. The param names which one.
invalid_range
from is after to. Swap them.
range_too_long
The range spans more than 366 days. Ask for a shorter range, or fetch it in parts.
invalid_app
The app value is not an app name from the keys configuration (for /plugboard/usage), or not a valid runtime key name (/plugboard/admin/keys: lowercase letters, digits and -, and not default).
invalid_user
The user filter is not 1 to 64 characters of A-Z, a-z, 0-9, ., _, @, : or -.
app_forbidden
An app key asked for the usage of another app. An app key reads only its own app; the admin key reads any app.
key_not_found
No runtime key has that id. List the keys at GET /plugboard/admin/keys to find the right one.
config_key
The key is in the plugboard config (keys), so the admin routes cannot revoke it. Remove it there and reload OpenCode.
app_has_opencode_tools
The app runs OpenCode tools (apps.<name>.opencodeTools), and a runtime key would give the same power. Set its key in the plugboard config instead.
app_has_config_key
The app already has a key in the plugboard config. Edit keys to change it, or pick another app name.
not_ready
The backend cannot serve yet. The detail names the first missing piece: no generation, no app directory used since start, the event pump not running, or the model catalog not loaded. Send one model request to an app to confirm the directory; the other pieces follow on their own. The same reason is in the /readyz document.
body_read_timeout
The request body stopped arriving for limits.bodyReadTimeoutMs, or was not complete within limits.bodyReadMaxMs. Send the body without a pause.
request_body_too_large
The request body is over the body cap: limits.maxBodyBytes (32 MiB by default), or the app's limits.apps.<name>.maxBodyBytes. Send a smaller body, or ask the operator to raise the cap.
server_busy
Too many request bodies are being read at once (limits.maxBodyBytesInFlight), or this app already holds its share of it (see maxBodyBytesInFlight in Configuration). Retry after the Retry-After delay.
body_budget_insufficient
The admin key API refused to create a key for a new app: the set limits.maxBodyBytesInFlight cannot hold one body of the largest size for every app, this one included. Raise the budget, or unset it to let it grow with the apps. Nothing was created.
timeout
The request ran past requestTimeoutMs. Raise that option, or ask for less work per request.