Per-plan ceilings, response headers, and backoff strategy.
Two different ceilings can return a 429, and both come from the plan of the account that owns the API key: a daily API call cap and a monthly request allowance. There is no per-key, per-minute counter — the only per-minute ceiling applied to a request is the per-IP flood limiter in front of /api.
Resets at midnight UTC. Exceeding it returns 429 with code DAILY_CAP_EXCEEDED and a resetsAt timestamp in the body.
| Plan | API calls / day |
|---|---|
| Developer Sandbox | 100 |
| Micro | 5,000 |
| Growth | 25,000 |
| Scale | 100,000 |
| Enterprise | Unlimited |
Every authenticated /api/v1 request — sandbox calls included — counts against the monthly allowance of your plan. It covers a UTC calendar month and starts again on the 1st. Exceeding it returns 429 Monthly quota exceeded. Check where you stand with GET /api/v1/validate-key (limits.requestsPerMonth is -1 on a plan with no monthly allowance), or on the API Usage page in the dashboard.
| Plan | API requests / month |
|---|---|
| Developer Sandbox | 1,000 |
| Micro | 10,000 |
| Growth | 50,000 |
| Scale | 150,000 |
| Enterprise | Unlimited |
| Essential, Plus, Elite (consumer plans) | 1,000 |
| Header | Description |
|---|---|
| RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset | The per-IP flood limiter on /api (standard draft headers). This is the only per-minute ceiling actually applied to the connection. |
| X-RateLimit-Limit | Your plan's advisory per-minute guidance (Developer Sandbox 60, micro 120, growth 600, scale 2,000, enterprise 10,000). Informational only — it is not separately enforced. |
| X-Quota-Limit / X-Quota-Remaining / X-Quota-Reset | Your plan's monthly request allowance, how much of it is left, and the Unix timestamp (UTC) when it resets. Not sent on a plan with no monthly allowance. |
X-RateLimit-Remaining and X-RateLimit-Reset are not sent. Back off on Retry-After or the standard RateLimit-Reset header, and fall back to exponential backoff when neither is present.
async function verifyWithRetry(payload: object, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch('https://api.trustverify.co.uk/api/v1/kyc/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (res.status !== 429) return res;
// A daily-cap or monthly-quota 429 will not clear within a retry window;
// only the per-IP flood limiter is worth waiting out.
const body = await res.clone().json().catch(() => ({} as any));
if (body?.code === 'DAILY_CAP_EXCEEDED') {
throw new Error(`Daily API call cap reached; resets at ${body.resetsAt}`);
}
// Retry-After (seconds) and RateLimit-Reset (delta-seconds) come from the
// flood limiter. Neither is guaranteed, so back off exponentially if absent.
const retryAfter = Number(res.headers.get('Retry-After'));
const resetIn = Number(res.headers.get('RateLimit-Reset'));
const hintedMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000
: Number.isFinite(resetIn) && resetIn > 0 ? resetIn * 1000
: 0;
await new Promise(r => setTimeout(r, Math.max(hintedMs, 1000 * 2 ** attempt)));
}
throw new Error('Rate limit retries exhausted');
}