Kwill

Rate limits

How Kwill limits API request volume and how to handle 429 responses gracefully.

The Kwill API enforces two overlapping sliding-window limits:

LimitWindowRequestsKeyed by
Per API key15 minutes300The API key (api_keys.id)
Per IP address15 minutes600The 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.

On this page