Reference Developers

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/v1 with their API key. Only platform merchants can use this API; a merchant created by a delivery company receives 403 api_not_allowed.
  • Delivery company admins calling /api/company/v1 with 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; company POST /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 15 GET /statement calls 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 0 means 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/me returns the same usage rows (name, used, limit; limit is null when 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:

json
{
  "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#

  1. Ask for JSON

    Send Accept: application/json on every call so that every 429 carries a JSON body and a Retry-After header.

  2. Wait for Retry-After, then retry

    On 429 pause for the number of seconds in Retry-After (or error.retry_after), then retry. Add a random delay if several workers run from one IP address, because they share the counter.

  3. Make creation retries safe

    Send a reference with POST /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 gives 409 duplicate_reference: fetch the parcel with GET /api/company/v1/parcels/{reference}.

  4. Poll less

    Use webhooks for status changes, and cache lists that rarely change, such as /governorates, /companies and /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 api of your active plan must be on for every /api/v1 endpoint (not only for creating parcels), otherwise the response is 403 plan_feature; a merchant with no active subscription gets it too. For companies, an expired subscription gives 402 subscription_expired.
  • A suspended account receives 403 account_inactive.

API

  • GET /api/v1/parcels
  • POST /api/v1/parcels
  • GET /api/v1/statement
  • POST /api/v1/webhooks
  • GET /api/company/v1/me
  • POST /api/company/v1/parcels