API basics
Rate limits
How many requests a key, an organization and a chat visitor may send, what a publishable key may spend, how to read what is left, and how to back off when you hit the limit.
Request limits count requests in fixed one-minute windows. A window starts on the minute and resets all at once when it ends; it does not roll. Public chat also has hourly and daily publishable key budgets.
| Who | Limit | Counted per |
|---|---|---|
Developer API (/api) | 120 requests a minute | API key |
Developer API (/api), all keys together | 600 requests a minute | Organization |
Public chat (/public/v1) | 30 requests a minute | Visitor IP address |
Public chat, polling /updates | 60 requests a minute | Conversation and IP address |
Public chat, analytics /telemetry | 120 requests a minute | Visitor IP address |
Both developer limits apply to every /api request, so minting more keys raises the per-key budget only up to the organization's 600. A request refused for a bad key or a read-only key is refused before it is counted.
On public chat, starting a conversation, sending a message, uploading a file and ending all share the visitor's 30. Polling for updates has its own budget, so a chat that polls does not starve the visitor's sends. The widget's own routes (/public/chat) count separately, so your chat UI and the widget on the same network never spend each other's budget.
Publishable keys
A publishable key sits in your page for anyone to copy, so on top of the per-visitor limits each key has budgets that every visitor of your site shares. They bound what a copied key can spend; a real site stays far inside them.
| What | Limit | Counted per |
|---|---|---|
| New conversations | 300 an hour | Publishable key |
| New conversations | 30 an hour | Publishable key and IP address |
| Assistant replies | 5,000 a day | Publishable key |
| Voice sessions started | 60 an hour | Publishable key |
| Voice sessions started | 10 an hour | Publishable key and IP address |
| Voice sessions running at once | 5 | Publishable key |
| Voice minutes | 300 a day (UTC) | Publishable key |
Going over answers 429 with Retry-After and the code key_quota_exceeded (voice_busy when all five voice sessions are running). The X-RateLimit-* headers on such a response describe the per-minute request limit, not what is left of the key's budget. A voice session holds its full max_seconds from the day's minutes while it runs and gives back what it did not use when it ends. If these counters cannot be reached, chat carries on, but a voice session is refused with 503 voice_unavailable.
Read what is left
Every counted response carries the state of its window:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The per-key (or per-visitor) limit for this window. |
X-RateLimit-Remaining | Requests left in this window. |
X-RateLimit-Reset | When the window resets, as Unix seconds. |
X-RateLimit-Org-Limit, -Remaining, -Reset | The same three for your organization's shared budget, on /api. |
If the counter itself cannot be reached, the request is let through without these headers, so treat them as optional when you parse them.
When you go over
The request is refused with status 429 and a Retry-After header holding the whole seconds until the window resets:
HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1793180460
{ "error": "rate_limited", "message": "Rate limit exceeded. Retry after 17s." }Wait that long and send it again. Retrying sooner is refused again and still counts.
async function apiFetch(url, init = {}, tries = 3) {
for (let i = 0; ; i++) {
const res = await fetch(url, {
...init,
headers: { Authorization: `Bearer ${process.env.MLF_API_KEY}`, ...init.headers },
})
if (res.status !== 429 || i === tries - 1) return res
const wait = Number(res.headers.get('retry-after') ?? '1')
await new Promise((r) => setTimeout(r, wait * 1000))
}
}Staying under
- Spread batch jobs out: 120 a minute is two a second per key.
- Use webhooks for anything you would otherwise poll for.
- Page with the largest
limitthe endpoint allows (see pagination), so a backfill takes fewer requests. - Run independent integrations on separate keys so one cannot spend another's budget, keeping the organization's 600 in mind.