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:
{
"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. Exceeding it returns:
{ "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 |
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 billing period). Quota exhaustion returns 402 payment_required, not
429, and resets on your billing cycle rather than a rolling time window. Each metered
resource has its own usage endpoint (e.g. GET /verify/usage,
GET /leads/quota).
