Reference Developers

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, the ck_ 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/json with bodies and always Accept: application/json; add Accept-Language: en for English messages. The envelope is {"success": true, "data": ...} (meta only on GET /merchants and GET /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#

json
{
  "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).

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

json
{
  "success": true,
  "data": [
    { "id": 5, "name": "Samer", "phone": "0944000001", "active": true, "open_parcels": 12, "parcel_limit": 50, "online": true, "available": true }
  ]
}
  • open_parcels counts 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_limit is the courier's limit, 5 when none is set.
  • online is the courier's duty flag. available is 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_id is required and must be one of your merchants (GET /merchants); otherwise 422 and validation_failed with the field merchant_id.
  • There is no company_id: the parcel belongs to your company. use_own_account, warehouse_items and wait_for_balance are not supported.
  • The governorate or the coordinates are required according to your company's pricing mode, as described in the Merchant API reference.
  • A reference that the merchant already used gives 409 and duplicate_reference; the API does not return the old parcel. Fetch it with GET /parcels?merchant_id=<id>&reference=<reference> (or with its tracking number). A bare GET /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 201 with the parcel object.

POST /parcels/{tracking}/assign#

Assigns a courier of your company.

json
{ "deliveryman_id": 5, "note": "Call before arriving", "force": false }
  • deliveryman_id is required and must be one of your couriers (422 and validation_failed otherwise).
  • When the courier already holds parcel_limit open parcels you get 409 and deliveryman_full; send "force": true to go beyond the limit.
  • If your company requires couriers to be online and this one is offline, you get 422 on the field delivery_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.

json
{ "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: 409 and not_allowed.
  • An unknown action gives 422 and validation_failed; partial without a numeric collected gives 422 on collected.
  • A change the workflow does not allow, or a collected above the COD amount, gives 409 and invalid_transition with 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"}, or null when no courier holds the parcel.
  • events: only when you fetch one parcel; each has status_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.

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

bash
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
  }'
javascript
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
<?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";
python
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#

bash
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" }'
javascript
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
<?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";
python
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. 422 on merchant_id; take ids from GET /merchants.
  • Repeating a reference. 409 and duplicate_reference, not the old parcel as in the merchant API.
  • Marking a parcel delivered before assigning a courier. 409 and invalid_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 500 and server_error with a reference; check status_code first.
  • Changing the status of a carrier parcel. 409 and not_allowed; its status follows the carrier.
  • Filtering with an unknown stage. 422, unlike the merchant API, which ignores it.
  • Reading cod_amount in 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 429 and rate_limited with retry_after; the general and route limits return 429 with {"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/me
  • GET /api/company/v1/merchants
  • GET /api/company/v1/deliverymen
  • GET /api/company/v1/parcels
  • GET /api/company/v1/parcels/{tracking}
  • POST /api/company/v1/parcels
  • POST /api/company/v1/parcels/{tracking}/assign
  • POST /api/company/v1/parcels/{tracking}/status