Skip to main content

Rate limits

Requests are rate-limited per token, with separate buckets for reads and writes — a burst of card creations does not starve your ability to read, and vice versa. The concrete numbers are operational configuration, not part of the API contract: always read them from the response headers instead of hard-coding them.

The headers

Every response reports the state of the bucket the request consumed from:

RateLimit-Limit: 600
RateLimit-Remaining: 599
RateLimit-Reset: 1752703200
HeaderMeaning
RateLimit-LimitTotal requests allowed in the current window
RateLimit-RemainingRequests left in the window
RateLimit-ResetWhen the window resets (Unix timestamp, seconds)

When you run out

An over-limit request is rejected with 429 and code rate_limited in the standard error envelope, plus a Retry-After header telling you how many seconds to wait:

HTTP/1.1 429 Too Many Requests
Retry-After: 30

The request was not executed — retrying after the delay is always safe, and for mutating POSTs you should retry with the same Idempotency-Key (a 429 outcome is never stored, so the retry executes normally).

A well-behaved client

  1. Honor Retry-After on 429 — never hammer.
  2. Watch RateLimit-Remaining and throttle proactively when it gets low.
  3. Spread bulk work (imports, syncs) instead of bursting.
  4. Add jitter to retries so parallel workers do not stampede the same reset.

cURL (show the rate-limit headers for a request):

curl -sS -D - -o /dev/null "$KANBAN_API_BASE_URL/boards" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"

JavaScript:

const baseUrl =
process.env.KANBAN_API_BASE_URL ?? "https://app.filetag.ai/api/kanban/v1";
const token = process.env.KANBAN_API_TOKEN;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function apiWithRateLimit(path, options = {}, attempts = 5) {
for (let attempt = 1; attempt <= attempts; attempt += 1) {
const response = await fetch(`${baseUrl}${path}`, {
...options,
headers: { Authorization: `Bearer ${token}`, ...options.headers },
});

const remaining = Number(response.headers.get("ratelimit-remaining"));
if (Number.isFinite(remaining) && remaining < 5) {
console.warn(`rate budget low: ${remaining} requests left`);
}

if (response.status !== 429) {
return response;
}

const retryAfter = Number(response.headers.get("retry-after") ?? "1");
const jitter = Math.random() * 500;
await sleep(retryAfter * 1000 + jitter);
}
throw new Error("still rate limited after retries");
}

apiWithRateLimit("/boards")
.then(async (response) => {
const payload = await response.json();
console.log(payload.data.length, "boards");
})
.catch((error) => {
console.error(error);
process.exit(1);
});

Python:

import json
import os
import random
import time
import urllib.error
import urllib.request

BASE_URL = os.environ.get(
"KANBAN_API_BASE_URL", "https://app.filetag.ai/api/kanban/v1"
)
TOKEN = os.environ["KANBAN_API_TOKEN"]


def api_with_rate_limit(path, attempts=5):
for _ in range(attempts):
request = urllib.request.Request(
f"{BASE_URL}{path}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
try:
with urllib.request.urlopen(request) as response:
remaining = response.headers.get("RateLimit-Remaining")
if remaining is not None and int(remaining) < 5:
print(f"rate budget low: {remaining} requests left")
return json.load(response)
except urllib.error.HTTPError as exc:
if exc.code != 429:
error = json.load(exc)["error"]
raise RuntimeError(
f"{error['code']}: {error['message']} "
f"(request {error['request_id']})"
) from exc
retry_after = int(exc.headers.get("Retry-After", "1"))
time.sleep(retry_after + random.uniform(0, 0.5))
raise RuntimeError("still rate limited after retries")


payload = api_with_rate_limit("/boards")
print(len(payload["data"]), "boards")

One more property worth knowing: rate limiting fails closed. If the API cannot verify your limit, it rejects the request rather than letting it through unmetered — another reason your client should treat 429 and 5xx as normal, retryable events.