Skip to main content

Rate limits

Limits are applied per credential, under an account ceiling, and per route category, over a rolling 60-second window.

CategoryRoutesLimit
Sending messagesPOST /messages and POST /v1/.../messages500 req/min
Media uploadPOST /v1/.../media60 req/min
Readseverything else (/templates, /events, other /v1/*)60 req/min

The values in the table are the defaults. Your plan may set its own limits per category, and then the plan's apply.

Not limited: /plans and /health.

Per key, under an account ceiling​

Each API key has its own bucket, sized by the plan — and so does each person signed in to the dashboard. An integration stuck in a loop exhausts only its own quota: the WordPress plugin's key hammering /v1 does not leave your ERP's key on 429.

Above the keys' buckets, the account has a ceiling per category, 3 times the plan's limit by default. That is what keeps extra keys from multiplying the quota without bound: with sending at 500/min, a single key reaches 500 sends a minute, and all of the account's keys together, 1,500.

The dashboard stays outside that ceiling: a signed-in person spends only their own bucket. A key stuck in a loop can exhaust the account's ceiling, but it does not lock you out of the dashboard — which is where you find and revoke that key.

Control headers​

Every response on a limited route carries the current state of the bucket:

HeaderContents
X-RateLimit-Categorysend, media or default
X-RateLimit-ScopeThe bucket described: key (API key), session (dashboard) or account
X-RateLimit-LimitThat bucket's ceiling for the window
X-RateLimit-RemainingHow much is left

On a successful response, the headers describe your credential's bucket. On a 429, they describe the bucket that refused — which may be the account's.

HTTP/1.1 201 Created
X-RateLimit-Category: send
X-RateLimit-Scope: key
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 487

Watch X-RateLimit-Remaining and slow down before you hit the ceiling — it's cheaper than handling 429.

When the limit is hit​

HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Category: send
X-RateLimit-Scope: key
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 0
{
"error": {
"message": "Rate limit exceeded for send requests with this credential. Try again in 12s.",
"type": "rate_limit",
"scope": "key"
}
}

The error.scope field repeats X-RateLimit-Scope. When it says account, your key still had quota: it was the account's other keys, added up, that used up the ceiling. Spreading the calls over more keys does not help — only waiting does.

The Retry-After header says exactly how many seconds to wait. Respect it — pushing earlier only burns quota.

Handling the 429​

Node.js — retry honouring Retry-After
async function send(payload, attempt = 0) {
const response = await fetch('https://api.joinotify.com/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.JOINOTIFY_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});

if (response.status === 429 && attempt < 3) {
const wait = Number(response.headers.get('retry-after') ?? 5);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
return send(payload, attempt + 1);
}

return response.json();
}

For bulk sending, prefer a rate-controlled queue (say 8 messages per second, well under 500/min) over firing everything and reacting to the error.

Meta's limits apply too​

The numbers above are Joinotify's limits. Underneath, Meta enforces its own — the cap on business-initiated messages in 24 hours, which varies with your number's quality and verification tier.

When it is Meta that refuses, the response comes back as meta_error with their original body in error.meta, not as rate_limit.

Need more?​

Limits are configurable per account. If your volume demands more, contact support with an estimate of your peak per minute.