api · reference
Rate limits
Request quotas per API key, the headers that report remaining budget, and what happens at the limit.
Algorithm: sliding-log per API key (Upstash Redis) Window: 60 seconds Last updated: 2026-05-25
Per-plan defaults
Each API key has two independent buckets:
- Read — GET / HEAD requests.
- Write — POST / PUT / PATCH / DELETE.
| Plan | Read / min | Write / min |
|---|---|---|
| free | 30 | 5 |
| starter | 60 | 15 |
| professional | 120 | 30 |
| business | 300 | 60 |
| enterprise | 600 | 120 |
Writes are intentionally tighter — every write may trigger a saga, a domain event, and an outbound webhook fan-out.
Headers
Every successful response carries:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 42
X-RateLimit-Reset is seconds until the window resets, not a
Unix timestamp.
On block (429 Too Many Requests) we additionally set Retry-After
and return RFC 7807 problem details:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 27
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 27
{
"type": "https://docs.vendu.app/errors/rate-limit-exceeded",
"title": "Rate limit exceeded",
"status": 429,
"retry_after_sec": 27
}
Bulk requests
Bulk endpoints (POST /api/v1/{resource}/bulk) currently consume one
write-bucket slot regardless of items.length (the maxBatchSize: 50
ceiling bounds blast radius). Charging N slots is a future change —
when it ships, the X-RateLimit-Remaining returned with a bulk
response will already reflect the multi-slot cost.
Fallback when Redis is unavailable
If UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN are not
configured (local dev / unit tests) the limiter admits every request
and reports remaining = limit. Production deploys must set both
vars in vercel env.
Webhook-only buckets
Inbound platform webhooks (Telegram, Viber, MonoPay, …) have a
separate, IP-based limiter at src/lib/rate-limit/webhooks.ts
(60 req/min webhook, 30 req/min payment) — independent from the
per-API-key buckets above.