# Rate limits

> Request limits for the REST API and MCP server, and how to handle a 429 cleanly.

<!-- Source: https://docs.actuallycare.com/api/rate-limits -->

Every response from the API tells you where you stand. Make any request with `-i` and read the headers:

```bash
curl -i "https://api.actuallycare.com/v1/listings" \
  -H "X-API-Key: YOUR_API_KEY"
```

```text
HTTP/2 200
RateLimit-Limit: 500
RateLimit-Remaining: 499
RateLimit-Reset: 893
```

`RateLimit-Limit` is your budget for the window, `RateLimit-Remaining` is what's left, and `RateLimit-Reset` is the number of seconds until the window resets. These are the standard `RateLimit-*` headers — not the older `X-RateLimit-*` convention, so check the exact names if your HTTP library helpfully "normalizes" headers.

## The limits

| Scope | Limit | Window |
| --- | --- | --- |
| All `/v1` endpoints | 500 requests per IP | 15 minutes |
| Auth endpoints (`/v1/auth`, excluding `/refresh`) | 30 **failed** attempts per IP — successful requests don't count | 15 minutes |
| `POST /v1/auth/login` (additional cap) | 50 **total** attempts per IP — successes included | 15 minutes |
| `/v1/auth/refresh` | 30 requests per IP | 1 minute |

Limits apply per IP address. Two limits stack on login: the 30-failed-attempts budget is the one a broken integration hits first, and a second 50-total-attempts cap protects the endpoint even from successful-login loops. The `RateLimit-*` headers on a login response report the **50-total** cap (`RateLimit-Limit: 50`) because it's the last limiter in the chain — the 30-failed budget is enforced but not surfaced in headers. If you're hitting either one, something is failing repeatedly, or you're logging in per request instead of reusing tokens. Authenticate once and refresh; see [Authentication](/api/authentication).

## When you hit 429

A rate-limited request returns the standard error envelope with `error.code` set to `RATE_LIMIT_EXCEEDED` (see [Errors](/api/errors)), along with the `RateLimit-*` headers above and a `Retry-After` header in seconds. The polite retry honors `Retry-After` (falling back to `RateLimit-Reset`) instead of guessing:

```javascript
async function getJSON(url, attempt = 0) {
  const res = await fetch(url, {
    headers: { "X-API-Key": process.env.ACTUALLYCARE_API_KEY },
  });

  if (res.status === 429 && attempt < 3) {
    const wait =
      Number(res.headers.get("Retry-After")) ||
      Number(res.headers.get("RateLimit-Reset")) ||
      2 ** attempt * 5;
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
    return getJSON(url, attempt + 1);
  }

  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}
```

Retry only on `429` and `5xx` — other errors won't get better with repetition. The full backoff pattern is on the [Errors](/api/errors) page.

## Stay under the limits

- **Cache reads.** If a script asks for the same listings three times in one run, that's two wasted requests. Fetch once, reuse.
- **Use webhooks instead of polling.** Re-fetching everything on a timer to spot changes is the fastest way to burn 500 requests. Subscribe to [webhooks](/concepts/webhooks) and let ActuallyCare push changes to you.
- **Fetch big lists at `limit=100`.** One page of 100 costs a fifth of five pages of 20 — see [Pagination](/api/pagination).
- **Spread out scheduled jobs.** A cron job that fans out dozens of parallel requests at the top of the hour competes with itself. Stagger the work.

## MCP server limits

If you're building a custom MCP client, the MCP server has its own per-user limits, separate from the REST limits above:

| Scope | Limit |
| --- | --- |
| All MCP requests | 120 per minute, per user |
| Tool calls | 30 per minute, per user |

Tool executions can also run for up to 300 seconds before timing out, so set your client timeouts accordingly. See [Build a custom MCP app](/mcp/custom-apps) for the rest of the operational details.
