# Rate limits

Default quotas, per integration per workspace:

| Bucket | Limit |
| --- | --- |
| Requests per minute | 300 |
| Requests per day | 20 000 |
| Write requests per minute | 60 |

Writes have their own, tighter budget because each one can fan out to a channel
push. A runaway read loop costs us queries; a runaway write loop reaches the
OTAs.

Higher tiers exist. If a legitimate integration pattern does not fit, talk to us
rather than working around it — usually the answer is a filter you are not using
yet, and occasionally it is a tier change.

## Headers

Every response carries the current state, not just the ones that fail:

```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 284
X-RateLimit-Reset: 1755500400
```

`X-RateLimit-Reset` is a Unix timestamp. The values reflect whichever bucket is
tightest right now, which is the one you will hit next.

## When you exceed it

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 37
```

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry in 37s.",
    "details": { "limit": 300, "reset_at": 1755500400 },
    "request_id": "…"
  }
}
```

Honour `Retry-After`. Retrying sooner just consumes budget you do not have.

## Staying inside the quota

The limits are set on the assumption that you use the API as designed. Three
habits account for almost all of the difference:

- **Delta sync, not full sync.** [`updated_after`](/filtering-and-sync) turns a
  nightly re-read of every reservation into a handful of requests.
- **Batch your writes.** `PUT /v1/calendar` applies a whole season across many
  properties in one call. Ninety separate per-day requests do the same work for
  ninety times the quota, and cost far more to propagate to the channels.
- **Subscribe to [webhooks](/webhooks).** Most polling exists to notice changes.
  Being told instead removes it.

## Back off properly

```javascript
async function callWithRetry(request, attempt = 0) {
  const res = await request();
  if (res.status !== 429 || attempt >= 5) return res;

  // Trust Retry-After; fall back to exponential backoff if it is absent.
  const retryAfter = Number(res.headers.get('retry-after')) || 2 ** attempt;
  await new Promise((r) => setTimeout(r, retryAfter * 1000));
  return callWithRetry(request, attempt + 1);
}
```

Add jitter if you run several workers, or they will all wake up together and
spend the new window in the first second.
