Errors
Reference article: Part of the API reference. Every error links here through its
doc_url.
The API uses HTTP status codes to say whether a request worked. A 2xx status means it did. A 4xx status means something about the request needs to change, such as a missing key or scope. A 5xx status means something went wrong on our side.
Every error has the same JSON body:
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "This API key does not have the 'messages:write' scope. Create a key with it, or use a secret key.",
"doc_url": "https://docs.gonimbus.ai/api-reference/errors#insufficient_scope",
"request_id": "req_nn1fXk2LmQ8rT4vW6yZ0aB3c"
}
}
| Field | Description |
|---|---|
type |
The kind of error. One of the types below. Branch on this for broad handling. |
code |
A stable, machine-readable code, such as invalid_api_key. Branch on this for specific handling. |
message |
A sentence for people. It can change at any time, so don't parse it. |
param |
The parameter or header at fault, when there is one, such as Nimbus-Version. |
doc_url |
A link to this code's entry on this page. |
request_id |
The same ID as the Request-Id header. Quote it to support. |
Handling errors
::: code-group
curl -i https://api.gonimbus.ai/v1/whoami \
-H "Authorization: Bearer nmb_sk_not_a_real_key"
# HTTP/2 401
# request-id: req_…
# {"error":{"type":"authentication_error","code":"invalid_api_key",…}}
class NimbusApiError extends Error {
constructor(
readonly status: number,
readonly type: string,
readonly code: string,
message: string,
readonly requestId?: string,
) {
super(message)
}
}
const call = async (path: string, init: RequestInit = {}) => {
const response = await fetch(`https://api.gonimbus.ai/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`, ...init.headers },
})
if (response.ok) return response.json()
const { error } = await response.json()
throw new NimbusApiError(response.status, error.type, error.code, error.message, error.request_id)
}
try {
await call('/whoami')
} catch (err) {
if (err instanceof NimbusApiError && err.type === 'authentication_error') {
// Page someone: the key is wrong, revoked, or expired.
}
throw err
}
import os
import requests
class NimbusApiError(Exception):
def __init__(self, status, error):
super().__init__(error["message"])
self.status = status
self.type = error["type"]
self.code = error["code"]
self.request_id = error.get("request_id")
def call(method, path, **kwargs):
response = requests.request(
method,
f"https://api.gonimbus.ai/v1{path}",
headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
**kwargs,
)
if response.ok:
return response.json()
raise NimbusApiError(response.status_code, response.json()["error"])
try:
call("GET", "/whoami")
except NimbusApiError as err:
if err.type == "authentication_error":
pass # Page someone: the key is wrong, revoked, or expired.
raise
:::
Error types {#error-types}
| Type | Status | Meaning | Retry? |
|---|---|---|---|
invalid_request_error |
400, 402, 404 |
The request can't be done as sent: an unknown URL, a bad header value or parameter, or a plan limit. | No. Fix the request. |
authentication_error |
401 |
No valid key was sent. | No. Fix or replace the key. |
permission_error |
403 |
The key is valid but may not do this. | No. Use a key with the right scope. |
not_found_error |
404 |
The object doesn't exist, or the key can't reach it. | No. |
idempotency_error |
409 |
An Idempotency-Key was reused for a different request, or its first request is still running. See Idempotent requests. |
Only idempotency_request_in_progress, after a moment. |
rate_limit_error |
429 |
Too many requests in a short time. | Yes, after Retry-After seconds. |
api_error |
500, 503 |
Something went wrong on our side. | Yes, with backoff. |
Error codes
missing_api_key {#missing_api_key}
401 · authentication_error. The request had no Authorization header, or it wasn't in the form Bearer <key>. See Authentication.
invalid_api_key {#invalid_api_key}
401 · authentication_error. The key doesn't exist. Check for copy-paste truncation, and that you aren't sending a key from a different environment.
api_key_revoked {#api_key_revoked}
401 · authentication_error. The key was revoked, or rolled with no grace period. Use its replacement or create a new key. See Revoking a key.
api_key_expired {#api_key_expired}
401 · authentication_error. The key was rolled and its grace period has ended. Deploy the new key from the roll. See Rolling a key.
invalid_signature {#invalid_signature}
401 · authentication_error. An incoming webhook request had no Nimbus-Signature header, the signature doesn't match the body, or its timestamp is more than 5 minutes old. Sign <timestamp>.<raw body> with the install's signing secret, and send the exact bytes you signed. See Signing the request.
missing_install_token {#missing_install_token}
401 · authentication_error. An MCP request had no Authorization: Bearer nmb_it_… header.
invalid_install_token {#invalid_install_token}
401 · authentication_error. The MCP token doesn't match any install, for example because it's mistyped or the install's credentials were rotated. Use the current token from the install.
install_not_found {#install_not_found}
401 · authentication_error. No App install has the ID in the incoming webhook URL. Copy the install's hook_url again.
install_revoked {#install_revoked}
401 · authentication_error. The App was uninstalled from this room, or the App was deleted, so its signing secret and token no longer work. Reinstall the App to get new credentials.
insufficient_scope {#insufficient_scope}
403 · permission_error. The key is a restricted key without the scope this request needs. The message names the scope. Create a key with it, or use a secret key. See Scopes.
organization_unavailable {#organization_unavailable}
403 · permission_error. The organization the key belongs to can't be found, for example because it has been deleted. Contact support if you think this is wrong.
message_not_owned {#message_not_owned}
403 · permission_error. An App tried to update a message it didn't post. Apps can only update their own messages. param is message_id.
parameter_missing {#parameter_missing}
400 · invalid_request_error. A required parameter or body field wasn't sent. param names it.
parameter_invalid {#parameter_invalid}
400 · invalid_request_error. A parameter has the wrong type, is out of range, or isn't one of the allowed values, such as a limit of 500 or a cursor the API didn't issue. param names it, and the message says what's wrong.
parameter_unknown {#parameter_unknown}
400 · invalid_request_error. The body has a field the endpoint doesn't take, often a typo such as reply_too. param names it. Remove it, or check the endpoint's reference for the right name.
invalid_json {#invalid_json}
400 · invalid_request_error. The request body isn't valid JSON. Check for trailing commas, unquoted keys, or a body cut short.
unsupported_content_type {#unsupported_content_type}
400 · invalid_request_error. The request has a body but its Content-Type isn't application/json. param is Content-Type. Send JSON with Content-Type: application/json.
frame_invalid {#frame_invalid}
400 · invalid_request_error. A Frame breaks the Frames v1 rules: an unknown node type or field, a value too long, too many nodes, a duplicate action_id, or a table row with the wrong number of cells. param gives the path, such as frame.nodes.0.type, and the message says what's wrong. See Frame rules.
callback_url_invalid {#callback_url_invalid}
400 · invalid_request_error. An App's callback_url isn't allowed: it isn't https, has credentials in it, or points at localhost or a private address. param is callback_url. See Apps.
app_already_installed {#app_already_installed}
400 · invalid_request_error. The App is already installed on that workstream or Conflux room. param is surface. To get new credentials, rotate the existing install instead.
live_workstream_limit_exceeded {#live_workstream_limit_exceeded}
402 · invalid_request_error. Creating the workstream would go over your plan's limit on live workstreams. Archive or delete a workstream to free a slot, or upgrade your plan. See Create a workstream.
invalid_api_version {#invalid_api_version}
400 · invalid_request_error. The Nimbus-Version header names a version the API doesn't know. The message lists the supported versions. See Versioning.
unrecognized_request_url {#unrecognized_request_url}
404 · invalid_request_error. No endpoint matches the method and path. Check the method, and that the path starts with /v1.
resource_missing {#resource_missing}
404 · not_found_error. The object doesn't exist, or the key can't reach it. The API answers both the same way, so a key can't discover objects outside its reach. For a workstream, or anything inside one, check the key has been given it: see Workstream access. param names the path parameter at fault, such as workstream.
idempotency_key_reused {#idempotency_key_reused}
409 · idempotency_error. The Idempotency-Key was already used in the last 24 hours with a different request: another method, path, or body. Use a new key for a new request. See Idempotent requests.
idempotency_request_in_progress {#idempotency_request_in_progress}
409 · idempotency_error. A request with the same Idempotency-Key is still being processed. Wait a moment and retry with the same key. If the first request succeeded, you get its response; if it failed, your retry runs as new.
url_invalid {#url_invalid}
400 · invalid_request_error. A webhook endpoint's URL isn't allowed: it isn't https, has credentials in it, or points at a private or internal address, or its host doesn't resolve. param is url, and the message says which rule. See URL rules.
webhook_endpoint_limit {#webhook_endpoint_limit}
400 · invalid_request_error. The organization already has 16 webhook endpoints, the most it can have. Delete one you no longer use, or point several event types at one endpoint.
rate_limited {#rate_limited}
429 · rate_limit_error. The key sent too many requests. Wait for the number of seconds in the Retry-After header, then retry. See Rate limits.
internal_error {#internal_error}
500 · api_error. Something went wrong on our side. Retry with exponential backoff; if it keeps happening, contact support with the request_id.
webhook_secrets_unavailable {#webhook_secrets_unavailable}
503 · api_error. A webhook signing secret couldn't be created, so the endpoint wasn't created or its secret wasn't rolled. Retry later; if it keeps happening, contact support with the request_id.
app_secrets_unavailable {#app_secrets_unavailable}
503 · api_error. App install credentials couldn't be created or read, so the install, rotation, or signed request couldn't go ahead. Retry later; if it keeps happening, contact support with the request_id.
FAQ
Should I branch on type or code?
Both, at different levels. Use type for broad handling, such as "retry rate_limit_error and api_error, alert on authentication_error". Use code when one specific case needs its own handling. New codes can be added within a type without a new API version, so always keep a fallback for unknown codes.
Do failed requests show up on the Developers page?
Yes. Every request made with one of your keys is logged with its status, error code, and message, including requests refused because the key was revoked or expired. Filter the request log to Failed to see them.
Is a 404 always not_found_error?
No. A 404 for a path the API doesn't have is invalid_request_error with the code unrecognized_request_url. A 404 for an object that doesn't exist, or that the key can't reach, is not_found_error with the code resource_missing.
Which errors are safe to retry?
rate_limit_error (after Retry-After), api_error (with backoff), and idempotency_request_in_progress (after a moment, with the same key). Other errors will fail the same way until the request or key changes. When you retry a request that creates something, send an Idempotency-Key so it can't be created twice.
Why does the message differ from what I saw last week?
Messages are written for people and can be reworded at any time. Codes and types are stable within an API version.