---
title: "Errors"
description: "The error envelope and every stable error code."
canonical: https://docs.mentio.dev/errors
markdown: https://docs.mentio.dev/errors.mdx
---

# Errors

The error envelope and every stable error code.

{/* Generated by scripts/generate-api-docs.mjs from openapi.json (x-mentio-errors, the core error catalog). Do not edit: change the catalog in packages/core/src/errors.ts and regenerate. */}

All errors use one envelope with a stable machine-readable code and the id of the request:

```json
{
  "error": {
    "code": "validation_error",
    "message": "term: String must contain at least 2 character(s)",
    "requestId": "3f9c1e2a-7b4d-4c1e-9a0b-2d5e6f7a8b9c"
  }
}
```

`requestId` is also the `X-Request-Id` response header (every response has one, see [Conventions](/conventions#errors)); quote it when you write to support. A `rate_limited` error adds `retryAfterSeconds` and the `Retry-After` header. A 5xx is safe to retry with backoff; a 4xx is not, with the same payload.

## Codes

| Code                      | Status | When                                                                                             | What to do                                                        |
| ------------------------- | ------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| `unauthorized`            | 401    | No credential, or one that does not resolve to a workspace.                                      | Send Authorization: Bearer mk\_live\_… with a key from /api-keys. |
| `forbidden`               | 403    | The credential is valid but may not do this.                                                     |                                                                   |
| `read_only_key`           | 403    | A read key was used on a write endpoint.                                                         | Create a key with write scope.                                    |
| `validation_error`        | 400    | The body or query failed validation; the message names the field.                                |                                                                   |
| `not_found`               | 404    | No such resource in your workspace. An id from another workspace is a 404, never a 403.          |                                                                   |
| `invalid_cursor`          | 400    | The cursor was not one this endpoint issued, or the filters changed between pages.               |                                                                   |
| `payload_too_large`       | 413    | The body exceeds the endpoint limit.                                                             |                                                                   |
| `rate_limited`            | 429    | The per-workspace limit for this endpoint was hit; Retry-After says when.                        |                                                                   |
| `duplicate_keyword`       | 409    | The workspace already tracks this term.                                                          |                                                                   |
| `insufficient_balance`    | 402    | The wallet cannot cover one more keyword-day.                                                    | Top up from Billing; tracking resumes on its own.                 |
| `keyword_limit_reached`   | 402    | The self-serve keyword cap; beyond it is an enterprise conversation.                             |                                                                   |
| `billing_not_configured`  | 503    | This deployment has no payment provider set up.                                                  |                                                                   |
| `schedule_required`       | 400    | A daily alert rule was created without a schedule.                                               |                                                                   |
| `unknown_channel`         | 400    | A rule referenced a channel id the workspace does not own.                                       |                                                                   |
| `not_a_digest`            | 400    | A digest-only action was called on an instant rule.                                              |                                                                   |
| `slack_not_connected`     | 409    | Connect Slack from Settings first. 404 on the Slack connection endpoints, 409 elsewhere.         |                                                                   |
| `slack_not_configured`    | 503    | This deployment has no Slack app credentials.                                                    |                                                                   |
| `telegram_not_configured` | 503    | This deployment has no Telegram bot.                                                             |                                                                   |
| `email_not_configured`    | 503    | This deployment has no Resend key, so nothing that needs Resend (the audience sync) can run.     |                                                                   |
| `invalid_assignee`        | 400    | The assignee id is not a workspace member.                                                       |                                                                   |
| `classification_pending`  | 409    | A relevance or sentiment verdict was sent for a mention the classifier has not scored yet.       |                                                                   |
| `invalid_member`          | 400    | A person's owner, and the member an outreach activity is credited to, must be workspace members. |                                                                   |
| `already_member`          | 409    | An invitation was sent to an address that already belongs to a member.                           |                                                                   |
| `last_owner`              | 409    | Removing this member would leave the workspace without an owner.                                 |                                                                   |
| `duplicate_segment`       | 409    | Segment names are unique per workspace.                                                          |                                                                   |
| `invalid_signature`       | 401    | An inbound webhook did not carry a valid signature.                                              |                                                                   |
| `webhook_not_configured`  | 503    | The inbound webhook has no secret on this deployment.                                            |                                                                   |
| `invalid_token`           | 400    | A signed link (unsubscribe, confirmation) was tampered with or is malformed.                     |                                                                   |
| `protected_user`          | 409    | Staff accounts cannot be deleted from the admin page.                                            |                                                                   |
| `upstream_unavailable`    | 502    | A provider (Slack, Telegram, Polar, …) failed while serving your request. Retry with backoff.    |                                                                   |
| `internal_error`          | 500    | Something failed on our side. The response carries a requestId; quote it to support.             |                                                                   |

Handle errors by branching on `error.code`, not on the message; messages can change, codes will not. Codes are additive: treat one you do not know as a generic failure of the same status.
