Using the API
Rate limits
Per-key request limits, Free fair-use limits, email and AI limits, the rate-limit headers, and how to back off when you get 429.
Rate limits keep the service fast and fair for everyone, and stop a runaway script or a stolen key from doing much harm. Most integrations never reach them. When you do, the API tells you exactly how long to wait.
All limits are settings that we can tune without a release, so the numbers on this page are the starting values planned for launch (proposed). Read the headers on each response rather than hard-coding a number.
The limits
Per API key or MCP token
Each API key and each MCP token has its own budget of requests per minute. The planned starting value is 600 requests per minute per key. A key can also be given its own lower limit, which is useful for a script you want to keep gentle.
Because the budget is per key, giving each integration its own key also gives each its own budget, and a busy integration cannot slow down another.
Per account: fair use on Free
The Free plan includes every feature. To stop abuse, Free accounts have monthly fair-use limits on:
| Limit | Free (proposed) | Team (proposed) |
|---|---|---|
| API calls per month | 10,000 | 100,000 |
| File storage | 1 GB | 10 GB |
| Sends per minute | 10 | 10 |
You can see your usage against these limits in Settings > Usage, or with Get usage. A warning flag is set at 80%. Passing a monthly limit returns plan_limit_reached. See Fair-use limits.
Sending email
The platform mailer has its own limits: a per-minute limit (planned at 10 per minute per account) and the monthly quota of platform emails, 20 on Free and 100 on Team. When the quota is used up, sends return quota_exceeded. Your own SMTP server is not limited by the monthly quota. See Platform email.
AI agent
The AI invoice agent is limited to about 10 requests per minute per account (proposed), on top of credits and a monthly ceiling per account. When credits run out you get ai_credits_exhausted.
Sign-in
Password sign-in is limited per IP address and locks an account for a short time after repeated failures (planned: 5 failures, then a 15-minute lock). Magic-link and password-reset requests are also limited.
Public routes
Routes that need no account are limited per IP address (proposed):
| Route | Limit |
|---|---|
| No-auth Generate a PDF | 30 per hour per IP |
| Token pages: pay page, verification, estimate, portal | 60 per minute per IP |
| "I have paid" with a proof upload | 5 per hour per token |
Rate-limit headers
Responses include three headers (proposed names, from the IETF draft for rate-limit headers):
| Header | Meaning |
|---|---|
RateLimit-Limit | The size of the current budget |
RateLimit-Remaining | How many requests are left in the current window |
RateLimit-Reset | Seconds until the window resets |
HTTP/1.1 200 OK
RateLimit-Limit: 600
RateLimit-Remaining: 41
RateLimit-Reset: 17
X-Request-Id: req_0193f2a2c0e5Use RateLimit-Remaining to slow down before you hit the limit.
When you are over the limit
You get HTTP 429 with the code rate_limited and a Retry-After header in seconds:
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Try again in 17 seconds.",
"request_id": "req_0193f2a2c0e6"
}
}Nothing was done for a rejected request, so it is always safe to retry it. Wait at least Retry-After seconds, then send the same request with the same Idempotency-Key.
async function call(url, options, attempt = 0) {
const response = await fetch(url, options);
if (response.status !== 429 || attempt >= 5) return response;
const wait = Number(response.headers.get("Retry-After") ?? 1);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
return call(url, options, attempt + 1); // same body, same Idempotency-Key
}Staying under the limits
- Use one key per integration. Each key has its own per-minute budget.
- Page with
limit=200for bulk reads. Fewer, larger pages use fewer requests. See Pagination and filters. - Use webhooks instead of polling. Subscribe to webhook events and fetch only what changed.
- Use
updated_afterto sync only records that changed since your last run. - Queue writes in your own system and send them at a steady pace rather than in bursts.
- Do not retry in a tight loop. Always honour
Retry-After, and add backoff for other temporary errors.