Rate limits
The budget
Section titled “The budget”Each account has a budget of requests per minute, shared by all its keys:
60 on loomkeep.app, more with Premium where it is offered. A self-hosted
instance sets its own limits in Admin › Settings. GET /v1/me says what
yours is.
GET /v1/export also has its own pace: once an hour per account.
Where you stand
Section titled “Where you stand”Every response says how much is left:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed per minute. |
X-RateLimit-Remaining |
Requests left in the current minute. |
X-RateLimit-Reset |
Seconds until the minute starts over. |
Past the limit, the API answers 429 with the code api.rate_limited and a
Retry-After header, in seconds. The hourly export answers the same way.
Handling a 429
Section titled “Handling a 429”Wait as long as Retry-After says, then try once more:
async function loomkeep(path) { const url = `https://loomkeep.app/api/v1${path}`; const headers = { Authorization: `Bearer ${process.env.LOOMKEEP_API_KEY}` };
for (let attempt = 0; attempt < 3; attempt++) { const res = await fetch(url, { headers }); if (res.status !== 429) { if (!res.ok) throw new Error((await res.json()).code); return res.json(); } const seconds = Number(res.headers.get("Retry-After") ?? 60); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } throw new Error("api.rate_limited");}import osimport time
import requests
def loomkeep(path): url = f"https://loomkeep.app/api/v1{path}" headers = {"Authorization": f"Bearer {os.environ['LOOMKEEP_API_KEY']}"}
for _ in range(3): res = requests.get(url, headers=headers) if res.status_code != 429: res.raise_for_status() return res.json() time.sleep(int(res.headers.get("Retry-After", 60))) raise RuntimeError("api.rate_limited")Staying well within it
Section titled “Staying well within it”- Wait for
Retry-Afterafter a429rather than retrying at once; a loop that keeps retrying only keeps the account blocked. - Poll gently. A dashboard widget needs nothing faster than every 5 to 15 minutes: the calendar and stats don’t change by the second.
- Ask for less. Filter on the server (
?phase=,?domain=,fromandtoon the history) instead of paging through everything and filtering afterwards, and uselimit=100when you do need every page. - Sync incrementally. To keep a copy of your history, ask only for what
happened since your last run with
GET /v1/history?from=…. - Back up with the export. One
GET /v1/exporta day gets everything; paging through every endpoint costs far more requests. - Cache what barely moves: your profile, your lists, last year’s stats.