Rate limits
600 requests per minute per key, the RateLimit headers on every response, and how to back off on 429.
Every API key gets 600 requests per minute, shared across /v1/api/* and the MCP endpoint. Requests without a valid key are limited per IP address instead.
#Headers
Every response, success or error, carries:
| Header | Meaning |
|---|---|
RateLimit-Limit | Your budget for the window (600) |
RateLimit-Remaining | Requests left in the current window |
RateLimit-Reset | Seconds until the window resets |
When you exceed the limit you get 429 with code resource_exhausted and a Retry-After header:
HTTP
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 23
Retry-After: 23
Content-Type: application/json
{"error":{"code":"resource_exhausted","message":"Rate limit exceeded (600 requests/minute). Retry after 23s.","requestId":"req_…"}}The window is a fixed 60 seconds.
#Staying under the limit
- Watch
RateLimit-Remainingand slow down as it approaches zero, rather than reacting to429s. - Use webhooks instead of polling. Subscribe to
message.receivedand Ody pushes new messages to you. If you must poll a shared inbox, every 10–30 seconds is plenty. - Request bigger pages (
limitup to each endpoint's maximum) when syncing. - Back off with jitter on
429and5xx; honourRetry-After; cap your retries.
TypeScript
async function withBackoff<T>(fn: () => Promise<T>, attempts = 5): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err: any) {
const retriable = err?.status === 429 || err?.status >= 500;
if (!retriable || i >= attempts - 1) throw err;
const wait = err.retryAfter ? err.retryAfter * 1000 : Math.min(30_000, 2 ** i * 500) * (0.5 + Math.random());
await new Promise((r) => setTimeout(r, wait));
}
}
}Need a higher limit for a specific integration? Email [email protected] with your use case.
Last updated Wed Oct 14 2026 00:00:00 GMT+0000 (Coordinated Universal Time)Something wrong on this page? Tell us