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
| Header | Meaning |
|---|---|
RateLimit-Limit | Total requests allowed in the current window |
RateLimit-Remaining | Requests left in the window |
RateLimit-Reset | When 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
- Honor
Retry-Afteron429— never hammer. - Watch
RateLimit-Remainingand throttle proactively when it gets low. - Spread bulk work (imports, syncs) instead of bursting.
- 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.