Company API v1 reference
Every company API v1 endpoint under /api/company/v1: merchants, couriers, parcels, creating a parcel, assigning a courier and updating status, with parameters, responses and curl, JavaScript, PHP and Python samples.
Last updated 22 October 2026 · Last verified 22 October 2026
Basics#
The company API lets a delivery company's own system read its merchants, couriers and parcels, create parcels for its merchants, assign couriers and update parcel statuses.
- Base URL: your platform domain plus
/api/company/v1. The Integrations and API page shows it as Base URL at the bottom of the API key card. - Authentication:
Authorization: Bearer YOUR_COMPANY_API_KEY, theck_key of your company (see Authentication and API keys). The company needs an active subscription and a plan that includes the API feature; platform-owned and merchant-owned companies are exempt from both. - Scope: everything is limited to your company. Parcels are matched by the company on the parcel, merchants by the company of the merchant.
- Format: JSON in and out. Send
Content-Type: application/jsonwith bodies and alwaysAccept: application/json; addAccept-Language: enfor English messages. The envelope is{"success": true, "data": ...}(metaonly onGET /merchantsandGET /parcels) and{"success": false, "error": {"code", "message"}}.
Endpoints at a glance#
| Method | Path | Purpose | Route limit per minute |
|---|---|---|---|
| GET | /me |
Company, plan and usage | 300 |
| GET | /merchants |
Your merchants with their pickup locations | 300 |
| GET | /deliverymen |
Your couriers with their load | 300 |
| GET | /parcels |
List your parcels | 300 |
| GET | /parcels/{tracking} |
One parcel with its events | 300 |
| POST | /parcels |
Create a parcel for one of your merchants | 120 |
| POST | /parcels/{tracking}/assign |
Assign a courier | 300 |
| POST | /parcels/{tracking}/status |
Update the status | 300 |
Route limits are counted per IP address on one shared counter: a POST /parcels request counts against both the group limit of 300 and its own limit of 120, so these are not separate quotas. A general limit of 60 requests per minute per IP address also applies (see the API overview). Because a POST /parcels request is counted twice, one IP address can in practice create at most about 60 parcels per minute, and fewer if it also makes other calls in that minute.
Account and lists#
GET /me#
{
"success": true,
"data": {
"company": { "id": 13, "name": "Example Delivery", "phone": "0944000000" },
"plan": {
"name": "Pro",
"expires_at": "2026-12-01 00:00:00",
"features": [ { "feature": "Company API — 120 per minute / 20,000 per day", "enabled": true } ]
},
"usage": [ { "name": "API requests today", "used": 1240, "limit": 20000 } ]
}
}
plan is null when there is no active subscription. features[].feature is a display label in the request language. usage rows have name, used and limit (null means no limit), for example API requests today, webhook URLs and merchants.
GET /merchants#
Your merchants. Query parameters: q (part of the business name or mobile number), per_page (1 to 100, default 50) and page. The answer has meta. Each pickup location has id, name, address, phone, default_shop (a 0 or 1 flag) and default (true or false).
{
"success": true,
"data": [
{
"id": 41, "business_name": "Flower Shop", "contact_name": "Ahmad", "phone": "0933000000", "email": "shop@example.com", "active": true,
"pickup_locations": [ { "id": 7, "name": "Main branch", "address": "Damascus, Mazzeh", "phone": "0933000000", "default_shop": 1, "default": true } ]
}
],
"meta": { "page": 1, "per_page": 50, "total": 12, "last_page": 1 }
}
GET /deliverymen#
All your couriers; this list is not paginated.
{
"success": true,
"data": [
{ "id": 5, "name": "Samer", "phone": "0944000001", "active": true, "open_parcels": 12, "parcel_limit": 50, "online": true, "available": true }
]
}
open_parcelscounts the parcels in status Delivery Man Assign or Delivery Re-Schedule that have an event for this courier, an approximation of what the courier holds (the assign dialog in the panel counts the same way);parcel_limitis the courier's limit, 5 when none is set.onlineis the courier's duty flag.availableis false when the courier is not active, or when your company requires couriers to be online and this one is not.
Parcels#
GET /parcels#
Lists the parcels of your company, newest first, without events. Query parameters:
| Parameter | Meaning |
|---|---|
merchant_id, deliveryman_id |
Only this merchant or courier |
reference |
Exact match on the merchant's reference |
from, to |
Creation date range, YYYY-MM-DD |
updated_since |
Parcels updated at or after this time, for example 2026-10-13 10:00:00; useful for synchronisation |
stage |
pending, processing, out_for_delivery, delivered, partially_delivered, returning or returned. Any other value gives 422 and validation_failed |
per_page, page |
1 to 100 per page (default 20) |
stage=processing matches only the status codes 2, 3, 4, 5, 6 and 19 (pickup assigned or rescheduled, received by the pickup courier, received at the warehouse, transferring to a hub, received by the hub). A parcel object has stage processing for every status not listed under the other stages, so some processing parcels are not returned by this filter. Among them, only parcels at the warehouse or hub (5 and 19) can be assigned a courier.
GET /parcels/{tracking}#
Takes a tracking number or a reference (the newest match among the parcels of all your merchants wins). The answer is the parcel object plus events. An unknown parcel gives 404 and not_found.
POST /parcels#
Creates a parcel on behalf of one of your merchants. The fee is calculated with your company's pricing. Nothing is taken from platform wallets: it is deducted from the merchant's balance with your company only when the merchant's delivery-fee setting is "From their balance in advance" (billing mode wallet); with "From COD" nothing is deducted at creation. The fields are the same as in the Merchant API reference (recipient.name, recipient.phone, recipient.address, recipient.governorate, recipient.lat, recipient.lng, reference, cod_amount, currency, weight, pickup_location_id, note, shipping_paid_by) with the same validation rules, except:
merchant_idis required and must be one of your merchants (GET /merchants); otherwise422andvalidation_failedwith the fieldmerchant_id.- There is no
company_id: the parcel belongs to your company.use_own_account,warehouse_itemsandwait_for_balanceare not supported. - The governorate or the coordinates are required according to your company's pricing mode, as described in the Merchant API reference.
- A
referencethat the merchant already used gives409andduplicate_reference; the API does not return the old parcel. Fetch it withGET /parcels?merchant_id=<id>&reference=<reference>(or with its tracking number). A bareGET /parcels/{reference}matches the parcels of all your merchants and returns the newest match, so it can return another merchant's parcel when two merchants use the same reference. - The answer is
201with the parcel object.
POST /parcels/{tracking}/assign#
Assigns a courier of your company.
{ "deliveryman_id": 5, "note": "Call before arriving", "force": false }
deliveryman_idis required and must be one of your couriers (422andvalidation_failedotherwise).- When the courier already holds
parcel_limitopen parcels you get409anddeliveryman_full; send"force": trueto go beyond the limit. - If your company requires couriers to be online and this one is offline, you get
422on the fielddelivery_man_id. - A parcel already with that courier is returned unchanged.
- Assign parcels that are at the warehouse or hub (status Received Warehouse or Received by hub) or already assigned (Delivery Man Assign or Delivery Re-Schedule). A parcel whose status has no transition to Delivery Man Assign, such as a Pending one or one still in pickup, is rejected (see Common mistakes).
POST /parcels/{tracking}/status#
Updates the parcel status.
{ "action": "partial", "collected": 15, "note": "One item returned" }
action |
Result | Works when the parcel is |
|---|---|---|
received |
Received Warehouse | Pending, in pickup, or with a courier (sent back) |
deliver |
Delivered | With a courier (Delivery Man Assign or Delivery Re-Schedule) |
partial |
Partly delivered; collected is required and cannot exceed the COD amount |
With a courier |
fail |
Return to Courier (stage returning) |
At the warehouse or hub, or with a courier |
reschedule |
Back to the warehouse for a new dispatch; counts one attempt | With a courier |
returned |
Return received by merchant (final) | After fail |
- Parcels sent to an external shipping carrier cannot be changed here:
409andnot_allowed. - An unknown
actiongives422andvalidation_failed;partialwithout a numericcollectedgives422oncollected. - A change the workflow does not allow, or a
collectedabove the COD amount, gives409andinvalid_transitionwith the reason in the message.
The parcel object#
Parcels returned by this API have every field of the merchant API parcel object (id, tracking_number, reference, stage, status_code, status_label, company_id, company_name, external_tracking_number, recipient, cod_amount, cod_collected, weight_kg, charges, delivery_attempts, note, tracking_url, created_at, updated_at) plus:
merchant:{"id", "name"}with the merchant's business name.deliveryman:{"id", "name"}, ornullwhen no courier holds the parcel.events: only when you fetch one parcel; each hasstatus_code,stage,status_label,note,at.
stage is one of pending (status 1), processing (any status not listed below), out_for_delivery (7, 8), delivered (9), partially_delivered (32), returning (11, 12, 24, 26, 27) and returned (13, 30). cod_amount is in US dollars. The same object is what your webhooks carry in data.
{
"success": true,
"data": {
"id": 5521,
"tracking_number": "DS261013AB12CD",
"reference": "ORD-1001",
"stage": "out_for_delivery",
"status_code": 7,
"status_label": "Delivery Man Assign",
"company_id": 13,
"company_name": "Example Delivery",
"external_tracking_number": null,
"recipient": { "name": "Sara", "phone": "0944123456", "governorate": "Damascus", "address": "Mazzeh, building 4", "lat": null, "lng": null },
"cod_amount": 25,
"cod_collected": 0,
"weight_kg": 1,
"charges": { "delivery_fee": 6, "platform_fee": 0 },
"delivery_attempts": 0,
"note": null,
"tracking_url": "https://YOUR_DOMAIN/t/DS261013AB12CD",
"created_at": "2026-10-13T10:00:00+03:00",
"updated_at": "2026-10-13T12:10:00+03:00",
"merchant": { "id": 41, "name": "Flower Shop" },
"deliveryman": { "id": 5, "name": "Samer" }
}
}
Code samples#
Create a parcel for one of your merchants#
curl -X POST https://YOUR_DOMAIN/api/company/v1/parcels \
-H "Authorization: Bearer YOUR_COMPANY_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"merchant_id": 41,
"reference": "ORD-1001",
"recipient": {
"name": "Sara",
"phone": "0944123456",
"governorate": "Damascus",
"address": "Mazzeh, building 4"
},
"cod_amount": 25,
"weight": 1
}'
const BASE = "https://YOUR_DOMAIN/api/company/v1";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": `Bearer ${process.env.DS_COMPANY_KEY}`,
"Content-Type": "application/json",
"Accept": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!json.success) {
throw new Error(`${res.status} ${json.error.code}: ${json.error.message}`);
}
return json.data;
}
async function createParcel() {
const parcel = await call("POST", "/parcels", {
merchant_id: 41,
reference: "ORD-1001",
recipient: { name: "Sara", phone: "0944123456", governorate: "Damascus", address: "Mazzeh, building 4" },
cod_amount: 25,
weight: 1,
});
console.log(parcel.tracking_number, parcel.stage, parcel.merchant.name);
}
createParcel();
<?php
function ds_call(string $method, string $path, ?array $body = null): array
{
$options = [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DS_COMPANY_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
];
if ($body !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($body);
}
$ch = curl_init('https://YOUR_DOMAIN/api/company/v1' . $path);
curl_setopt_array($ch, $options);
$res = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!($res['success'] ?? false)) {
throw new RuntimeException("$status {$res['error']['code']}: {$res['error']['message']}");
}
return $res['data'];
}
$parcel = ds_call('POST', '/parcels', [
'merchant_id' => 41,
'reference' => 'ORD-1001',
'recipient' => ['name' => 'Sara', 'phone' => '0944123456', 'governorate' => 'Damascus', 'address' => 'Mazzeh, building 4'],
'cod_amount' => 25,
'weight' => 1,
]);
echo $parcel['tracking_number'], "\n";
import os
import requests
BASE = "https://YOUR_DOMAIN/api/company/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DS_COMPANY_KEY']}", "Accept": "application/json"}
def call(method, path, body=None):
r = requests.request(method, BASE + path, json=body, headers=HEADERS, timeout=30)
data = r.json()
if not data["success"]:
raise RuntimeError(f"{r.status_code} {data['error']['code']}: {data['error']['message']}")
return data["data"]
parcel = call("POST", "/parcels", {
"merchant_id": 41,
"reference": "ORD-1001",
"recipient": {"name": "Sara", "phone": "0944123456", "governorate": "Damascus", "address": "Mazzeh, building 4"},
"cod_amount": 25,
"weight": 1,
})
print(parcel["tracking_number"], parcel["stage"])
List, assign and deliver#
curl "https://YOUR_DOMAIN/api/company/v1/parcels?stage=processing&per_page=100" \
-H "Authorization: Bearer YOUR_COMPANY_API_KEY" -H "Accept: application/json"
curl -X POST https://YOUR_DOMAIN/api/company/v1/parcels/DS261013AB12CD/assign \
-H "Authorization: Bearer YOUR_COMPANY_API_KEY" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{ "deliveryman_id": 5, "note": "Call before arriving" }'
curl -X POST https://YOUR_DOMAIN/api/company/v1/parcels/DS261013AB12CD/status \
-H "Authorization: Bearer YOUR_COMPANY_API_KEY" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{ "action": "deliver", "note": "Received by the family" }'
const BASE = "https://YOUR_DOMAIN/api/company/v1";
const HEADERS = {
"Authorization": `Bearer ${process.env.DS_COMPANY_KEY}`,
"Content-Type": "application/json",
"Accept": "application/json",
};
async function call(method, path, body) {
const res = await fetch(BASE + path, { method, headers: HEADERS, body: body ? JSON.stringify(body) : undefined });
const json = await res.json();
if (!json.success) {
throw new Error(`${res.status} ${json.error.code}: ${json.error.message}`);
}
return json;
}
async function dispatchAndDeliver() {
const list = await call("GET", "/parcels?stage=processing&per_page=100");
// assign only parcels at the warehouse (5) or hub (19)
for (const p of list.data.filter((x) => [5, 19].includes(x.status_code))) {
try {
await call("POST", `/parcels/${p.tracking_number}/assign`, { deliveryman_id: 5 });
} catch (e) {
console.error(p.tracking_number, e.message); // for example 409 deliveryman_full
}
}
const done = await call("POST", "/parcels/DS261013AB12CD/status", { action: "deliver", note: "Received by the family" });
console.log(done.data.stage, done.data.deliveryman);
}
dispatchAndDeliver();
<?php
function ds_call(string $method, string $path, ?array $body = null): array
{
$options = [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DS_COMPANY_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
];
if ($body !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($body);
}
$ch = curl_init('https://YOUR_DOMAIN/api/company/v1' . $path);
curl_setopt_array($ch, $options);
$res = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!($res['success'] ?? false)) {
throw new RuntimeException("$status {$res['error']['code']}: {$res['error']['message']}");
}
return $res;
}
$list = ds_call('GET', '/parcels?stage=processing&per_page=100');
foreach ($list['data'] as $p) {
if (!in_array($p['status_code'], [5, 19], true)) {
continue; // assign only parcels at the warehouse (5) or hub (19)
}
try {
ds_call('POST', "/parcels/{$p['tracking_number']}/assign", ['deliveryman_id' => 5]);
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n"; // for example 409 deliveryman_full
}
}
$done = ds_call('POST', '/parcels/DS261013AB12CD/status', ['action' => 'deliver', 'note' => 'Received by the family']);
echo $done['data']['stage'], "\n";
import os
import requests
BASE = "https://YOUR_DOMAIN/api/company/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DS_COMPANY_KEY']}", "Accept": "application/json"}
def call(method, path, body=None, params=None):
r = requests.request(method, BASE + path, json=body, params=params, headers=HEADERS, timeout=30)
data = r.json()
if not data["success"]:
raise RuntimeError(f"{r.status_code} {data['error']['code']}: {data['error']['message']}")
return data
parcels = call("GET", "/parcels", params={"stage": "processing", "per_page": 100})["data"]
for p in parcels:
if p["status_code"] not in (5, 19):
continue # assign only parcels at the warehouse (5) or hub (19)
try:
call("POST", f"/parcels/{p['tracking_number']}/assign", {"deliveryman_id": 5})
except RuntimeError as e:
print(e) # for example 409 deliveryman_full
done = call("POST", "/parcels/DS261013AB12CD/status", {"action": "deliver", "note": "Received by the family"})
print(done["data"]["stage"], done["data"]["deliveryman"])
Errors specific to this API#
| HTTP | error.code |
When |
|---|---|---|
| 402 | subscription_expired |
Your company subscription has ended |
| 402 | insufficient_balance |
The balance is not enough for the parcel fee |
| 403 | plan_feature |
Your plan does not include the API |
| 409 | duplicate_reference |
The merchant already has a parcel with this reference |
| 409 | deliveryman_full |
The courier reached the parcel limit |
| 409 | not_allowed |
The parcel is with an external carrier |
| 409 | invalid_transition |
The action does not fit the current status, or collected is too high |
| 422 | validation_failed |
A missing or wrong field; fields lists them (an unknown stage, a missing or foreign deliveryman_id and an unknown action carry only the message) |
The other codes are in Errors and status codes.
Common mistakes#
- Creating a parcel for a merchant that is not yours.
422onmerchant_id; take ids fromGET /merchants. - Repeating a
reference.409andduplicate_reference, not the old parcel as in the merchant API. - Marking a parcel delivered before assigning a courier.
409andinvalid_transition; assign first. - Assigning a parcel whose status cannot move to Delivery Man Assign, for example a pending one or one still in pickup. The workflow refuses it and the API answers
500andserver_errorwith areference; checkstatus_codefirst. - Changing the status of a carrier parcel.
409andnot_allowed; its status follows the carrier. - Filtering with an unknown
stage.422, unlike the merchant API, which ignores it. - Reading
cod_amountin the currency the merchant entered. It is in US dollars.
Limits and permissions#
- Only the company key works here, and only while the company subscription is active and the plan includes the API feature (platform-owned and merchant-owned companies are exempt from both).
- Route limits are in the table above (one shared counter per IP address, not separate quotas). Plan limits per minute and per day return
429andrate_limitedwithretry_after; the general and route limits return429with{"message": "Too Many Attempts."}. - The company API has no webhook endpoints; manage company webhooks in the panel (Integrations and API, then Webhook). They carry parcel events only, for the parcels of your company.
API
GET /api/company/v1/meGET /api/company/v1/merchantsGET /api/company/v1/deliverymenGET /api/company/v1/parcelsGET /api/company/v1/parcels/{tracking}POST /api/company/v1/parcelsPOST /api/company/v1/parcels/{tracking}/assignPOST /api/company/v1/parcels/{tracking}/status