Rate limits
How Kwill limits API request volume and how to handle 429 responses gracefully.
The Kwill API enforces two overlapping sliding-window limits:
| Limit | Window | Requests | Keyed by |
|---|---|---|---|
| Per API key | 15 minutes | 300 | The API key (api_keys.id) |
| Per IP address | 15 minutes | 600 | The originating IP |
Both limits apply simultaneously. A request is rejected if either bucket is exhausted.
429 response
When a limit is hit, the API returns 429 Too Many Requests with a Retry-After header:
HTTP/1.1 429 Too Many Requests
Retry-After: 47
Content-Type: application/json
{
"error": "Too many requests. Retry after 47 seconds."
}Retry-After is the number of seconds to wait before retrying. Always read this header — the window is sliding, not fixed, so the value varies.
Backoff with retry
Read Retry-After and wait that long before retrying. For bulk operations, add jitter to prevent thundering-herd problems when multiple workers hit the limit at the same time.
async function kwillFetch(url, options = {}, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${process.env.KWILL_API_KEY}`,
...options.headers,
},
});
if (res.status !== 429) return res;
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '5', 10);
const jitter = Math.random() * 1000; // up to 1 s of jitter
await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter));
}
throw new Error(`Kwill API still rate-limited after ${maxRetries} attempts.`);
}Tips for staying within limits
- Batch where possible. Fetch lists rather than looping over individual GETs.
- Cache read responses. Most resource data changes infrequently; cache GET results locally and invalidate via webhooks.
- Use webhooks instead of polling. Subscribe to
invoice.paid,proposal.signed, and other events instead of polling for status changes. See Webhooks.