docs(api): address rate limit review findings

- document the real 429 bodies: v1 returns { message }, v2 returns the structured error object
- exclude CORS preflight responses from the header guarantee
- describe monthly quotas as organisation-wide api/document/email counters, not envelope-only
- retry example: honor Retry-After exactly; cap only the exponential fallback delay
This commit is contained in:
ephraimduncan
2026-07-30 22:04:00 +00:00
parent d3eb0c7999
commit 478229aa90
2 changed files with 40 additions and 17 deletions
@@ -35,7 +35,9 @@ The rate limit applies to:
### Rate Limit Headers ### Rate Limit Headers
Every response from `/api/v1/*`, `/api/v2/*`, and `/api/v2-beta/*` includes these headers: Responses from `/api/v1/*`, `/api/v2/*`, and `/api/v2-beta/*` include these headers. The only
exception is CORS preflight (`OPTIONS`) requests, which are answered before the rate limiter runs
and carry no rate limit headers:
| Header | Description | | Header | Description |
| ----------------------- | ---------------------------------------------------------------------- | | ----------------------- | ---------------------------------------------------------------------- |
@@ -60,23 +62,40 @@ Beyond HTTP rate limits, your account has usage limits based on your subscriptio
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited | | Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
| Direct Templates | 3 | Unlimited | Unlimited | Unlimited | | Direct Templates | 3 | Unlimited | Unlimited | Unlimited |
### AppError-Based 429 Response ### Organisation Limit 429 Responses
Organisation windowed limits and monthly envelope quotas return an AppError response rather than Organisation windowed limits and organisation monthly quotas produce 429 responses whose body
the global per-IP limiter's `{ "error": "..." }` response. For example, an organisation windowed shape depends on the API version, and neither matches the global per-IP limiter's
limit returns: `{ "error": "..." }` body.
On `/api/v1/*`, the body contains only a message:
```json ```json
{ {
"code": "TOO_MANY_REQUESTS", "message": "Too many requests, please try again later. Contact support if you require higher limits."
"message": "Too many requests, please try again later. Contact support if you require higher limits.",
"statusCode": 429
} }
``` ```
The monthly quota response uses the same `code`, `message`, and `statusCode` shape with a On `/api/v2/*` and `/api/v2-beta/*`, the body is a structured error object:
quota-specific message. It does not add quota-specific rate limit headers or `Retry-After` because
the quota is not a time window. The global API headers described above may still be present. ```json
{
"message": "Too many requests, please try again later. Contact support if you require higher limits.",
"code": "TOO_MANY_REQUESTS",
"data": {
"code": "TOO_MANY_REQUESTS",
"httpStatus": 429,
"appError": {
"code": "TOO_MANY_REQUESTS",
"message": "Too many requests, please try again later. Contact support if you require higher limits."
}
}
}
```
Organisation windowed limit responses include the `X-RateLimit-*` headers and `Retry-After` for
their own window. Monthly quota responses carry no quota-specific rate limit headers or
`Retry-After` because the quota is not a time window; rely on the status code and message instead.
## Error Codes ## Error Codes
@@ -87,10 +106,11 @@ the quota is not a time window. The global API headers described above may still
There are three sources of `TOO_MANY_REQUESTS` responses: There are three sources of `TOO_MANY_REQUESTS` responses:
1. The global per-IP limit returns the `{ "error": "..." }` body shown above. 1. The global per-IP limit, returning the `{ "error": "..." }` body shown above.
2. Organisation windowed limits return the AppError body shown above. 2. Organisation windowed rate limits for the `api`, `document`, and `email` counters.
3. The monthly envelope quota returns an AppError body but does not add quota-specific rate limit 3. Organisation monthly quotas for the same three counters. Every authenticated API request
headers or `Retry-After`. consumes the `api` counter, so any endpoint can return this 429 once the monthly API quota is
exhausted — not just envelope-related ones.
--- ---
@@ -1000,9 +1000,12 @@ async function fetchWithRetry(
// Retry on rate limit // Retry on rate limit
if (response.status === 429) { if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After'); const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelayMs * Math.pow(2, attempt); // Honor Retry-After exactly; the cap only applies to the exponential fallback.
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
console.log(`Rate limited, waiting ${delay}ms...`); console.log(`Rate limited, waiting ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, maxDelayMs))); await new Promise((resolve) => setTimeout(resolve, delay));
continue; continue;
} }