# Errors & Rate Limits

> The standard error response shape used across the Warmerly API, plus every documented rate limit: per-key, per-endpoint, and the public-endpoint IP backstop.

Source: https://warmerly.com/docs/errors

This page documents the two things that apply the same way across almost every endpoint in this API, so each reference page doesn't have to repeat them. Endpoint pages link back here rather than restating this.

## Error response shape

Almost every error response from the authenticated endpoints is JSON in this shape, with a matching HTTP status code. The public, unauthenticated endpoints (for example `POST /public/deliverability-check` and `POST /contact`) are an exception: they return a bare string such as `{ "error": "invalid_body" }`.

```json
{
  "error": {
    "code": "bad_request",
    "message": "invalid_body",
    "details": null
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `error.code` | string | A stable, machine-readable error code, safe to branch on in your integration (see the table below). |
| `error.message` | string | A short human-readable reason. Sometimes a slug like `invalid_body`, sometimes a full sentence, treat it as debugging context, not something to parse. |
| `error.details` | object \\| null | Extra structured context when relevant (e.g. quota numbers, validation field errors). Always present in the response, `null` when there's nothing to add. |

Common `error.code` values and their status:

| Code | Status | Meaning |
| --- | --- | --- |
| `bad_request` | 400 | Malformed request body/query, often paired with field errors in `details`. |
| `unauthorized` | 401 | Missing, invalid, or revoked API key. |
| `forbidden` | 403 | Authenticated, but not allowed to access this resource. |
| `account_suspended` | 403 | The account is suspended. Returned on every request, including ones made with an API key. |
| `channel_not_included` | 403 | Your plan doesn't include the channel (email/LinkedIn/WhatsApp) this action needs. |
| `agency_plan_required` | 403 | This feature (API keys, webhooks) requires the Agency plan. |
| `not_found` | 404 | The resource doesn't exist, or doesn't belong to you. |
| `conflict` | 409 | The request conflicts with the resource's current state. |
| `payment_required` / `quota_exhausted` / `plan_capacity` / `campaign_lead_limit` / `billing_locked` | 402 | A plan limit or monthly allowance is used up (the code varies by endpoint), or the workspace's billing needs attention (`billing_locked`). |
| `too_many_requests` | 429 | Rate limit exceeded, see below. |
| `service_unavailable` | 503 | A feature or an upstream service is not available right now. |
| `internal_error` | 500 | Something went wrong on Warmerly's end. |

**Known inconsistency:** an unhandled exception in a route (a bug, not an expected error) falls back to a bare `{ "error": "server_error" }`, a string, not the object shape above, with status `500`. This is rare (every documented error case in these docs uses the object shape) but if you're strictly parsing `error.code`, guard against `error` being a string.

There is currently no request ID or correlation ID field in error responses. If you need to reference a specific failed request when contacting support, include the timestamp and endpoint instead.

## Rate limits

Rate limiting happens at up to three layers, depending on the endpoint:

### 1. Per-API-key limit (applies to every key)

Every API key has its own **per-minute** request limit, checked on every authenticated request regardless of endpoint. Default **60 requests/minute**, configurable up to a cap of **120 requests/minute** per key when you [create it](https://warmerly.com/docs/keys). Exceeding it returns:

```json
{ "error": { "code": "too_many_requests", "message": "rate_limit_exceeded", "details": null } }
```

with HTTP status `429`.

### 2. Per-endpoint limits (a small number of endpoints)

Some endpoints layer an additional, tighter limit on top of the per-key limit, because their abuse case (scraping, credential stuffing) isn't covered by a generous per-minute number. Other endpoints, such as sign-in, contact forms and workspace invites, have limits of their own too. This table lists two that matter most to API users; where a limit applies to an endpoint covered in these docs, its page says so:

| Endpoint | Limit | Keyed by |
| --- | --- | --- |
| `GET /leads/search` | 120 requests / 5 minutes | Authenticated user |
| `POST /public/deliverability-check` | 10 requests / 60 minutes | IP address |
| `POST /public/deliverability-check` with `check: "blocklist"` | the limit above, plus 400 requests / 60 minutes across all callers | IP address, then site-wide |

`GET /leads/search` returns the `429` shape above. `POST /public/deliverability-check` returns `{ "error": "too_many_requests" }` with a `Retry-After` header in seconds.

### 3. Unauthenticated request backstop

Requests that carry **no** session cookie and no `X-Api-Key` header (i.e. fully unauthenticated calls, such as `POST /contact`) are capped at **120 requests/minute per IP** as a coarse flood backstop, independent of the limits above. A request that sends only an `Authorization: Bearer` header (the Android app) does not count as carrying credentials here, so it is counted against this limit too. The check runs in Warmerly's request middleware before the route handler, keeps its count in memory on each server instance, and returns the `429` shape with `Retry-After: 60`. The MCP endpoint and the mail add-on endpoints are exempt.

### What isn't separately rate-limited

Session-cookie (dashboard) requests from a logged-in user have no dedicated per-user rate limit beyond the unauthenticated-IP backstop above (which doesn't apply to them once logged in), the practical ceiling for browser-driven dashboard use is whatever each page's own endpoints enforce. If you're building an integration, use an API key rather than reusing a dashboard session, both for correctness and so you get a predictable, documented limit.

## Plan quotas vs. rate limits

Don't confuse a rate limit (how fast you can call an endpoint) with a **plan quota** (how much of a metered resource: verifications, lead exports, email-finder lookups, you can use in a calendar month). Quota exhaustion returns `402` (with `error.code` `quota_exhausted` or `payment_required`, depending on the endpoint), not `429`. Monthly allowances reset on the **1st of the month**, not on your subscription's renewal date and not on a rolling window, and they do not roll over. Each metered resource has its own usage endpoint (e.g. [`GET /verify/usage`](https://warmerly.com/docs/verify#usage--quota), [`GET /leads/quota`](https://warmerly.com/docs/leads#export-quota)), and [`GET /usage/quotas`](https://warmerly.com/docs/usage) returns every one of them in a single call.

## Every error code has its own page (15)

- [bad_request](https://warmerly.com/docs/errors/bad-request)
- [unauthorized](https://warmerly.com/docs/errors/unauthorized)
- [forbidden](https://warmerly.com/docs/errors/forbidden)
- [account_suspended](https://warmerly.com/docs/errors/account-suspended)
- [agency_plan_required](https://warmerly.com/docs/errors/agency-plan-required)
- [not_found](https://warmerly.com/docs/errors/not-found)
- [conflict](https://warmerly.com/docs/errors/conflict)
- [payment_required](https://warmerly.com/docs/errors/payment-required)
- [quota_exhausted](https://warmerly.com/docs/errors/quota-exhausted)
- [plan_capacity](https://warmerly.com/docs/errors/plan-capacity)
- [campaign_lead_limit](https://warmerly.com/docs/errors/campaign-lead-limit)
- [billing_locked](https://warmerly.com/docs/errors/billing-locked)
- [too_many_requests](https://warmerly.com/docs/errors/too-many-requests)
- [service_unavailable](https://warmerly.com/docs/errors/service-unavailable)
- [internal_error](https://warmerly.com/docs/errors/internal-error)

## More in Reference

- [API Changelog](https://warmerly.com/docs/changelog): Reverse-chronological log of changes to the Warmerly public API, and the deprecation policy for breaking…
