> ## Documentation Index
> Fetch the complete documentation index at: https://docs.setaside.money/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

> Rate limits: Set Aside Money developer API reference.

There are two limits, and they do different jobs.

| Limit | Pro | Max | What it is for |
| - | - | - | - |
| Per key, per minute | 60 | 60 | Catching a runaway loop quickly, while still allowing a burst. |
| Per account, per hour | 500 | 1,500 | The overall ceiling, shared across every key on the account. |

The per-minute window lets you burst. A backfill that pages through several thousand transactions
runs at full speed for about eight minutes before the hourly ceiling starts to apply, which is well
past what most backfills need.

For comparison, a job polling every fifteen minutes uses 4 requests an hour.

## Headers

Every response carries both windows, whether it succeeded or not, so you can pace yourself without
having to be refused first.

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1786170960
X-RateLimit-Limit-Hour: 500
X-RateLimit-Remaining-Hour: 437
X-RateLimit-Reset-Hour: 1786186800
```

`Reset` values are Unix timestamps in seconds. Windows are aligned to the clock, so the per-minute
window resets at the top of each minute and the hourly window at the top of each hour.

## When you are limited

You get a `429` with a `Retry-After` header in seconds:

```json theme={null}
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Retry in 34 seconds.",
    "retry_after": 34,
    "request_id": "req_01J8Z3K9X2QY4M7N"
  }
}
```

Two codes distinguish which limit you hit:

* `rate_limit_exceeded` is the per-key per-minute window. Wait, then continue.
* `account_rate_limit_exceeded` is the account's hourly ceiling. Another key on the same account may
  be consuming it too.

A refused request does not consume either budget, so being limited never makes the situation worse.

## Backing off

Honour `Retry-After`. It is exact, not an estimate.

```js theme={null}
async function callApi(path, options = {}) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`https://api.setaside.money/v1${path}`, options);
    if (response.status !== 429) return response;

    const retryAfter = Number(response.headers.get("Retry-After") || 1);
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
  }
  throw new Error("Still rate limited after 5 attempts");
}
```

There is little point polling hard. Nothing in a budget changes second to second, bank data arrives
in batches, and a sync every few hours sees everything a sync every minute would. Once every 15
minutes is plenty, and it uses under 1% of the hourly allowance.

## Provider limits are separate

`POST /v1/sync` asks your banks for new data, and the providers behind that impose their own quotas
which are much tighter than ours. If you hit one, you get a `429` with the code
`provider_quota_exhausted`. That is the bank's limit, not the API's, and retrying sooner will not
help.

Scheduled syncs run on their own anyway. You rarely need to trigger one by hand.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.