[relaybell docs › Reference › Errors & rate limits](https://relaybell.com/docs/errors-limits) · [All docs as Markdown](https://relaybell.com/docs.md) · [Index for AI assistants](https://relaybell.com/llms.txt)

# Errors & rate limits

> Every error code with its HTTP status, plus the request and send rate limits.

Errors return a JSON body `{ "error": "code", "request_id": "…" }` (often with a readable `message`, and sometimes more fields) and a standard HTTP status. The same `request_id` comes back in the `X-Request-Id` header of every response — quote it when you contact support. Branch on `error`, not on `message`, which may change.

| Status | Code | When |
| --- | --- | --- |
| `400` | `invalid_json` | The body isn't valid JSON (send `Content-Type: application/json`). |
| `400` | `title_required`, `external_ids_empty`, `segment_not_found`, `unknown_topic` | A send (or a draft being sent): no title; `externalId` / `externalIds` given but empty; a segment that isn't this website's; an unknown topic. |
| `400` | `segment_needs_a_rule`, `name_required`, `nothing_to_update` | Segments — see [Segments](https://relaybell.com/docs/segments-topics#segments) — and topics or templates without a name. |
| `400` | `event_required`, `action_title_required` | An event or automation without an event name; an automation without `action.title`. |
| `400` | `trigger_event_required`, `at_least_one_step_required` | Sequences (journeys). |
| `400` | `content_title_required`, `invalid_timezone`, `invalid_url`, `invalid_segment`, `name_and_event_required` | Recurring schedules and website settings, webhooks and feeds, goals. |
| `400` | `invalid_settings`, `invalid_prompt`, `invalid_welcome`, `invalid_color`, `invalid_retention_days` | Settings, prompt and welcome bodies. |
| `400` | `invalid_required`, `invalid_scopes`, `invalid_expiry` | `PUT /identity`; creating an API key. |
| `400` | `invalid_range`, `invalid_tz`, `invalid_status`, `invalid_cursor` | Report and list filters. |
| `400` | `ids_required`, `too_many_ids`, `invalid_id`, `invalid_label`, `reserved_label`, `too_many_labels`, `nothing_to_change`, `invalid_filters`, `invalid_device` | The `/api/v1/audience` routes. |
| `400` | `invalid_file`, `no_subscribers`, `no_account_email` | An upload that isn't an image; an empty import; `POST /reports/test` without an account email. |
| `400` | `unknown_field` | A send field written in snake_case (`external_id`, `segment_id`, `send_at` …). It would be ignored and the send would reach everyone, so it is refused; `use` names the camelCase field. |
| `401` | `not_authenticated`, `invalid_api_key`, `api_key_expired` | No key sent (and no dashboard session); a key that isn't recognised or was revoked; a key past its expiry date. |
| `403` | `insufficient_scope` | The key lacks the scope named in `required` (see [scopes](https://relaybell.com/docs/authentication#scopes)). |
| `403` | `email_not_verified` | Sends are refused until the account's email address is verified. |
| `403` | `account_suspended` | The account is suspended. |
| `403` | `session_required`, `identity_off_requires_dashboard` | Only the signed-in owner can do this: create keys, revoke another key, rotate the identity secret, turn identity verification off. |
| `404` | `not_found` | The id isn't one of this website's — including any id that isn't a valid UUID. |
| `404` | `not_scheduled`, `project_not_found`, `subscriber_not_found` | Cancelling a send that isn't waiting; a public SDK route with an unknown Project ID; topic preferences for a browser that isn't subscribed. |
| `409` | `not_a_draft`, `too_many_drafts` | Only a draft can be changed, sent or deleted that way; a website keeps up to 500 drafts. |
| `409` | `segment_in_use` | Deleting a segment that feeds, schedules or pending sends still use. |
| `409` | `structural_change_requires_no_active_enrollments` | Reordering or removing journey steps while people are in the journey. |
| `409` | `name_taken` | Renaming a goal to the name of another goal. |
| `409` | `delivery_in_progress`, `body_too_large` | Resending a webhook delivery that already has an attempt waiting, or whose event was over 64 KB. |
| `413` | `draft_too_large`, `payload_too_large`, `file_too_large` | A draft holds up to 256 KB, a request body up to 1 MB, an uploaded image up to 2 MB. |
| `429` | `rate_limited`, `send_limit_reached` | Too many requests, or the website's hourly send budget is used up — see the limits below. |
| `500` | `internal_error` | Something went wrong on our side; try again later. |

Every `/api/v1` route checks the credential first, so an unknown path answers `401 not_authenticated` without a key (and a plain `404` page with one). Request bodies can be up to 1 MB.

## Rate limits {#rate-limits}

| Requests | Limit | Counted per |
| --- | --- | --- |
| `POST /api/v1/notifications` and `/notifications/:id/send` (every request; saving a draft isn't counted) | 60 a minute and 600 an hour | API key (dashboard: per account) |
| Broadcasts: sends to everyone, a segment, a topic or filters | 30 per clock hour | website |
| Sends to specific users (`externalId` / `externalIds`), or `transactional: true` with a segment or filters | 1,000 per clock hour | website |
| Test sends to your own devices (dashboard) | 120 per clock hour | website |
| `POST` / `GET /api/v1/events` with an API key | 3,000 a minute | API key |
| Browser SDK requests (`/api/v1/projects/…`: config, subscribe, `track()`, inbox, preferences; click and delivery pings), and `/api/v1/events` without a key | 120 a minute | visitor IP address |
| Sign-up, sign-in (including two-factor and Google/GitHub sign-in), email verification, password reset; changing the account's password or email; turning two-factor on or off; deleting or restoring the account | 30 per 15 minutes | IP address |
| Other dashboard account routes (`/api/v1/auth/me`, `/api/v1/account`, …) | 120 a minute | IP address |
| `POST /api/v1/ai/suggest` | 30 an hour | IP address |
| `POST /api/v1/upload` | 60 an hour | IP address |
| `POST /api/v1/support` | 5 an hour | account |

Other API-key routes (stats, reports, segments, automations, settings, …) have no request limit of their own; keep to a sensible pace.

Going over a request limit answers `429 rate_limited`; going over one of the three hourly send budgets (per website) answers `429 send_limit_reached`. A limited route's responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets). Every `429` also carries `Retry-After` (seconds): wait that long before trying again, rather than retrying in a loop. The hourly send budgets restart at the top of each hour; a `send_limit_reached` answer says which budget ran out: `{ "error": "send_limit_reached", "message": "…", "limit": 30, "kind": "broadcast", "retryAfter": 1260 }` (`kind` is `broadcast`, `transactional` or `test`). A send refused this way isn't counted.

**Sending rules:** quiet hours and a per-subscriber daily **frequency cap** are set per website (Settings → Sending) and apply automatically — no per-request flags needed. Quiet hours hold a send that would go out now until the quiet time ends; the cap skips subscribers who already got that many notifications in the last 24 hours. Sends with `transactional: true` skip both.
