Errors and status codes
The real HTTP status and error codes of the merchant and company APIs: the error shape, the order of the checks, a table with the meaning and fix of each code, 422 validation errors and how to handle them.
Last updated 22 October 2026 · Last verified 22 October 2026
Purpose#
This page lists the HTTP status codes and error codes that the merchant API (/api/v1) and the company API (/api/company/v1) really return, what each one means and how to fix it. Build your error handling on error.code.
The error shape#
Success is 200, or 201 when a new resource is created, such as a parcel or a webhook endpoint. A 200 with "duplicate": true means that the reference already exists (merchant API). Every error raised by the API itself looks like this:
{
"success": false,
"error": {
"code": "validation_failed",
"message": "...",
"fields": { "customer_phone": ["..."] }
}
}
codeis a short snake_case code: the value to program against.messageis text in the request language: Arabic by default, English withAccept-Language: en. A few messages exist only in Arabic, so never branch on the text.fieldsappears on most validation errors: field names with their messages, andmessagenormally repeats the first of those messages (the exception is the company APImerchant_iderror:fields.merchant_idsays "Merchant not found in your company." whilemessagehas the longermerchant_idtext shown in the validation table). A few company API errors (an unknownstage, a missing or foreigndeliveryman_id, an unknownaction) carry onlymessage. The field names are the platform's internal ones (customer_name,customer_phone,customer_address,customer_city,lat,long, andshop_idinstead ofpickup_location_id).retry_after(seconds) appears onrate_limited,referenceonserver_errorandresponse_statusondelivery_failed.
The order of the checks#
Each call goes through these checks and stops at the first failure. After them, the endpoint applies its own validation and business rules.
| Step | Merchant API | Company API |
|---|---|---|
| 1 | Key present: unauthenticated |
Key present: unauthenticated |
| 2 | Key valid: invalid_key |
Key valid: invalid_key |
| 3 | Account active: account_inactive |
Account and company active: account_inactive |
| 4 | Not a company merchant: api_not_allowed |
Subscription active: subscription_expired |
| 5 | Plan includes the API: plan_feature |
Plan includes the API: plan_feature |
| 6 | Plan limit: rate_limited |
Plan limit: rate_limited |
Status codes and error codes#
| HTTP | error.code |
API | Meaning | How to fix |
|---|---|---|---|---|
| 401 | unauthenticated |
both | No key in an Authorization: Bearer header |
Send the key in that header |
| 401 | invalid_key |
both | The key is unknown: renewed, revoked, mistyped or of another type | Copy the current key from the panel; use the Developer API key (sk_) on /api/v1 and the company key (ck_) on /api/company/v1 |
| 402 | insufficient_balance |
both | The balance is not enough; the message gives the required and available amounts | Top up the wallet, or send wait_for_balance: true (merchant API) |
| 402 | subscription_expired |
company | The company subscription has ended | Renew it in the company panel |
| 403 | account_inactive |
both | The key's user, or the merchant or company, is suspended or missing | Contact the platform administrator |
| 403 | api_not_allowed |
merchant | The merchant belongs to a delivery company | Use a platform merchant account |
| 403 | plan_feature |
both | The plan lacks the feature (the API, or webhooks for POST /webhooks), or the merchant has no active plan |
Subscribe or upgrade; the message names the feature |
| 404 | not_found |
both | No parcel with this tracking number or reference in your account or company, or no such webhook | Check the value; a cancelled parcel no longer exists |
| 409 | not_cancellable |
merchant | The parcel left the Pending or Received Warehouse status, or went to an external carrier | Only a Pending or Received Warehouse parcel that was not sent to an external carrier can be cancelled |
| 409 | duplicate_reference |
company | The merchant already has a parcel with this reference |
Fetch it with GET /parcels?merchant_id=<id>&reference=<reference> (a bare GET /parcels/{reference} searches all your merchants) |
| 409 | deliveryman_full |
company | The courier holds the maximum number of open parcels | Pick another courier, or send force: true |
| 409 | not_allowed |
company | The parcel is with an external carrier, which controls its status | Do not change its status here |
| 409 | invalid_transition |
company | The action does not fit the current status, or collected is above the COD amount |
Read the reason in the message |
| 422 | validation_failed |
both | A field is missing or wrong; fields lists them (a few company API cases carry only the message) |
See the next section |
| 422 | quote_failed |
merchant | The price could not be calculated | Check company_id and the recipient fields |
| 422 | carrier_error |
both | The external carrier refused the parcel, is inactive, or cannot be used (your own account is not linked, the currency or shipping_paid_by is not supported) |
Fix the cause named in the message |
| 422 | limit_reached |
merchant | You have the maximum number of webhook endpoints | Delete one or upgrade the plan |
| 422 | delivery_failed |
merchant | The test ping did not reach your URL; response_status may be included |
Check your URL and server |
| 429 | rate_limited |
both | The plan's per-minute or per-day limit was reached (the general and route limits answer without error.code, see Errors outside this shape) |
Wait retry_after seconds |
| 500 | server_error |
both | Unexpected error on the platform. The company API also returns it every time when you assign a courier to a parcel whose status cannot move to Delivery Man Assign | Retry later and give support the reference; for assign, check status_code first |
Two of these extra keys, retry_after and reference, look like this:
{
"success": false,
"error": {
"code": "rate_limited",
"message": "...",
"retry_after": 42
}
}
{
"success": false,
"error": {
"code": "server_error",
"message": "...",
"reference": "E3FA91C"
}
}
Validation errors (422)#
fields maps each failing field to its messages. These are the fields you meet most:
| Field | Typical cause | Example message |
|---|---|---|
company_id |
Missing, or the company is not available to you | "company_id is required — get the list from GET /companies" |
customer_name, customer_phone, customer_address |
Missing; the phone allows 6 to 30 characters of digits, +, spaces, - and parentheses |
Standard validation text |
customer_city |
The governorate cannot be found, the company has no price for it, or it is outside the coverage | "The delivery company prices by governorate: we could not determine the recipient governorate from the address «...» — choose the governorate from the list." |
lat, long |
Out of range, or distance pricing needs the recipient location | "The delivery company prices by distance: set the recipient location on the map." |
shop_id |
The pickup location is not yours, you have none, it has no map location for distance pricing, or a warehouse booking ended | "The selected pickup location does not belong to you" |
warehouse_items |
The pickup location is a warehouse and items are missing or exceed the stock | Message names the item and the quantity |
weight |
Outside 0.01 to 1000, or above the company's maximum | "“...” accepts parcels up to N kg — this parcel is M kg." |
cod_amount |
Negative or above 100,000,000 | Standard validation text |
cod_currency or currency |
Unsupported currency, not accepted by the company, or no exchange rate | "“...” accepts amounts in: ... — change the parcel currency." |
shipping_paid_by |
Not sender, customer or included_in_cod |
Standard validation text |
plan |
The parcel quota of the merchant plan is used up (merchant API only; an expired or missing plan gives 403 and plan_feature instead) |
"You have reached your plan limit: N parcels during the subscription period (until ...), and you used M. Upgrade your plan or renew the subscription to continue." |
merchant_id |
Company API: not one of your merchants | message: "merchant_id is required and must be a merchant of your company (GET /merchants)."; fields.merchant_id: "Merchant not found in your company." |
collected |
Company API, partial without a number |
"Partial delivery requires collected (the amount collected)." |
delivery_man_id |
Company API: your company requires couriers to be online and this one is not | Message asks the courier to press online in the courier app |
url, events, name, active |
Merchant API webhook rules: https only, a public host, at least one event | "The URL must start with https://" |
Errors outside this shape#
A few errors are produced before the API code runs, so they do not use the envelope:
- An unknown path (
404), a wrong HTTP method (405) and the general and route request limits (429). WithAccept: application/jsonyou get a JSON body with amessage; without it you get an HTML page. - With the JSON header these limits answer with
{"message": "Too Many Attempts."}and the headersRetry-After,X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. A429can therefore come from three places: the general limit of 60 requests per minute per IP address and the per-route limits (noerror.code), and the plan limit (rate_limitedin the envelope). - Rarely, an unexpected failure that happens before the API's own error handling returns
{"success": false, "message": "..."}with500.
{ "message": "Too Many Attempts." }
Handling errors in your client#
Check the HTTP status first
A
4xxmeans your request must change, except429. A5xxis normally a problem on the platform side: retry later. One case does not go away by retrying: in the company API, assigning a courier to a parcel whose status cannot move to Delivery Man Assign (for example a Pending one or one still in pickup) returns500andserver_errorevery time, so check the parcel'sstatus_codebefore calling assign.Branch on error.code
Use
error.codefor logic,error.fieldsto highlight inputs anderror.messageonly to display to a person.Wait on 429
Pause for
error.retry_afterseconds, or for theRetry-Afterheader when the general or a route limit answered (noerror.code), then retry.Make creation retries safe
After a timeout or a
500on parcel creation, look the parcel up before creating it again: withGET /parcels/{reference}in the merchant API, and withGET /parcels?merchant_id=<id>&reference=<reference>in the company API (a bareGET /parcels/{reference}there searches all your merchants and can return another merchant's parcel). In the merchant API, sending the samereferenceagain returns the existing parcel with"duplicate": true.
Common mistakes#
- Treating
200with"duplicate": trueas a new parcel. It is the parcel you created before. - Expecting
409for a repeatedreferencein the merchant API. That is the company API; the merchant API returns the old parcel. - Retrying a
4xxunchanged. Fix the request first. - Parsing
error.message. It changes with the language; some messages are Arabic only. - Not sending
Accept: application/json. Errors raised before the API code runs (see "Errors outside this shape") then arrive as HTML pages. - Ignoring
fields. When present, it names the exact input that failed.
Limits and permissions#
plan_feature,rate_limitedandapi_not_alloweddepend on your plan and account type, not on the request body.- Only the languages
arandenare used for messages. - The codes marked merchant or company exist only on that API.
API
POST /api/v1/parcelsPOST /api/v1/parcels/{tracking}/cancelPOST /api/company/v1/parcelsPOST /api/company/v1/parcels/{tracking}/status