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 requests (per IP — e.g. GET /plans) | 60 requests per minute |
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. Try again in 14 seconds."
}
}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). Your tenant admin receives an alert when usage crosses 80% and
100% of the cap.
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. (Push-style webhooks are planned — see Webhooks.)