Reference Developers

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:

json
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "...",
    "fields": { "customer_phone": ["..."] }
  }
}
  • code is a short snake_case code: the value to program against.
  • message is text in the request language: Arabic by default, English with Accept-Language: en. A few messages exist only in Arabic, so never branch on the text.
  • fields appears on most validation errors: field names with their messages, and message normally repeats the first of those messages (the exception is the company API merchant_id error: fields.merchant_id says "Merchant not found in your company." while message has the longer merchant_id text shown in the validation table). A few company API errors (an unknown stage, a missing or foreign deliveryman_id, an unknown action) carry only message. The field names are the platform's internal ones (customer_name, customer_phone, customer_address, customer_city, lat, long, and shop_id instead of pickup_location_id).
  • retry_after (seconds) appears on rate_limited, reference on server_error and response_status on delivery_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:

json
{
  "success": false,
  "error": {
    "code": "rate_limited",
    "message": "...",
    "retry_after": 42
  }
}
json
{
  "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). With Accept: application/json you get a JSON body with a message; without it you get an HTML page.
  • With the JSON header these limits answer with {"message": "Too Many Attempts."} and the headers Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 can therefore come from three places: the general limit of 60 requests per minute per IP address and the per-route limits (no error.code), and the plan limit (rate_limited in the envelope).
  • Rarely, an unexpected failure that happens before the API's own error handling returns {"success": false, "message": "..."} with 500.
json
{ "message": "Too Many Attempts." }

Handling errors in your client#

  1. Check the HTTP status first

    A 4xx means your request must change, except 429. A 5xx is 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) returns 500 and server_error every time, so check the parcel's status_code before calling assign.

  2. Branch on error.code

    Use error.code for logic, error.fields to highlight inputs and error.message only to display to a person.

  3. Wait on 429

    Pause for error.retry_after seconds, or for the Retry-After header when the general or a route limit answered (no error.code), then retry.

  4. Make creation retries safe

    After a timeout or a 500 on parcel creation, look the parcel up before creating it again: with GET /parcels/{reference} in the merchant API, and with GET /parcels?merchant_id=<id>&reference=<reference> in the company API (a bare GET /parcels/{reference} there searches all your merchants and can return another merchant's parcel). In the merchant API, sending the same reference again returns the existing parcel with "duplicate": true.

Common mistakes#

  • Treating 200 with "duplicate": true as a new parcel. It is the parcel you created before.
  • Expecting 409 for a repeated reference in the merchant API. That is the company API; the merchant API returns the old parcel.
  • Retrying a 4xx unchanged. 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_limited and api_not_allowed depend on your plan and account type, not on the request body.
  • Only the languages ar and en are used for messages.
  • The codes marked merchant or company exist only on that API.

API

  • POST /api/v1/parcels
  • POST /api/v1/parcels/{tracking}/cancel
  • POST /api/company/v1/parcels
  • POST /api/company/v1/parcels/{tracking}/status