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
  }
}
FieldTypeDescription
error.codestringA stable, machine-readable error code — safe to branch on in your integration (see the table below).
error.messagestringA short human-readable reason. Sometimes a slug like invalid_body, sometimes a full sentence — treat it as debugging context, not something to parse.
error.detailsobject | nullExtra 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:

CodeStatusMeaning
bad_request400Malformed request body/query — often paired with Zod field errors in details.
unauthorized401Missing, invalid, or revoked API key.
forbidden403Authenticated, but not allowed to access this resource.
channel_not_included403Your plan doesn't include the channel (email/LinkedIn/WhatsApp) this action needs.
agency_plan_required403This feature (API keys, webhooks) requires the Agency plan.
not_found404The resource doesn't exist, or doesn't belong to you.
conflict409The request conflicts with the resource's current state.
payment_required / billing_locked402Out of plan quota, or the workspace's billing needs attention.
too_many_requests429Rate limit exceeded — see below.
internal_error500Something 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:

EndpointLimitKeyed by
GET /leads/search120 requests / 5 minutesAuthenticated user
POST /public/deliverability-check10 requests / 60 minutesIP 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).