TouricalDevelopers

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).

BucketLimitWindowAlgorithmUse case
Burst60 requests1 secondFixed windowCatches runaway loops and accidental DDOS.
Sustained600 requests60 secondsFixed windowKeeps 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 429
  • X-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 5xx responses the same as 429 for 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.

On this page