API rate limits as actually enforced
The limits the code really enforces on /api: 60 requests per minute per IP address, route limits, plan limits per minute and per day, and what a 429 looks like. The panel's older per-key figures (120 and 300) are not enforced per key; the real limits are per IP address plus your plan limits.
Last updated 22 October 2026 · Last verified 22 October 2026
Purpose#
This page lists the request limits that the code actually enforces on the merchant API (/api/v1) and the delivery company API (/api/company/v1).
Who is affected#
- Merchants calling
/api/v1with their API key. Only platform merchants can use this API; a merchant created by a delivery company receives403 api_not_allowed. - Delivery company admins calling
/api/company/v1with the company key.
The limits that are enforced#
| Layer | Applies to | Counted per | Limit |
|---|---|---|---|
| 1. Global API limiter | every /api/* request |
IP address | 60 per minute |
| 2. Route limits, merchant API | the whole /api/v1 group; then POST /parcels, GET /statement, POST /webhooks and POST /webhooks/{id}/test |
IP address | 120; then 60, 30, 20 and 10 per minute |
| 3. Route limits, company API | the whole /api/company/v1 group; then POST /parcels |
IP address | 300; then 120 per minute |
| 4. Plan limit per minute | every call made with the account's key | account | api_per_minute of the plan |
| 5. Plan limit per day | every call made with the account's key | account | api_per_day of the plan |
How the layers combine:
- The first ceiling you reach stops the request before the requested action runs, so nothing is created.
- Layers 1 to 3 are tied to the IP address, not to the API key, so everything behind the same public IP (an office, a hosting server, a NAT, even two keys on one server) shares them. Layer 1 allows only 60 requests per minute across all of
/api/*, so the group limits of 120 and 300 are almost never reached (the counters are separate, so this is not guaranteed); the lower route limits (60, 30, 20, 10) can be reached. - The route limits have no counter per route: they all count on one per-IP counter, and a call to a route that has its own limit (merchant
POST /parcels,GET /statement,POST /webhooks,POST /webhooks/{id}/test; companyPOST /parcels) is counted twice, at group level and at route level. Its practical ceiling is therefore about half the number in the table (for example roughly 15GET /statementcalls per minute, not 30, if nothing else is called from that IP). - Each counter is a fixed window: it starts with the first counted request and resets after 60 seconds. The daily plan counter resets at 00:00 UTC.
Plan limits#
- A new plan form is pre-filled with 60 requests per minute and 5,000 per day for merchants, and 120 per minute and 20,000 per day for companies; an administrator can change them for any plan.
- An empty value or
0means no limit. A legacy plan whose features were never configured has no limits at all. - Merchant limits apply only to platform merchants, and only while at least one merchant plan is active on the platform.
- To see your limits: a merchant opens the plans page (the plan card lists the API figures and "Your plan usage" shows "API requests today"); a company admin sees them on "Integrations and API", and
GET /api/company/v1/mereturns the same usage rows (name,used,limit;limitisnullwhen unlimited).
What a rejected request looks like#
Plan limit (layers 4 and 5). HTTP 429 with the usual API error envelope and a Retry-After header in seconds:
{
"success": false,
"error": {
"code": "rate_limited",
"message": "Localized text with the limit and the wait time",
"retry_after": 42
}
}
IP and route limits (layers 1 to 3). HTTP 429 from the framework, before the API code runs. With Accept: application/json you get a JSON body whose message is Too Many Attempts. together with the headers Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Without it (curl sends Accept: */*) you get an HTML error page and no Retry-After.
Successful responses also carry X-RateLimit-Limit and X-RateLimit-Remaining for these IP limits (the one with the fewest requests left), so you can slow down before the first 429. Plan limits are not reflected in those headers.
The error.message of the plan-limit response is localized (send Accept-Language: en for English; the default is Arabic), while the Too Many Attempts. message of the IP and route limits is always English. Build your logic on the HTTP status and the Retry-After header (and on error.code for plan limits), not on the message text.
Older numbers in the panel#
The panel's own API documentation promises per-key limits, but the code has no per-key limit: "120 requests per minute per key, of which 60 for creating parcels" (merchant API page) and "a global limit of 300 requests per minute per key, of which 120 for creating parcels" (company API page). The merchant page also says an active plan is needed "to create parcels"; in fact, when plans apply, it is needed for every endpoint (see "Permissions and requirements"). What applies is the table above.
Handling limits in your client#
Ask for JSON
Send
Accept: application/jsonon every call so that every 429 carries a JSON body and aRetry-Afterheader.Wait for Retry-After, then retry
On 429 pause for the number of seconds in
Retry-After(orerror.retry_after), then retry. Add a random delay if several workers run from one IP address, because they share the counter.Make creation retries safe
Send a
referencewithPOST /parcels. On the merchant API, sending the same reference again (one retry at a time) returns the existing parcel (200 with"duplicate": true) instead of creating another; refunded or expired parcels are ignored by this check. On the company API a repeated reference for the same merchant gives409 duplicate_reference: fetch the parcel withGET /api/company/v1/parcels/{reference}.Poll less
Use webhooks for status changes, and cache lists that rarely change, such as
/governorates,/companiesand/pickup-locations.
Permissions and requirements#
- Each account has one key: a merchant generates it with "Generate" on the "Developer API" row of "Webhook and API"; a company admin generates it on "Integrations and API".
- When plans apply to your account, the plan flag
apiof your active plan must be on for every/api/v1endpoint (not only for creating parcels), otherwise the response is403 plan_feature; a merchant with no active subscription gets it too. For companies, an expired subscription gives402 subscription_expired. - A suspended account receives
403 account_inactive.
API
GET /api/v1/parcelsPOST /api/v1/parcelsGET /api/v1/statementPOST /api/v1/webhooksGET /api/company/v1/mePOST /api/company/v1/parcels