# Rate limits

EnvoAPI applies a rolling Account Rate Limit to the Customer Account, not to
one API Key. All active keys on that Account and every Lookup Request or retry
share the same allowance. The limit comes from the Account's current Access
Limits; do not hardcode a limit in client code.

Lookup responses that reach rate-limit accounting expose these headers:

| Header                  | Meaning                                                                  |
| ----------------------- | ------------------------------------------------------------------------ |
| `X-RateLimit-Limit`     | The Account's current rolling request ceiling.                           |
| `X-RateLimit-Remaining` | Requests remaining in the current rolling window.                        |
| `X-RateLimit-Reset`     | Whole seconds until capacity resets.                                     |
| `Retry-After`           | On `429`, the positive number of seconds to wait before another attempt. |

## The 429 response

A request over the Account Rate Limit is rejected with `429`; it is not queued.
The response uses the standard [error envelope](https://docs.envoapi.com/guides/errors.md) and includes
`Retry-After`:

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "The Account rate limit was exceeded.",
    "retryable": true,
    "details": []
  },
  "meta": {
    "requestId": "request-example"
  }
}
```

Treat the response headers as the current source of truth. Limits can change
when Account terms change, and traffic from another key or worker affects the
remaining allowance.

## Retry with a bound

Honor `Retry-After`, add a small random delay so concurrent workers do not
retry together, and cap the attempt count. All current customer lookup
operations are `GET`, but every retry still consumes a rate-limit slot once it
is accepted and can incur a Lookup Cost.

```ts
const delay = (milliseconds: number) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

export async function fetchWithRateLimitBackoff(
  url: URL,
  attempts = 3,
) {
  for (let attempt = 0; attempt < attempts; attempt += 1) {
    const response = await fetch(url, {
      headers: {
        accept: "application/json",
        authorization: `Bearer ${process.env.ENVO_API_KEY ?? ""}`,
      },
    });

    if (response.status !== 429) return response;
    if (attempt === attempts - 1) break;

    const retryAfter = Number(response.headers.get("retry-after"));
    const seconds = Number.isSafeInteger(retryAfter) && retryAfter > 0
      ? retryAfter
      : 2 ** attempt;
    const jitterMilliseconds = Math.floor(Math.random() * 250);
    await delay(seconds * 1_000 + jitterMilliseconds);
  }

  throw new Error("Rate limited after bounded retries");
}
```

Coordinate concurrency across workers that share a Customer Account. Cache
reusable results when appropriate and slow sustained workloads instead of
looping on `429`. See [Pagination](https://docs.envoapi.com/guides/pagination.md) for bounding multi-page jobs
and [Authentication](https://docs.envoapi.com/guides/authentication.md) for the relationship between keys and
Accounts.

[Documentation index](https://docs.envoapi.com/llms.txt) · [All endpoints](https://docs.envoapi.com/api/index.md)
