Rate limits
60 requests/second burst, 600 requests/minute sustained. Per token. Headers tell you where you stand.
The /v1/* API is rate-limited per token. Two buckets run in parallel; you have to fit under both. A third, much larger per-IP ceiling runs before authentication purely as an abuse guard (see below).
| Bucket | Limit | Window | Algorithm | Use case |
|---|---|---|---|---|
| Burst | 60 requests | 1 second | Fixed window | Catches runaway loops and accidental DDOS. |
| Sustained | 600 requests | 60 seconds | Fixed window | Keeps long-running polling jobs in budget. |
If you exceed either bucket you get 429 Too Many Requests with Retry-After set to the seconds until the bucket refreshes.
The pre-authentication IP guard
Before a token is even looked up, each client IP is capped at 2,000 requests per minute across all tokens. This exists only to blunt credential-stuffing against /api/v1/*; a NAT'd office running several integrations should never get near it. When it trips you get 429 with Retry-After and X-RateLimit-Scope: ip (the per-token 429s carry no scope header). If you legitimately need more from one egress IP, write to support@tourical.com.
Headers on every response
Every response — success or failure — carries:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 587
X-RateLimit-Reset: 32
Retry-After: 32 # only on 429X-RateLimit-Limit— the limit of the sustained bucket. The burst bucket isn't surfaced because it's intended to catch programming errors, not normal traffic.X-RateLimit-Remaining— minimum of (burst remaining, sustained remaining). Treat 0 as "back off now."X-RateLimit-Reset— seconds until the applicable bucket refreshes. Read it before retrying.
Backoff pattern
The pattern that survives partial outages without burning your budget:
async function call(path: string, init?: RequestInit) {
let attempt = 0;
for (;;) {
const res = await fetch(`https://app.tourical.com${path}`, {
...init,
headers: {
...(init?.headers ?? {}),
Authorization: `Bearer ${process.env.TOURICAL_TOKEN}`,
},
});
if (res.status !== 429 && res.status < 500) return res;
if (attempt >= 5) return res;
const retryAfter = Number(res.headers.get("retry-after") ?? "1");
const jitter = Math.random() * 250;
await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter));
attempt++;
}
}Notes:
- Cap the retry count. Six attempts with the documented backoff is already 1 minute on a 429 streak.
- Add jitter so a fleet of workers doesn't synchronise their retries.
- Treat
5xxresponses the same as429for retry purposes — but with a longer base delay (~2s).
Polling vs webhooks
If you're polling for changes (e.g. "any new bookings since last check?"), you're probably an order of magnitude away from the sustained limit. But you're also burning resources on both sides — both yours and ours — for nothing most of the time.
Subscribe to a webhook instead. See Webhooks. The same data arrives within a few seconds of the change, with no polling cost.
Lifting the limit
These limits are deliberately generous for an integration that doesn't poll. If you have a legitimate need to burst higher (one-time backfill, migration, batch import) — write to support@tourical.com before you hit the limit, with a sketch of the request shape and a time window. We'd rather hand you a temporary bypass than have your job die mid-flight.

