Rate Limits
The EmbayLMS API enforces two independent limits to ensure fair use and platform stability:
- A per-minute rate limit per API key
- A monthly usage cap per tenant, based on your subscription plan
Both return HTTP 429 when exceeded.
Per-Minute Rate Limit
| Request type | Limit |
|---|---|
| Authenticated requests (per API key) | 1,000 requests per minute |
| Unauthenticated public endpoints | GET /plans is not rate limited (the response is cached for 5 minutes); POST /contact and POST /signup are limited per IP address |
The limit is a fixed one-minute window applied per API key, not per IP address. Multiple integrations using separate API keys each get their own limit.
429 Too Many Requests
When the per-minute limit is exceeded, the API returns HTTP 429 with a
Retry-After header and the X-RateLimit-* headers describing the window:
HTTP/1.1 429 Too Many Requests
Retry-After: 14
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748995260| Header | Description |
|---|---|
Retry-After | Seconds until the window resets — wait this long before retrying |
X-RateLimit-Limit | Total requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window (0 when limited) |
X-RateLimit-Reset | Unix timestamp when the window resets |
The response body uses the standard error envelope with a numeric code (see Errors):
{
"data": null,
"meta": null,
"error": {
"code": 429,
"message": "Rate limit exceeded"
}
}Monthly Usage Cap
Every authenticated API call also counts against a monthly usage cap tied
to your subscription plan. When the cap is reached, requests return 429 with
a Retry-After header until the next calendar month (or until you upgrade
your plan). Every Owner and Admin of the tenant is emailed once per month when
usage crosses 80% and 100% of the cap (the check runs every 15 minutes), and
the billing portal’s API usage section shows the live count.
To raise the cap, upgrade your plan from your tenant’s billing portal or contact support.
Handling Rate Limits in Code
Always respect the Retry-After header, and add jitter so parallel workers do
not retry in lockstep.
Node.js with Exponential Backoff
async function apiRequestWithRetry(url, options, maxRetries = 5) {
let attempt = 0;
while (attempt <= maxRetries) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
const jitter = Math.random() * 1000; // add up to 1s jitter
const delay = retryAfter * 1000 + jitter;
console.warn(`Rate limited. Retrying in ${Math.round(delay / 1000)}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
attempt++;
}
throw new Error('Max retries exceeded');
}Python with httpx
import time
import httpx
def api_request_with_retry(client, url, max_retries=5, **kwargs):
for attempt in range(max_retries + 1):
response = client.get(url, **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response
retry_after = int(response.headers.get("Retry-After", "1"))
jitter = 0.5
delay = retry_after + jitter
print(f"Rate limited. Retrying in {delay}s (attempt {attempt + 1}/{max_retries})")
time.sleep(delay)
raise Exception("Max retries exceeded")Tips for Staying Under Limits
- Cache read responses — user lists change infrequently. Cache aggressively.
- Use the maximum page size — one request with
per_page=200costs a fifth of five requests withper_page=40, against both the per-minute limit and the monthly cap. - Spread requests over time — if you need to pull thousands of records, pace the requests rather than firing them all at once.
- Poll on a schedule, not a loop — a compliance sync that pulls
GET /completionshourly is far cheaper than polling every few seconds. Better yet, use push-style Webhooks instead of polling.