<!-- Errors & Rate Limits — https://docs.warmerly.com/errors -->

# Errors & Rate Limits

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

Nearly every error response — across every endpoint documented in these docs — is JSON
in this shape, with a matching HTTP status code:

```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 Zod field errors in `details`. |
| `unauthorized` | 401 | Missing, invalid, or revoked API key. |
| `forbidden` | 403 | Authenticated, but not allowed to access this resource. |
| `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` / `billing_locked` | 402 | Out of plan quota, or the workspace's billing needs attention. |
| `too_many_requests` | 429 | Rate limit exceeded — see below. |
| `internal_error` | 500 | Something went wrong on Warmerly's end. Safe to retry. |

**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://docs.warmerly.com/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)

A few 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:

| 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 |

These also return the `429` shape above.

### 3. Unauthenticated request backstop

Requests that carry **no** session cookie, `Authorization` header, or `X-Api-Key` (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. This applies at
the edge before your request reaches a route handler.

### 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 payment_required`, 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://docs.warmerly.com/verify#usage--quota),
[`GET /leads/quota`](https://docs.warmerly.com/leads#export-quota)), and [`GET /usage/quotas`](https://docs.warmerly.com/usage) returns every
one of them in a single call.
