Rate limits
Request rate, burst, and monthly quota limits of Vacation Tracker API v2.
Every API key has its own limits. Requests from one key never use up the limits of another key.
Limits per API key
| Limit | Value |
|---|---|
| Rate | 10 requests per second (steady rate) |
| Burst | 50 requests |
| Quota | 30,000 requests per calendar month |
- Rate and burst use a token bucket: a key can send up to 50 requests at once, and the bucket refills at 10 requests per second.
- Quota counts every request in the month and resets at the start of the next month (UTC).
Reads and writes share the same limits. Polling GET /v2/operations/{id} and managing webhooks through /v2/webhooks count too.
When you hit a limit
The API returns 429 Too Many Requests and does not run the request. It is safe to retry. For writes, retry with the same Idempotency-Key.
Rate or burst exceeded:
{
"error": {
"code": "RATE_LIMITED",
"name": "RateLimited",
"description": "Too many requests were sent in a short time.",
"resolution": "Wait a few seconds and retry, using the same Idempotency-Key for writes.",
"requestId": "..."
}
}Monthly quota used up:
{
"error": {
"code": "QUOTA_EXCEEDED",
"name": "QuotaExceeded",
"description": "The request quota for this API key has been used up.",
"resolution": "Wait until the quota resets, or contact Vacation Tracker support for a higher quota.",
"requestId": "..."
}
}The response has no Retry-After header. Use the backoff below instead.
Retry with exponential backoff and jitter
Wait longer after each 429, and add randomness so many clients do not retry at the same moment. Stop retrying QUOTA_EXCEEDED: it only clears when the month resets.
async function requestWithRetry(url: string, init: RequestInit, maxAttempts = 5): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const response = await fetch(url, init)
if (response.status !== 429 || attempt === maxAttempts - 1) return response
const body = await response.clone().json().catch(() => undefined)
if (body?.error?.code === 'QUOTA_EXCEEDED') return response
const capMs = Math.min(30_000, 500 * 2 ** attempt)
await new Promise((resolve) => setTimeout(resolve, Math.random() * capMs))
}
}Pass the same init (including the Idempotency-Key header) on every attempt.
Higher limits
If your integration needs more, email developers@vacationtracker.io with your company name, the integration, and the traffic you expect.