Rate limits
Request limits for the REST API and MCP server, and how to handle a 429 cleanly.
Every response from the API tells you where you stand. Make any request with -i and read the headers:
curl -i "https://api.actuallycare.com/v1/listings" \
-H "X-API-Key: YOUR_API_KEY"HTTP/2 200
RateLimit-Limit: 500
RateLimit-Remaining: 499
RateLimit-Reset: 893RateLimit-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.
When you hit 429#
A rate-limited request returns the standard error envelope with error.code set to RATE_LIMIT_EXCEEDED (see 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:
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 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 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. - 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 for the rest of the operational details.