Rate Limiting
The Arsel API enforces per-organization rate limits to ensure fair usage and platform stability. The limit is shared across every API key in the organization — adding more keys does not raise the ceiling.
Default Limits
The API allows 1,000 requests per 60-second window per organization (about 16 requests per second on average). It is a fixed window, so the full budget can be spent at any point inside the 60 seconds.
POST /events/send is the one exception — it is budgeted in events, not requests. See Event ingestion below.
Need more? Contact support@arsel.sa.
Event ingestion
POST /events/send has its own budget of 6,000 events per 60-second window per organization (about 100 events per second on average). It does not consume the 1,000-request budget above.
The unit is events, not requests: a batch of 50 events spends 50. This is deliberate — it means the ceiling describes the same amount of data whether your client batches events or sends them one at a time.
POST /events/send { "event": "order.completed", ... } → costs 1
POST /events/send { "events": [ <50 items> ] } → costs 50
Batching is still worth doing — it is far fewer round trips, and one Idempotency-Key covers the whole batch — but it does not buy a discount against this limit.
The budget is shared across every credential in the organization, secret keys (be_…) and publishable client keys (pub_…) alike.
This page covers the 60-second-window request and event limits. Your plan also has a monthly send quota per channel. Exhausting the monthly quota returns 429 monthly_quota_exceeded — a separate error that retrying won't resolve. See Common Send Errors.
Rate Limit Headers
Every API response includes standard rate limit headers:
| Header | Description |
|---|---|
ratelimit-limit | Maximum allowed in the current window — 1000 requests, or 6000 events on /events/send |
ratelimit-remaining | How much of that budget is left in the current window |
ratelimit-reset | Seconds remaining until the window resets (a duration, not a Unix timestamp) |
On /events/send these count events, matching that endpoint's budget. Reading
ratelimit-remaining is the only reliable way to know your position there: a request costs
between 1 and 50 depending on how many events it carries, so counting your own calls tells you
nothing.
ratelimit-* headers are not returned on a 429 response — only retry-after is. Do not build your backoff around reading ratelimit-remaining from a 429; on a 429, wait for retry-after seconds.
When a rate limit is exceeded, this header is included instead:
| Header | Description |
|---|---|
retry-after | Seconds to wait before retrying |
Rate Limit Exceeded
If you exceed the limit, the API returns HTTP 429:
{
"status_code": 429,
"name": "rate_limit_exceeded",
"message": "Too many requests. Please retry after the period indicated in the retry-after header."
}
Match on status_code and name — the exact message string is not a stable contract and may change.
Handling Rate Limits
Read the headers on successful responses. Check ratelimit-remaining on 2xx responses to pace yourself — especially on /events/send, where request count is not a proxy for budget spent. On a 429, these headers are absent — use retry-after instead.
Wait for retry-after. When you receive a 429, wait the number of seconds in the retry-after header before retrying. Avoid tight retry loops.
Retry safely with an idempotency key. A retried send or event can be processed twice — double-counting rollups and revenue. Send an Idempotency-Key header so a retry within 24 hours returns the original result instead of repeating the side effect. See Idempotency for the endpoints that accept it.
Batch where a batch endpoint exists. Three endpoints take many items in one request:
| Endpoint | Items per request | Cost against the limit |
|---|---|---|
POST /events/send | 50 | one per event |
POST /push/devices/bulk | 1,000 | one call |
POST /contacts/bulk-delete | 1,000 | one call |
Everything else is one call per item, so space a bulk push out rather than emptying the 60-second budget at once.