---
title: "Errors"
description: "The error envelope and the stable error codes, with what each one means and what to do."
canonical: https://docs.rumoro.dev/errors
markdown: https://docs.rumoro.dev/errors.mdx
---

# Errors

The error envelope and the stable error codes, with what each one means and what to do.

All errors look the same.

```json
{
  "error": {
    "code": "not_found",
    "message": "Mention not found",
    "requestId": "4b0e7d21-9c3a-4f62-8d15-7e2a0c9b6f13"
  }
}
```

- Handle errors by `code`. Codes don't change, but new ones can appear, so treat an unknown code like any error with its status.
- `message` is for people and can change. For `validation_error` it names the field (`"term: Required"`).
- `requestId` equals the `X-Request-Id` header. Include it when you contact support.

## Retrying

- Retry `5xx`, `408` and `429` after the wait the response gives. Every `429 rate_limited` has `Retry-After` and `retryAfterSeconds`.
- Don't retry `503 database_not_ready`. The deployment needs maintenance.
- Other `4xx` codes mean the request must change. `409 classification_pending` clears once the mention is scored.

## Codes

### Credentials and access

| Code | Status | Meaning |
| --- | --- | --- |
| `unauthorized` | 401 | Missing, unknown, revoked or expired credential. Send `Authorization: Bearer ref_…` with a key from the **API keys** page. |
| `read_only_key` | 403 | A `read` credential tried to write. Use a `write` key. |
| `forbidden` | 403 | Not allowed for this credential or role, such as an API key changing the team or anyone changing an owner's role |
| `key_limit` | 409 | The workspace has 100 keys that aren't revoked |

### Requests

| Code | Status | Meaning |
| --- | --- | --- |
| `validation_error` | 400 | A field or parameter is missing or wrong. The message names it. |
| `not_found` | 404 | No such resource in your workspace, or no such route. Also a cursor whose item is gone. |
| `invalid_cursor` | 400 | The cursor can't be read or belongs to another sort |
| `unsupported_media_type` | 415 | The body isn't `application/json` |
| `payload_too_large` | 413 | The body is over 128 KiB |
| `request_timeout` | 408 | The body took over 5 seconds |
| `rate_limited` | 429 | Over the workspace or endpoint limit ([Rate limits](/rate-limits)) |
| `conflict` | 409 | The change clashes with existing data |
| `request_conflict` | 409 | A `requestId` reused with a different body |
| `stale_version` | 409 | The resource changed since you read it (`ifVersion`) |

### Keywords, groups and billing

| Code | Status | Meaning |
| --- | --- | --- |
| `duplicate_keyword` | 409 | This group already has that term |
| `insufficient_balance` | 402 | The balance can't pay another keyword-day. Top up. |
| `keyword_limit_reached` | 402 | The 500-keyword limit is reached |
| `duplicate_group` | 409 | A group with that name or `externalId` exists |
| `default_group` | 409 | The default group can't be deleted |
| `default_group_context` | 400 | The default group has no description of its own. Use `PATCH /v1/company`. |
| `billing_not_configured` | 503 | Top-ups and invoices aren't available here |
| `no_billing_owner` | 404 | The workspace has no owner to bill |

### Mentions, views, segments and the team

| Code | Status | Meaning |
| --- | --- | --- |
| `invalid_assignee` | 400 | The assignee isn't in the workspace |
| `classification_pending` | 409 | A verdict for a mention that isn't scored yet |
| `already_classified` | 409 | A rescore for a mention that is already scored |
| `keyword_inactive` | 409 | A rescore for a mention whose keyword is muted |
| `duplicate_view` | 409 | A view with that name exists (case ignored) |
| `duplicate_segment` | 409 | A segment with that name exists |
| `already_member` | 409 | The invited address is already a member |
| `last_owner` | 409 | The last owner can't be removed |

### Alerts and channels

| Code | Status | Meaning |
| --- | --- | --- |
| `unknown_channel` | 400 | The rule names a channel outside the workspace |
| `not_a_digest` | 400 | `run` on an instant rule |
| `hourly_email_unsupported` | 400 | Hourly rules can't send email |
| `rule_disabled`, `no_channels` | 409 | The rule is off or has no channel. Only from test and run endpoints outside the reference. |
| `channel_disabled` | 409 | The channel is off |
| `slack_not_connected` | 409 | Slack isn't connected (404 on the Slack connection endpoints) |
| `slack_not_configured`, `telegram_not_configured`, `email_not_configured` | 503 | That channel type isn't set up here |
| `delivery_not_configured`, `delivery_key_unavailable` | 503 | Stored channel secrets can't be used here |
| `no_recipients` | 409 | The email channel has no recipients |
| `recipient_suppressed` | 409 | The address bounced or reported spam |
| `confirmation_rate_limited` | 429 | Too many confirmation emails to this address |
| `version_conflict` | 409 | The webhook's preset rules changed at the same time. Read it again. |
| `invalid_signature` | 401 | An incoming Slack or Telegram request wasn't signed correctly |
| `invalid_token` | 400 | An email link was changed, is malformed or expired (404 for invitation links) |

### Delivery log

| Code | Status | Meaning |
| --- | --- | --- |
| `not_retryable` | 409 | Only failed or held deliveries can be retried |
| `not_replayable` | 409 | Only finished deliveries can be replayed |
| `digest_retry_expired` | 409 | The retry window is over (6 hours, or 45 minutes past the hour for hourly digests) |
| `digest_has_replay` | 409 | There is a newer replay of this digest |
| `payload_too_large` | 409 | The payload is over 1 MiB and can't be sent |
| `email_hourly_limit` | 409 | Retrying an email held by the channel's 20-an-hour limit. It waits for a daily digest rule on the channel. |
| `email_idempotency_expired` | 409 | The safe retry window passed. A replay may send the email twice. |
| `confirmation_requires_resend` | 409 | Confirmation emails are resent, not retried |
| `delivery_request_incomplete` | 500 | A test's saved result couldn't be read. Read it again. |

A test email over the limit of 5 an hour, or an instant email over 20 an hour, is not an HTTP error. The request returns 200 and the delivery shows `email_test_rate_limited` or `email_hourly_limit`.

### Upstream and service

| Code | Status | Meaning |
| --- | --- | --- |
| `upstream_unavailable` | 502 | A provider such as payments or Slack didn't respond. Retry with backoff. |
| `source_unavailable` | 502, 503 | A platform didn't respond to a live search |
| `source_timeout` | 504 | A platform was too slow on a live search |
| `email_transport_unavailable` | 503 | Email sending isn't set up for this delivery |
| `invalid_host`, `invalid_origin` | 403 | An MCP request from a host or origin that isn't allowed |
| `database_unavailable` | 503 | A short database problem. Retry. |
| `database_not_ready` | 503 | The deployment needs maintenance |
| `not_configured` | 503 | No database is configured |
| `internal_error` | 500 | Something failed on our side. Retry, and send the `requestId` if it persists. |

The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) lists each operation's codes. The [MCP server](/mcp) returns the same codes as a tool error, `{ "error": { "code", "message", "requestId" } }`.
