OpenAPI 3.1Version v1 17 endpoints · 11 webhook events

Delivery Syria Merchant API

Price, create, list, track and cancel parcels, read the wallet statement and manage webhook endpoints.

Import this file into Postman, Insomnia or any OpenAPI 3.1 tool, or give the tool the address below. The server in the file is relative (it has no host): after importing, set your platform address as the base URL.

URL
https://deliverysyria.com/docs/api/merchant-v1.json

Overview

The merchant API (/api/v1) lets the system of a platform merchant work with parcels and the wallet. Every call is authenticated with a Bearer API key and every answer is JSON in the same envelope: {"success": true, "data": ...} (plus meta on paginated lists) or {"success": false, "error": {"code", "message"}}. Always send Accept: application/json.

This document was written from the application code and is checked against the real routes, validation rules and response shapes by automated tests. It describes only what the code does. There is no versioning header, no Idempotency-Key support and no "try it out" console. Rate limits are described on each operation: requests per minute per IP address, plus the limits of your plan.

For guides and explanations read the developer articles of the documentation center.

Base URL
https://YOUR_DOMAIN/api/v1 Replace YOUR_DOMAIN with your platform address.
Authentication

Your merchant developer API key in the Authorization header: Authorization: Bearer YOUR_API_KEY. Only this header is read; a key in the query string or in the body is ignored. Create the key in the merchant panel (Webhook and API, then the Developer API key). It works only on /api/v1: other keys (store integrations, the company key) give invalid_key.

HTTP
Authorization: Bearer YOUR_API_KEY
Accept-Language

Language of error messages and labels (status labels, event notes, names). If the value starts with en the answer is in English; any other value or no header gives Arabic. Machine-readable fields never change.

Rate limits

Three layers apply. (1) A general limit of 60 requests per minute per IP address on every /api request. (2) Route limits, counted per IP address on ONE shared counter: the group limit of 120 per minute, and 60 / 30 / 20 / 10 for POST /parcels, GET /statement, POST /webhooks and POST /webhooks/{id}/test; because a request to a route with its own limit is counted by both the group and the route, a route with its own limit allows only about half of it from one IP address. (3) Plan limits per account, per minute and per day (429 rate_limited with Retry-After). The limits that apply to each operation are listed on that operation.

No “Try it out” console

This reference has no “Try it out” console and sends no request from your browser. That would mean typing a live API key into a web page and calling the production API with production data. Test with the code samples and your own key from your own server or terminal instead.

Account

Your merchant profile, plan and wallet balances.

Method GET /me #

Get your account

Returns your merchant id, business name, plan and wallet balances. It is a good first call to check the key.

Responses

200 Your account.

Your account.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data object required

Your account.

merchant_id integer required

Your merchant id.

business_name string | null required

Business name of the merchant.

plan object | null required

Latest subscription, or null when there is no subscription record.

name string | null required

Plan name.

expires_on string (date) required

Subscription expiry date (YYYY-MM-DD).

active boolean required

Whether the plan is currently active.

balances object required

Wallet balances in US dollars.

recharge number required

Shipping balance (recharge wallet), in US dollars.

cod_due number required

Cash-on-delivery dues (return wallet), in US dollars.

held number required

Reserved amounts, in US dollars.

company_due number required

Company dues, in US dollars.

currency string required

The platform's configured currency symbol ($ by default). It is not the unit of the balances, which are always US dollars; GET /statement returns the code USD.

Example

JSON · 200
{
  "success": true,
  "data": {
    "merchant_id": 12,
    "business_name": "Example Shop",
    "plan": {
      "name": "Example Plan",
      "expires_on": "2026-12-01",
      "active": true
    },
    "balances": {
      "recharge": 150.5,
      "cod_due": 320,
      "held": 0,
      "company_due": 0
    },
    "currency": "$"
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/me" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/me");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/me", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/me",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Lookups

Reference lists you need before creating a parcel: delivery companies and carriers, governorates, pickup locations and warehouse stock.

Method GET /companies #

List delivery companies and carriers

Lists the delivery companies and shipping carriers you can use. Send an id as company_id when you quote or create a parcel. The list is not paginated.

  • Request GET /api/v1/companies
  • Rate limit per minute, per IP address 60 overall, 120 for the whole API
  • Requires plan feature: API (checked when plans apply to your account)
  • Guides Merchant API v1 reference

Responses

200 Companies you can use.

Companies you can use.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data array<object> required
id integer required

Send it as company_id.

name string required

Company name.

type string required

delivery_company or carrier (an external shipping carrier).

One of: delivery_company carrier

pricing_mode string required

How the price is calculated: governorate (fixed amount per governorate), per_kg, per_km, km_kg, or carrier_quote (the carrier's own price). Shown as per_km for a delivery company that has no stored mode.

One of: governorate per_kg per_km km_kg carrier_quote

supports_own_account boolean required

True when the carrier allows merchant accounts and you have linked yours.

coverage object | null required

null when the company has no restriction; otherwise country code to the list of governorate names it covers ([] means all governorates of the country).

Example

JSON · 200
{
  "success": true,
  "data": [
    {
      "id": 3,
      "name": "Example Delivery",
      "type": "delivery_company",
      "pricing_mode": "governorate",
      "supports_own_account": false,
      "coverage": null
    },
    {
      "id": 5,
      "name": "Example Carrier",
      "type": "carrier",
      "pricing_mode": "carrier_quote",
      "supports_own_account": true,
      "coverage": {
        "SY": []
      }
    }
  ]
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/companies" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/companies");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/companies", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/companies",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method GET /governorates #

List governorates

Returns id, name and name_ar for every active governorate. Send name or name_ar in recipient.governorate. The list is not paginated.

  • Request GET /api/v1/governorates
  • Rate limit per minute, per IP address 60 overall, 120 for the whole API
  • Requires plan feature: API (checked when plans apply to your account)
  • Guides Merchant API v1 reference

Responses

200 Active governorates.

Active governorates.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data array<object> required
id integer required

Governorate id. When you send it in recipient.governorate, send it as a string.

name string required

English name.

name_ar string required

Arabic name.

Example

JSON · 200
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Damascus",
      "name_ar": "دمشق"
    }
  ]
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/governorates" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/governorates");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/governorates", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/governorates",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method GET /pickup-locations #

List your pickup locations

Returns your pickup locations, default first. When you omit pickup_location_id on a parcel, the default location is used. Add locations in the merchant panel. The list is not paginated.

  • Request GET /api/v1/pickup-locations
  • Rate limit per minute, per IP address 60 overall, 120 for the whole API
  • Requires plan feature: API (checked when plans apply to your account)
  • Guides Merchant API v1 reference

Responses

200 Your pickup locations.

Your pickup locations.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data array<object> required
id integer required

Send it as pickup_location_id.

name string | null required

Location name.

phone string | null required

Contact phone.

address string | null required

Address.

lat number | null required

Latitude.

lng number | null required

Longitude.

default boolean required

True for the default location (used when pickup_location_id is omitted).

warehouse_id integer | null required

Warehouse id when this is a warehouse pickup location, otherwise null.

active boolean required

Whether the location can be used.

Example

JSON · 200
{
  "success": true,
  "data": [
    {
      "id": 7,
      "name": "Main branch",
      "phone": "0900000000",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null,
      "default": true,
      "warehouse_id": null,
      "active": true
    }
  ]
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/pickup-locations" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/pickup-locations");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/pickup-locations", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/pickup-locations",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method GET /warehouse/stock #

Get your warehouse stock

For each active warehouse allocation: the warehouse, its pickup location id (use it as pickup_location_id to ship from this stock), pallets and the items with quantities. Returns an empty list when you have no active allocation.

  • Request GET /api/v1/warehouse/stock
  • Rate limit per minute, per IP address 60 overall, 120 for the whole API
  • Requires plan feature: API (checked when plans apply to your account)
  • Guides Merchant API v1 reference

Responses

200 Stock per warehouse allocation.

Stock per warehouse allocation.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data array<object> required
warehouse_id integer required

Warehouse id.

warehouse string | null required

Warehouse name.

pickup_location_id integer required

Pickup location of this allocation: use it as pickup_location_id to ship from this stock.

pallets integer required

Pallets allocated.

items array<object> required

Stock items.

item_id integer required

Stock item id.

sku string | null required

Product code.

name string | null required

Product name.

qty integer required

Quantity in stock.

reserved integer required

Quantity reserved for other parcels.

available integer required

Quantity you can still ship.

Example

JSON · 200
{
  "success": true,
  "data": [
    {
      "warehouse_id": 1,
      "warehouse": "Example Warehouse",
      "pickup_location_id": 9,
      "pallets": 2,
      "items": [
        {
          "item_id": 5,
          "sku": "A1",
          "name": "Example item",
          "qty": 20,
          "reserved": 2,
          "available": 18
        }
      ]
    }
  ]
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/warehouse/stock" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/warehouse/stock");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/warehouse/stock", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/warehouse/stock",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Pricing

Price a parcel without creating it.

Method POST /quote #

Price a parcel without creating it

Calculates the price of a parcel on the server without creating it or charging anything. It takes the same fields as POST /parcels and only company_id is required, but the fields the price needs depend on the company's pricing_mode (see GET /companies): governorate needs recipient.governorate (or an address the platform can map, or coordinates); per_kg needs weight; per_km needs recipient.lat and recipient.lng and a pickup location with a map position; km_kg needs both; carrier_quote asks the carrier for its price. For internal pricing the weight is rounded up to a whole kilogram and a company minimum fee is applied. The answer differs for a delivery company and for a carrier (see Quote).

  • Request POST /api/v1/quote
  • Rate limit per minute, per IP address 60 overall, 120 for the whole API
  • Requires plan feature: API (checked when plans apply to your account)
  • Guides Merchant API v1 reference

Request body

application/json · required

FieldType RequiredDescription
company_id integer required

Delivery company or carrier, an id from GET /companies. It must be available to your account.

recipient object optional

The recipient. Each field may instead be sent flat at the top level (customer_name, customer_phone, customer_address, customer_city, lat, long); the nested form is the documented one.

name string optional

Recipient name.

max length 191

phone string optional

Recipient phone number: digits, spaces, +, - and parentheses, 6 to 30 characters.

max length 30pattern ^[+0-9\s\-()]{6,30}$

address string optional

Recipient address.

max length 1000

governorate string optional

Governorate name (English name or Arabic name_ar from GET /governorates). If you send the id, send it as a string such as "3": a JSON number is rejected. Needed for governorate pricing; otherwise the platform tries to find it from the address, then from the coordinates.

max length 120also accepted as customer_city, city

lat number optional

Recipient latitude. Required for distance pricing (per_km, km_kg).

minimum -90maximum 90

lng number optional

Recipient longitude. Required for distance pricing (per_km, km_kg).

minimum -180maximum 180also accepted as long

reference string optional

Your own reference (order number). It does not affect the price.

max length 191also accepted as invoice_no

cod_amount number optional

Cash-on-delivery amount the recipient pays, from 0 to 100,000,000 US dollars (a non-USD amount is converted to USD at the platform rate before the limit is checked). 0 means prepaid.

minimum 0maximum 100000000default 0

currency string optional

Currency code of cod_amount (case-insensitive). The amount is stored in US dollars, so cod_amount in parcel objects is always USD. A company may accept only some currencies. A missing exchange rate gives 422.

One of: USD SYP SAR AED QAR KWD JOD IQD EGP LBP TRY EUR

default USDalso accepted as cod_currency

weight number optional

Weight in kilograms, from 0.01 to 1000. Default 1. For internal pricing the weight is rounded up to a whole kilogram (minimum 1).

minimum 0.01maximum 1000default 1also accepted as weight_kg

pickup_location_id integer optional

One of your pickup locations (GET /pickup-locations). Your default location is used when omitted. Errors are reported on the field shop_id.

also accepted as shop_id

note string optional

Note for the delivery company, up to 2000 characters.

max length 2000

use_own_account boolean optional

Ship with your own account at a carrier that allows merchant accounts. It fails with carrier_error when the carrier allows them and you have not linked yours; it is ignored when the carrier does not allow them, and has no effect on delivery companies.

shipping_paid_by string optional

Who pays the shipping fee, for carriers that support the choice: sender, customer (the recipient) or included_in_cod.

One of: sender customer included_in_cod

Example

JSON
{
  "company_id": 3,
  "recipient": {
    "governorate": "Damascus"
  },
  "weight": 2
}

Responses

200 The quote.

The quote.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data oneOf required

The shape depends on the company: a delivery company gives QuoteDeliveryCompany, a carrier gives QuoteCarrier (tell them apart with pricing_mode).

Alternative QuoteDeliveryCompany Quote for a delivery company.
pricing_mode string required

The pricing mode used.

One of: governorate per_kg per_km km_kg

pricing_label string | null required

Label of the pricing mode in the request language.

governorate string | null required

Governorate used for governorate pricing, otherwise null.

distance_km number required

Distance used (0 unless the mode uses distance).

weight_kg number required

Weight used, rounded up to a whole kilogram (minimum 1).

delivery_fee number required

Delivery fee in US dollars.

platform_fee number required

Platform fee in US dollars.

charged_now number required

Amount charged to your wallet at creation: delivery fee plus platform fee, or only the platform fee for your own internal delivery company.

Alternative QuoteCarrier Quote for an external carrier: a different, smaller object. It has no pricing_label, governorate, distance_km, weight_kg or delivery_fee.
pricing_mode string required

constant "carrier_quote"

carrier string required

Carrier name.

account string required

Which account would be used: the platform account or your own account at the carrier.

One of: platform_account merchant_account

carrier_fee number required

The carrier's price in the carrier's currency.

carrier_currency string required

Currency of carrier_fee.

carrier_fee_usd number required

The carrier's price in US dollars.

platform_fee number required

Platform fee in US dollars.

charged_now number required

Amount charged to your wallet at creation, in US dollars.

held_now number required

Amount held until delivery (your own account at the carrier), in US dollars.

required_balance number required

Balance that must be available in your wallet, in US dollars.

shipping_paid_by string required

Who pays the shipping fee (sender, customer or included_in_cod).

receiver_pays number required

Amount the recipient pays on delivery, in US dollars.

net_after_delivery number required

Your net amount after delivery, in US dollars.

destination string | null required

Destination note from the carrier.

currency string required

constant "USD"

Example

JSON · 200
{
  "success": true,
  "data": {
    "pricing_mode": "governorate",
    "pricing_label": "Fixed amount per governorate",
    "governorate": "Damascus",
    "distance_km": 0,
    "weight_kg": 2,
    "delivery_fee": 6,
    "platform_fee": 1,
    "charged_now": 7
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

422 validation_failed:

validation_failed: company_id missing or not available to you, or a field the pricing needs is missing. quote_failed: the price could not be calculated. carrier_error: the carrier refused or could not price it.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Missing company

JSON · 422
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "company_id is required — get the list from GET /companies",
    "fields": {
      "company_id": [
        "company_id is required — get the list from GET /companies"
      ]
    }
  }
}

Example — Price not calculated

JSON · 422
{
  "success": false,
  "error": {
    "code": "quote_failed",
    "message": "Could not calculate the price."
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X POST "https://YOUR_DOMAIN/api/v1/quote" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "company_id": 3,
  "recipient": {
    "governorate": "Damascus"
  },
  "weight": 2
}'
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/quote");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'company_id' => 3,
        'recipient' => [
            'governorate' => 'Damascus',
        ],
        'weight' => 2,
    ]),
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/quote", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "company_id": 3,
    "recipient": {
      "governorate": "Damascus"
    },
    "weight": 2
  }),
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.post(
    "https://YOUR_DOMAIN/api/v1/quote",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    json={
        "company_id": 3,
        "recipient": {
            "governorate": "Damascus",
        },
        "weight": 2,
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Parcels

Create, list, track and cancel parcels.

Method GET /parcels #

List your parcels

Lists your parcels, newest first (by id), without events. Parcels in the awaiting_funds stage are not listed (fetch one by tracking number or reference). Paginated: the answer has meta.

Query parameters

NameTypeRequiredDescription
reference string optional

Exact match on the reference you sent at creation.

Example ORDER-1043

from string (date) optional

First creation day, YYYY-MM-DD, inclusive (UTC). The format is not validated: use only this one.

Example 2026-01-01

to string (date) optional

Last creation day, YYYY-MM-DD, inclusive (UTC). The format is not validated: use only this one.

Example 2026-01-31

stage string optional

Filter by the stage of the parcel. Only these six values filter; any other value (including processing, awaiting_funds and typos) is silently ignored and everything comes back unfiltered.

One of: pending out_for_delivery delivered partially_delivered returning returned

Example out_for_delivery

per_page integer optional

Page size, from 1 to 100 (default 20). A value outside the range is not rejected: it is clamped to the nearest limit and the effective value comes back in meta.per_page.

minimum 1maximum 100default 20

page integer optional

Page number, starting at 1. An invalid value or a value below 1 is treated as 1. A page beyond the last one returns an empty list.

minimum 1default 1

Responses

200 A page of parcels.

A page of parcels.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data array<Parcel> required
meta Meta required

Pagination block. There are no links and no cursor.

Example

JSON · 200
{
  "success": true,
  "data": [
    {
      "id": 1043,
      "tracking_number": "DS-000000",
      "reference": "ORDER-1043",
      "stage": "out_for_delivery",
      "status_code": 7,
      "status_label": "Delivery Man Assign",
      "company_id": 3,
      "company_name": "Example Delivery",
      "external_tracking_number": null,
      "recipient": {
        "name": "Example Name",
        "phone": "0900000000",
        "governorate": "Damascus",
        "address": "Example street, building 1",
        "lat": null,
        "lng": null
      },
      "cod_amount": 25,
      "cod_collected": 0,
      "weight_kg": 1.5,
      "charges": {
        "delivery_fee": 6,
        "platform_fee": 1
      },
      "delivery_attempts": 0,
      "note": null,
      "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
      "created_at": "2026-01-01T10:00:00+00:00",
      "updated_at": "2026-01-01T12:10:00+00:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "last_page": 1
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/parcels?reference=ORDER-1043&from=2026-01-01&to=2026-01-31&stage=out_for_delivery" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/parcels?reference=ORDER-1043&from=2026-01-01&to=2026-01-31&stage=out_for_delivery");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/parcels?reference=ORDER-1043&from=2026-01-01&to=2026-01-31&stage=out_for_delivery", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/parcels",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    params={
        "reference": "ORDER-1043",
        "from": "2026-01-01",
        "to": "2026-01-31",
        "stage": "out_for_delivery",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method POST /parcels #

Create a parcel

Creates a parcel. The delivery fee is calculated on the server and charged to your wallet. Send the same reference again to retry safely: you get the existing parcel back with "duplicate": true (HTTP 200) and nothing is charged (parcels that were refunded or expired do not count). There is no Idempotency-Key header. A merchant with no active plan is rejected with plan_feature before the parcel is processed; a reached parcel quota gives 422 on the field plan. If an external carrier rejects the parcel, nothing is created or charged (carrier_error).

Request body

application/json · required

FieldType RequiredDescription
company_id integer required

Delivery company or carrier, an id from GET /companies. It must be available to your account.

recipient object required

The recipient. Each field may instead be sent flat at the top level (customer_name, customer_phone, customer_address, customer_city, lat, long); the nested form is the documented one.

name string required

Recipient name.

max length 191

phone string required

Recipient phone number: digits, spaces, +, - and parentheses, 6 to 30 characters.

max length 30pattern ^[+0-9\s\-()]{6,30}$

address string required

Recipient address.

max length 1000

governorate string optional

Governorate name (English name or Arabic name_ar from GET /governorates). If you send the id, send it as a string such as "3": a JSON number is rejected. Needed for governorate pricing; otherwise the platform tries to find it from the address, then from the coordinates.

max length 120also accepted as customer_city, city

lat number optional

Recipient latitude. Required for distance pricing (per_km, km_kg).

minimum -90maximum 90

lng number optional

Recipient longitude. Required for distance pricing (per_km, km_kg).

minimum -180maximum 180also accepted as long

reference string optional

Your own unique reference (order number), up to 191 characters. It makes retries safe: sending the same reference again returns the existing parcel with "duplicate": true instead of creating a new one.

max length 191also accepted as invoice_no

cod_amount number optional

Cash-on-delivery amount the recipient pays, from 0 to 100,000,000 US dollars (a non-USD amount is converted to USD at the platform rate before the limit is checked). 0 means prepaid.

minimum 0maximum 100000000default 0

currency string optional

Currency code of cod_amount (case-insensitive). The amount is stored in US dollars, so cod_amount in parcel objects is always USD. A company may accept only some currencies. A missing exchange rate gives 422.

One of: USD SYP SAR AED QAR KWD JOD IQD EGP LBP TRY EUR

default USDalso accepted as cod_currency

weight number optional

Weight in kilograms, from 0.01 to 1000. Default 1. For internal pricing the weight is rounded up to a whole kilogram (minimum 1).

minimum 0.01maximum 1000default 1also accepted as weight_kg

pickup_location_id integer optional

One of your pickup locations (GET /pickup-locations). Your default location is used when omitted. Errors are reported on the field shop_id.

also accepted as shop_id

warehouse_items array<object> optional

Products shipped from your stock. Required when the pickup location is a warehouse; ignored otherwise. Also accepted as items.

also accepted as items

sku string optional

Product code (SKU) of the stock item.

item_id integer optional

Stock item id (alternative to sku).

qty integer required

Quantity to ship with the parcel; must not exceed the available quantity.

minimum 1

note string optional

Note for the delivery company, up to 2000 characters.

max length 2000

use_own_account boolean optional

Ship with your own account at a carrier that allows merchant accounts. It fails with carrier_error when the carrier allows them and you have not linked yours; it is ignored when the carrier does not allow them, and has no effect on delivery companies.

shipping_paid_by string optional

Who pays the shipping fee, for carriers that support the choice: sender, customer (the recipient) or included_in_cod.

One of: sender customer included_in_cod

wait_for_balance boolean optional

When the wallet balance is short, keep the parcel as awaiting_funds (answer 201, hidden from GET /parcels) instead of failing with 402. An hourly job completes it when the balance is enough and cancels it after 24 hours.

Example

JSON
{
  "company_id": 3,
  "reference": "ORDER-1043",
  "recipient": {
    "name": "Example Name",
    "phone": "0900000000",
    "governorate": "Damascus",
    "address": "Example street, building 1"
  },
  "cod_amount": 25,
  "weight": 1.5,
  "note": "Call before arriving"
}

Responses

200 A parcel with the same reference already exists:

A parcel with the same reference already exists: it is returned unchanged with duplicate: true.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

duplicate boolean required

constant true

Example

JSON · 200
{
  "success": true,
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  },
  "duplicate": true
}
201 The parcel was created.

The parcel was created.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON · 201
{
  "success": true,
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "processing",
    "status_code": 5,
    "status_label": "Received Warehouse",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

401 Missing or unknown API key. common response, see below

402 insufficient_balance:

insufficient_balance: the wallet balance is not enough (the message gives the required and available amounts). Send wait_for_balance: true to keep the parcel as awaiting_funds instead.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Balance too low

JSON · 402
{
  "success": false,
  "error": {
    "code": "insufficient_balance",
    "message": "Insufficient wallet balance. Required: 7.00, available: 2.00"
  }
}

403 Access refused. common response, see below

422 validation_failed:

validation_failed: a field is missing or wrong (fields lists them, using the platform's internal field names), or company_id is not available to you, or the quota (plan) is used up. carrier_error: the external carrier rejected the parcel or the carrier account is not usable.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Missing recipient phone

JSON · 422
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "The customer phone field is required.",
    "fields": {
      "customer_phone": [
        "The customer phone field is required."
      ]
    }
  }
}

Example — Carrier refused

JSON · 422
{
  "success": false,
  "error": {
    "code": "carrier_error",
    "message": "Localized message from the carrier."
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X POST "https://YOUR_DOMAIN/api/v1/parcels" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "company_id": 3,
  "reference": "ORDER-1043",
  "recipient": {
    "name": "Example Name",
    "phone": "0900000000",
    "governorate": "Damascus",
    "address": "Example street, building 1"
  },
  "cod_amount": 25,
  "weight": 1.5,
  "note": "Call before arriving"
}'
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/parcels");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'company_id' => 3,
        'reference' => 'ORDER-1043',
        'recipient' => [
            'name' => 'Example Name',
            'phone' => '0900000000',
            'governorate' => 'Damascus',
            'address' => 'Example street, building 1',
        ],
        'cod_amount' => 25,
        'weight' => 1.5,
        'note' => 'Call before arriving',
    ]),
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/parcels", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "company_id": 3,
    "reference": "ORDER-1043",
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1"
    },
    "cod_amount": 25,
    "weight": 1.5,
    "note": "Call before arriving"
  }),
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.post(
    "https://YOUR_DOMAIN/api/v1/parcels",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    json={
        "company_id": 3,
        "reference": "ORDER-1043",
        "recipient": {
            "name": "Example Name",
            "phone": "0900000000",
            "governorate": "Damascus",
            "address": "Example street, building 1",
        },
        "cod_amount": 25,
        "weight": 1.5,
        "note": "Call before arriving",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method GET /parcels/{tracking} #

Get one parcel with its events

Takes a tracking number or a reference (the newest match wins) and returns the parcel with events in time order. Also finds parcels in the awaiting_funds stage.

  • Request GET /api/v1/parcels/{tracking}
  • Rate limit per minute, per IP address 60 overall, 120 for the whole API
  • Requires plan feature: API (checked when plans apply to your account)
  • Guides Merchant API v1 reference

Path parameters

NameTypeRequiredDescription
tracking string required

A tracking number, or the reference you sent when creating the parcel (the newest match wins; a reference may resolve to a different parcel than you expect, so cancel by tracking number).

Example DS-000000

Responses

200 The parcel with its events.

The parcel with its events.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data object required

The parcel object plus its history (events). Returned when one parcel is fetched.

Includes every field of Parcel
events array<object> required

History of the parcel in time order. Present only when one parcel is fetched.

status_code integer required

Exact internal status code of that step.

stage string required

Simplified stage of that step.

One of: pending processing out_for_delivery delivered partially_delivered returning returned

status_label string required

Status label in the request language.

note string | null required

Free-text note recorded with the event.

at string (date-time) | null required

Time of the event (ISO 8601 with a UTC offset).

Example

JSON · 200
{
  "success": true,
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00",
    "events": [
      {
        "status_code": 1,
        "stage": "pending",
        "status_label": "Pending",
        "note": "Parcel created (api)",
        "at": "2026-01-01T10:00:00+00:00"
      },
      {
        "status_code": 5,
        "stage": "processing",
        "status_label": "Received Warehouse",
        "note": null,
        "at": "2026-01-01T10:00:01+00:00"
      }
    ]
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

404 not_found:

not_found: no parcel with this tracking number or reference in your account (a cancelled parcel no longer exists).

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Unknown parcel

JSON · 404
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "No parcel with this number for your account."
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/parcels/DS-000000" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/parcels/DS-000000");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/parcels/DS-000000", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/parcels/DS-000000",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method POST /parcels/{tracking}/cancel #

Cancel a parcel and refund the fee

Cancels a parcel and refunds what was charged at creation. It works only while the status is Pending or Received Warehouse and the parcel has no external_tracking_number. The parcel is deleted (a second call gives 404). An API cancellation does not send a parcel.cancelled webhook.

Path parameters

NameTypeRequiredDescription
tracking string required

A tracking number, or the reference you sent when creating the parcel (the newest match wins; a reference may resolve to a different parcel than you expect, so cancel by tracking number).

Example DS-000000

Responses

200 The parcel was cancelled and refunded.

The parcel was cancelled and refunded.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data object required

Result of a cancellation.

tracking_number string required

Tracking number of the cancelled parcel.

cancelled boolean required

constant true

refunded boolean required

constant true

Example

JSON · 200
{
  "success": true,
  "data": {
    "tracking_number": "DS-000000",
    "cancelled": true,
    "refunded": true
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

404 not_found:

not_found: no parcel with this number in your account.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Unknown parcel

JSON · 404
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "No parcel with this number for your account."
  }
}
409 not_cancellable:

not_cancellable: the parcel left the Pending or Received Warehouse status, or it went to an external carrier.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Already on its way

JSON · 409
{
  "success": false,
  "error": {
    "code": "not_cancellable",
    "message": "The parcel is out for delivery or sent to the shipping company — it cannot be cancelled from here."
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X POST "https://YOUR_DOMAIN/api/v1/parcels/DS-000000/cancel" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/parcels/DS-000000/cancel");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/parcels/DS-000000/cancel", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.post(
    "https://YOUR_DOMAIN/api/v1/parcels/DS-000000/cancel",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Statement

Wallet statement for a period.

Method GET /statement #

Get your wallet statement

Your wallet statement for a period: opening and closing balance per wallet, a summary per movement type and every movement (oldest first). Amounts are in US dollars. Paginated: meta counts the lines; wallets and by_category cover the whole period.

  • Request GET /api/v1/statement
  • Rate limit per minute, per IP address 60 overall, 120 for the whole API, 30 for this route
  • Requires plan feature: API (checked when plans apply to your account)
  • Guides Merchant API v1 reference

Query parameters

NameTypeRequiredDescription
from string (date) optional

First day, YYYY-MM-DD. Default: the first day of the current month. Accepted only as a literal YYYY-MM-DD; otherwise ignored. If from is after to they are swapped; a range longer than 366 days is shortened by moving from.

Example 2026-01-01

to string (date) optional

Last day, YYYY-MM-DD. Default: today. Same rules as from.

Example 2026-01-31

wallet string optional

Only this wallet: recharge (shipping balance), held (reserved amounts), return (cash-on-delivery dues) or payout_pending. Any other value is ignored.

One of: recharge held return payout_pending

category string optional

Only this movement type, for example parcel_created. Lowercase letters and underscores (2 to 60 characters); it is not checked against known types, so an unknown value returns no lines.

pattern ^[a-z_]{2,60}$

Example parcel_created

per_page integer optional

Page size, from 1 to 500 (default 100). A value outside the range is not rejected: it is clamped to the nearest limit and the effective value comes back in meta.per_page.

minimum 1maximum 500default 100

page integer optional

Page number, starting at 1. An invalid value or a value below 1 is treated as 1. A page beyond the last one returns an empty list.

minimum 1default 1

Responses

200 The statement.

The statement.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data object required

Wallet statement for a period. All amounts are in US dollars.

period object required

The period actually applied.

from string (date) required

First day (YYYY-MM-DD).

to string (date) required

Last day (YYYY-MM-DD).

currency string required

constant "USD"

wallets array<object> required

Opening and closing balance per wallet. Covers the whole period regardless of the page.

wallet string required

Wallet code.

One of: recharge held return payout_pending

name string required

Wallet label in the request language.

opening number required

Balance before the period.

credits number required

Total credits in the period.

debits number required

Total debits in the period.

closing number required

Balance at the end of the period.

count integer required

Number of movements.

by_category array<object> required

Summary per movement type. Covers the whole period regardless of the page.

category string required

Movement type code, for example parcel_created.

name string required

Label of the type in the request language.

credits number required

Credits of this type.

debits number required

Debits of this type.

count integer required

Number of movements.

lines array<object> required

The movements of the requested page, oldest first (date, then id). Paging applies to this list only.

id integer required

Movement id.

date string (date-time) required

Time of the movement (ISO 8601).

wallet string required

Wallet code.

wallet_name string required

Wallet label.

type string required

Movement type code.

type_name string required

Label of the type.

credit number required

Credit amount (0 for a debit).

debit number required

Debit amount (0 for a credit).

balance number required

Wallet balance after the movement.

description string | null required

Description of the movement.

parcel string | null required

Tracking number when the movement relates to a parcel.

meta Meta required

Pagination block. There are no links and no cursor.

Example

JSON · 200
{
  "success": true,
  "data": {
    "period": {
      "from": "2026-01-01",
      "to": "2026-01-31"
    },
    "currency": "USD",
    "wallets": [
      {
        "wallet": "recharge",
        "name": "Shipping balance",
        "opening": 50,
        "credits": 200,
        "debits": 132.5,
        "closing": 117.5,
        "count": 41
      }
    ],
    "by_category": [
      {
        "category": "parcel_created",
        "name": "Parcel creation fee",
        "credits": 0,
        "debits": 132.5,
        "count": 38
      }
    ],
    "lines": [
      {
        "id": 9811,
        "date": "2026-01-02T10:14:00+00:00",
        "wallet": "recharge",
        "wallet_name": "Shipping balance",
        "type": "parcel_created",
        "type_name": "Parcel creation fee",
        "credit": 0,
        "debit": 3.5,
        "balance": 46.5,
        "description": "Example description",
        "parcel": "DS-000000"
      }
    ]
  },
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "last_page": 1
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/statement?from=2026-01-01&to=2026-01-31&category=parcel_created" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/statement?from=2026-01-01&to=2026-01-31&category=parcel_created");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/statement?from=2026-01-01&to=2026-01-31&category=parcel_created", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/statement",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    params={
        "from": "2026-01-01",
        "to": "2026-01-31",
        "category": "parcel_created",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Webhook endpoints

Manage the URLs that receive your notifications. The messages themselves are described under Webhook events.

Method GET /webhooks/events #

List the events you can subscribe to

Lists the event names with their short name and description in the request language. Send * or exact names in events when you create an endpoint. ping is not listed because it cannot be selected.

Responses

200 The events.

The events.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data array<object> required
event string required

Event name.

One of: parcel.created parcel.status_changed parcel.out_for_delivery parcel.delivered parcel.returning parcel.returned parcel.cancelled parcel.awaiting_funds wallet.topup wallet.withdrawal

name string required

Short name in the request language.

description string required

When it is sent, in the request language.

Example

JSON · 200
{
  "success": true,
  "data": [
    {
      "event": "parcel.delivered",
      "name": "Delivered",
      "description": "The parcel was delivered (fully or partially) — includes the collected amount."
    }
  ]
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/webhooks/events" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/webhooks/events");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/webhooks/events", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/webhooks/events",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method GET /webhooks #

List your webhook endpoints

Lists your webhook endpoints. The signing key is never included. The list is not paginated.

Responses

200 Your endpoints.

Your endpoints.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data array<Webhook> required

Example

JSON · 200
{
  "success": true,
  "data": [
    {
      "id": 7,
      "name": "ERP",
      "url": "https://example.com/hooks/delivery",
      "events": [
        "parcel.delivered",
        "parcel.returned"
      ],
      "active": true,
      "disabled_reason": null,
      "last_status": null,
      "last_error": null,
      "last_sent_at": null
    }
  ]
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/webhooks");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/webhooks", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.get(
    "https://YOUR_DOMAIN/api/v1/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method POST /webhooks #

Add a webhook endpoint

Creates an endpoint and returns its signing key (secret) once. Needs a plan with the webhooks feature (403 plan_feature) and allows at most 10 endpoints, or your plan's limit if lower (422 limit_reached). The URL must be https, public, without user name or password, on port 443 or 8443. How messages are signed and retried: see the webhook articles.

Request body

application/json · required

FieldType RequiredDescription
name string | null optional

Optional label, up to 100 characters.

max length 100

url string (uri) required

HTTPS URL, up to 500 characters. The host must resolve to public addresses only, with no user name or password, on port 443 or 8443. Redirects are not followed.

max length 500pattern ^[Hh][Tt][Tt][Pp][Ss]://

events array<string> required

Events to receive: at least one entry, each * or an exact event name.

One of: * parcel.created parcel.status_changed parcel.out_for_delivery parcel.delivered parcel.returning parcel.returned parcel.cancelled parcel.awaiting_funds wallet.topup wallet.withdrawal

min items 1

active boolean optional

Whether the endpoint is enabled (default true).

Example

JSON
{
  "name": "ERP",
  "url": "https://example.com/hooks/delivery",
  "events": [
    "parcel.delivered",
    "parcel.returned"
  ]
}

Responses

201 The endpoint was created.

The endpoint was created.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data object required

A webhook endpoint, including its signing key (shown once).

id integer required

Endpoint id.

name string | null required

Label you gave the endpoint.

url string required

HTTPS URL that receives the messages.

events array<string> required

Subscribed events; ["*"] means all events.

active boolean required

False when disabled, including automatic suspension after 40 consecutive failed attempts.

disabled_reason string | null required

Why the endpoint was suspended automatically.

last_status integer | null required

HTTP status of the last delivery attempt.

last_error string | null required

Error of the last failed attempt.

last_sent_at string (date-time) | null required

Time of the last attempt (ISO 8601).

secret string required

Signing key (whsec_ plus 40 characters). It is returned only in this response: store it now.

Example

JSON · 201
{
  "success": true,
  "data": {
    "id": 7,
    "name": "ERP",
    "url": "https://example.com/hooks/delivery",
    "events": [
      "parcel.delivered",
      "parcel.returned"
    ],
    "active": true,
    "disabled_reason": null,
    "last_status": null,
    "last_error": null,
    "last_sent_at": null,
    "secret": "whsec_EXAMPLE_ONLY_NOT_A_REAL_SECRET"
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

422 validation_failed:

validation_failed: a field is missing or wrong (the URL must be https, public, port 443 or 8443; each event must be * or an exact name). limit_reached: you already have the maximum number of endpoints.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Not https

JSON · 422
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "The URL must start with https://",
    "fields": {
      "url": [
        "The URL must start with https://"
      ]
    }
  }
}

Example — Too many endpoints

JSON · 422
{
  "success": false,
  "error": {
    "code": "limit_reached",
    "message": "You have reached your plan limit: 10 webhook links."
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X POST "https://YOUR_DOMAIN/api/v1/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "ERP",
  "url": "https://example.com/hooks/delivery",
  "events": [
    "parcel.delivered",
    "parcel.returned"
  ]
}'
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/webhooks");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'name' => 'ERP',
        'url' => 'https://example.com/hooks/delivery',
        'events' => [
            'parcel.delivered',
            'parcel.returned',
        ],
    ]),
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/webhooks", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "ERP",
    "url": "https://example.com/hooks/delivery",
    "events": [
      "parcel.delivered",
      "parcel.returned"
    ]
  }),
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.post(
    "https://YOUR_DOMAIN/api/v1/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    json={
        "name": "ERP",
        "url": "https://example.com/hooks/delivery",
        "events": [
            "parcel.delivered",
            "parcel.returned",
        ],
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method DELETE /webhooks/{id} #

Delete a webhook endpoint

Deletes the endpoint. Messages still waiting for a retry are marked failed when their time comes.

Path parameters

NameTypeRequiredDescription
id integer required

Id of the webhook endpoint (from GET /webhooks).

Example 7

Responses

200 The endpoint was deleted.

The endpoint was deleted.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data object required

Result of a deletion.

deleted boolean required

constant true

Example

JSON · 200
{
  "success": true,
  "data": {
    "deleted": true
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

404 not_found:

not_found: no such webhook endpoint in your account.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Unknown webhook endpoint

JSON · 404
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "Link not found."
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X DELETE "https://YOUR_DOMAIN/api/v1/webhooks/7" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/webhooks/7");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "DELETE",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/webhooks/7", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.delete(
    "https://YOUR_DOMAIN/api/v1/webhooks/7",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method PATCH /webhooks/{id} #

Edit a webhook endpoint

Edits name, url, events or active; send only what you want to change. Turning active on again resets the failure counter and clears the suspension reason.

Path parameters

NameTypeRequiredDescription
id integer required

Id of the webhook endpoint (from GET /webhooks).

Example 7

Request body

application/json · required

FieldType RequiredDescription
name string | null optional

Label, up to 100 characters.

max length 100

url string (uri) optional

New HTTPS URL (same rules as on creation).

max length 500pattern ^[Hh][Tt][Tt][Pp][Ss]://

events array<string> optional

New list of events (at least one).

One of: * parcel.created parcel.status_changed parcel.out_for_delivery parcel.delivered parcel.returning parcel.returned parcel.cancelled parcel.awaiting_funds wallet.topup wallet.withdrawal

min items 1

active boolean optional

Turning it on again resets the failure counter and clears the suspension reason.

Example

JSON
{
  "active": true
}

Responses

200 The updated endpoint.

The updated endpoint.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data Webhook required

A webhook endpoint (the signing key is never included).

Example

JSON · 200
{
  "success": true,
  "data": {
    "id": 7,
    "name": "ERP",
    "url": "https://example.com/hooks/delivery",
    "events": [
      "parcel.delivered",
      "parcel.returned"
    ],
    "active": true,
    "disabled_reason": null,
    "last_status": null,
    "last_error": null,
    "last_sent_at": null
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

404 not_found:

not_found: no such webhook endpoint in your account.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Unknown webhook endpoint

JSON · 404
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "Link not found."
  }
}
422 validation_failed:

validation_failed: a field is wrong (same rules as on creation).

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Not https

JSON · 422
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "The URL must start with https://",
    "fields": {
      "url": [
        "The URL must start with https://"
      ]
    }
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X PATCH "https://YOUR_DOMAIN/api/v1/webhooks/7" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "active": true
}'
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/webhooks/7");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "PATCH",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'active' => true,
    ]),
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/webhooks/7", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "active": true
  }),
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.patch(
    "https://YOUR_DOMAIN/api/v1/webhooks/7",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    json={
        "active": True,
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Method POST /webhooks/{id}/test #

Send a test message

Sends a signed ping message to the endpoint and reports the HTTP status your system answered with. A successful test resets the failure counter. It is limited to 10 requests per minute on the route (about 5 in practice, see the rate-limit notes).

Path parameters

NameTypeRequiredDescription
id integer required

Id of the webhook endpoint (from GET /webhooks).

Example 7

Responses

200 Your system answered with a 2xx status.

Your system answered with a 2xx status.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

FieldType RequiredDescription
success boolean required

constant true

data object required

Result of a successful test message.

delivered boolean required

constant true

response_status integer required

HTTP status your endpoint answered with.

Example

JSON · 200
{
  "success": true,
  "data": {
    "delivered": true,
    "response_status": 200
  }
}

401 Missing or unknown API key. common response, see below

403 Access refused. common response, see below

404 not_found:

not_found: no such webhook endpoint in your account.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Unknown webhook endpoint

JSON · 404
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "Link not found."
  }
}
422 delivery_failed:

delivery_failed: the message did not arrive (non-2xx answer, redirect, timeout or connection error). response_status has the status your system returned, or null.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Your system refused

JSON · 422
{
  "success": false,
  "error": {
    "code": "delivery_failed",
    "message": "Not delivered: your system answered 500",
    "response_status": 500
  }
}

429 Rate limit. common response, see below

500 Unexpected server error. common response, see below

Code samples

curl
curl
curl -X POST "https://YOUR_DOMAIN/api/v1/webhooks/7/test" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
PHP
PHP
<?php
$ch = curl_init("https://YOUR_DOMAIN/api/v1/webhooks/7/test");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("DS_API_KEY"),
        "Accept: application/json",
    ],
]);
$body   = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (!($body["success"] ?? false)) {
    throw new RuntimeException("$status {$body["error"]["code"]}: {$body["error"]["message"]}");
}
print_r($body["data"]);
JavaScript
JavaScript
const response = await fetch("https://YOUR_DOMAIN/api/v1/webhooks/7/test", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.DS_API_KEY}`,
    "Accept": "application/json",
  },
});
const json = await response.json();
if (!json.success) {
  throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
}
console.log(json.data);
Python
Python
import os
import requests

response = requests.post(
    "https://YOUR_DOMAIN/api/v1/webhooks/7/test",
    headers={
        "Authorization": f"Bearer {os.environ['DS_API_KEY']}",
        "Accept": "application/json",
    },
    timeout=30,
)
body = response.json()
if not body["success"]:
    raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["data"])

Objects

Objects that several endpoints and webhook events return. Each is described once here; the other tables link to it.

Parcel #

A parcel as returned by the API and carried by webhooks (same fields every time).

FieldType RequiredDescription
id integer required

Parcel id.

tracking_number string required

Tracking number. Use it in {tracking} paths and on the public tracking page.

reference string | null required

Your own reference (order number) sent at creation.

stage string required

Simplified, stable state. Prefer it to status_code in your logic: awaiting_funds (saved, waiting for the wallet balance), pending (status 1), out_for_delivery (7, 8), delivered (9), partially_delivered (32), returning (11, 12, 24, 26, 27), returned (13, 30) and processing (every other status).

One of: pending processing out_for_delivery delivered partially_delivered returning returned awaiting_funds

status_code integer required

Exact internal status code (for example 1 Pending, 5 Received Warehouse, 7 Delivery Man Assign, 9 Delivered, 13 Returned Merchant, 32 Partial Delivered).

status_label string required

Label of the status in the request language.

company_id integer required

Delivery company or carrier that handles the parcel.

company_name string | null required

Name of that company.

external_tracking_number string | null required

Tracking number at the external shipping carrier, when the parcel was sent to one.

recipient object required

The recipient of the parcel.

name string | null required

Recipient name.

phone string | null required

Recipient phone.

governorate string | null required

Recipient governorate.

address string | null required

Recipient address.

lat number | null required

Latitude of the recipient.

lng number | null required

Longitude of the recipient.

cod_amount number required

Cash-on-delivery amount to collect, always in US dollars.

cod_collected number required

Amount collected so far, in US dollars (the collected amount for a partial delivery).

weight_kg number | null required

Parcel weight in kilograms.

charges object required

Fees of the parcel.

delivery_fee number required

Delivery fee in US dollars (the carrier cost for a carrier parcel).

platform_fee number required

Platform fee in US dollars.

delivery_attempts integer required

Number of delivery attempts.

note string | null required

Note sent with the parcel.

tracking_url string (uri) required

Public tracking page of the parcel.

created_at string (date-time) | null required

Creation time (ISO 8601 with a UTC offset).

updated_at string (date-time) | null required

Last update time (ISO 8601 with a UTC offset).

Meta #

Pagination block. There are no links and no cursor.

FieldType RequiredDescription
page integer required

Current page.

per_page integer required

Effective page size.

total integer required

Number of records after the filters.

last_page integer required

Number of pages (1 when there are no results).

Webhook #

A webhook endpoint (the signing key is never included).

FieldType RequiredDescription
id integer required

Endpoint id.

name string | null required

Label you gave the endpoint.

url string required

HTTPS URL that receives the messages.

events array<string> required

Subscribed events; ["*"] means all events.

active boolean required

False when disabled, including automatic suspension after 40 consecutive failed attempts.

disabled_reason string | null required

Why the endpoint was suspended automatically.

last_status integer | null required

HTTP status of the last delivery attempt.

last_error string | null required

Error of the last failed attempt.

last_sent_at string (date-time) | null required

Time of the last attempt (ISO 8601).

Webhook events

The platform sends these messages to the HTTPS endpoints you register (outgoing webhooks). Every message is a signed JSON POST and is retried when your system does not answer with a 2xx status. Each event below shows its body; all of them share the same headers and signature.

Request headers

NameTypeDescription
X-DS-Eventstring

The event name (for example parcel.delivered, or ping for a test).

X-DS-Event-Idstring

Event id, evt_ plus 24 lowercase characters. The same for every attempt and every endpoint of the event: use it to ignore duplicates.

X-DS-Deliverystring (uuid)

UUID of this message to this endpoint. The same across attempts.

X-DS-Attemptinteger

Attempt number, starting at 1.

X-DS-Timestampstring

Send time in Unix seconds, renewed on every attempt. It is part of the signed text.

X-DS-Signaturestring

sha256= followed by the lowercase hex of HMAC-SHA256 with the endpoint's signing key (whsec_..., the whole string) over the text X-DS-Timestamp + "." + raw request body. Compare in constant time.

Verify the webhook signature (X-DS-Signature)

Event POST parcel.created #

Parcel created

A parcel was created (from the panel, the API or a connected store). A new parcel normally sends this and then parcel.status_changed (status 5).

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.created"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.created",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST parcel.status_changed #

Parcel status changed

Any status change made through the parcel workflow, including the automatic move to status 5 right after creation. One change can send this and a stage event (each with its own event id). Not sent by the dedicated staff pickup and hub forms or by some undo forms: read the parcel when you need its current state.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.status_changed"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.status_changed",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST parcel.out_for_delivery #

Out for delivery

The parcel is with the courier (statuses 7 and 8).

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.out_for_delivery"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.out_for_delivery",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST parcel.delivered #

Delivered

The parcel was delivered, fully or partially (statuses 9 and 32). data.cod_collected has the collected amount.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.delivered"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.delivered",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST parcel.returning #

Returning

Delivery failed and the parcel is on its way back.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.returning"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.returning",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST parcel.returned #

Returned to the merchant

The merchant received the returned parcel (statuses 30 and 13).

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.returned"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.returned",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST parcel.cancelled #

Cancelled

The connected external carrier reported the parcel as cancelled and the amount went back to your balance. Not sent when you cancel with POST /parcels/{tracking}/cancel.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.cancelled"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.cancelled",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST parcel.awaiting_funds #

Awaiting funds

The order was saved but your balance is too low (with wait_for_balance, or store orders). It is completed when you top up.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "parcel.awaiting_funds"

created_at string (date-time) required

Time the event was created (ISO 8601).

data Parcel required

A parcel as returned by the API and carried by webhooks (same fields every time).

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "parcel.awaiting_funds",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": 1043,
    "tracking_number": "DS-000000",
    "reference": "ORDER-1043",
    "stage": "out_for_delivery",
    "status_code": 7,
    "status_label": "Delivery Man Assign",
    "company_id": 3,
    "company_name": "Example Delivery",
    "external_tracking_number": null,
    "recipient": {
      "name": "Example Name",
      "phone": "0900000000",
      "governorate": "Damascus",
      "address": "Example street, building 1",
      "lat": null,
      "lng": null
    },
    "cod_amount": 25,
    "cod_collected": 0,
    "weight_kg": 1.5,
    "charges": {
      "delivery_fee": 6,
      "platform_fee": 1
    },
    "delivery_attempts": 0,
    "note": null,
    "tracking_url": "https://YOUR_DOMAIN/t/DS-000000",
    "created_at": "2026-01-01T10:00:00+00:00",
    "updated_at": "2026-01-01T12:10:00+00:00"
  }
}

Event POST wallet.topup #

Balance topped up

Balance was added to your recharge (shipping) wallet by an approved top-up request or an online payment. Merchants only.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "wallet.topup"

created_at string (date-time) required

Time the event was created (ISO 8601).

data object required

Data of wallet.topup.

id string required

Identifier of the top-up (topup_<n> or payment_<n>).

method string | null required

Payment method or gateway.

source string required

manual (approved top-up request) or gateway (online payment).

One of: manual gateway

reference string | null optional

Payment reference. Present for gateway only.

amount number required

Amount in US dollars.

currency string required

constant "USD"

recharge_balance number required

Shipping balance after the event, in US dollars.

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "wallet.topup",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": "topup_1",
    "method": "bank_transfer",
    "source": "manual",
    "amount": 100,
    "currency": "USD",
    "recharge_balance": 150
  }
}

Event POST wallet.withdrawal #

Withdrawal decided

A payout withdrawal request was approved or rejected. Merchants only.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "wallet.withdrawal"

created_at string (date-time) required

Time the event was created (ISO 8601).

data object required

Data of wallet.withdrawal.

id string required

Identifier of the withdrawal (withdrawal_<n>).

status string required

approved or rejected.

One of: approved rejected

reference string | null optional

Payout reference. Present when approved.

note string | null optional

Administrator note. Present when rejected.

amount number required

Amount in US dollars.

currency string required

constant "USD"

recharge_balance number required

Shipping balance after the event, in US dollars.

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "wallet.withdrawal",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "id": "withdrawal_1",
    "status": "approved",
    "reference": null,
    "note": null,
    "amount": 50,
    "currency": "USD",
    "recharge_balance": 150
  }
}

Event POST ping #

Test message

Sent by POST /webhooks/{id}/test (and the panel's "Send test"). It cannot be selected as an event and is not retried automatically.

Message body

FieldType RequiredDescription
id string required

Event id (evt_ plus 24 lowercase characters); the same as the X-DS-Event-Id header.

pattern ^evt_[a-z0-9]{24}$

event string required

constant "ping"

created_at string (date-time) required

Time the event was created (ISO 8601).

data object required

Data of the ping test message.

message string required

Localized test text.

Example

JSON
{
  "id": "evt_abcdefghijklmnopqrstuvwx",
  "event": "ping",
  "created_at": "2026-01-01T12:10:00+00:00",
  "data": {
    "message": "Test message from Delivery Syria"
  }
}

Errors

Every error raised by the API itself uses the same JSON envelope. Build your logic on error.code and the HTTP status, never on the message text. These are the codes this API can return; errors that the framework raises before the API code runs (IP and route rate limits, unknown paths, wrong methods) use a plain {"message": "..."} format instead.

Common responses

These answers can come from any endpoint of this API. Each endpoint lists only its own specific answers and links here for these.

401 Missing or unknown API key.

Missing or unknown API key. unauthenticated: no key in an Authorization: Bearer header. invalid_key: the key is unknown (renewed, revoked, mistyped, or of another type).

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — No key

JSON · 401
{
  "success": false,
  "error": {
    "code": "unauthenticated",
    "message": "Send the key in the header: Authorization: Bearer <API key>"
  }
}

Example — Unknown key

JSON · 401
{
  "success": false,
  "error": {
    "code": "invalid_key",
    "message": "The key is invalid or revoked. Generate a new key from the merchant dashboard."
  }
}
403 Access refused.

Access refused. account_inactive: the user or the merchant profile is suspended or missing. api_not_allowed: the merchant belongs to a delivery company (the API is for platform merchants only). plan_feature: the plan does not include the API (or, for POST /webhooks, the webhooks feature), or the merchant has no active plan.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Suspended account

JSON · 403
{
  "success": false,
  "error": {
    "code": "account_inactive",
    "message": "Merchant account suspended or not found."
  }
}

Example — Company merchant

JSON · 403
{
  "success": false,
  "error": {
    "code": "api_not_allowed",
    "message": "Your account belongs to a delivery company — the API is for platform merchant accounts only."
  }
}

Example — Plan without the API

JSON · 403
{
  "success": false,
  "error": {
    "code": "plan_feature",
    "message": "Localized text that names the feature and the plan problem."
  }
}
429 Rate limit.

Rate limit. Two different answers share this status. (1) Plan limit (per minute or per day): the JSON error envelope with error.code rate_limited, error.retry_after and a Retry-After header. (2) IP or route limit: the framework answers before the API code runs, with {"message": "Too Many Attempts."} (JSON only when you send Accept: application/json; otherwise an HTML page) and the headers Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

Headers: Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

Example — Plan limit

JSON · 429
{
  "success": false,
  "error": {
    "code": "rate_limited",
    "message": "Localized text with the limit and the wait time",
    "retry_after": 42
  }
}

Example — IP or route limit

JSON · 429
{
  "message": "Too Many Attempts."
}
500 Unexpected server error.

Unexpected server error. The message quotes a short reference that support can find in the logs; repeat the request only if its outcome is unknown.

Headers: X-RateLimit-Limit X-RateLimit-Remaining

Example — Server error

JSON · 500
{
  "success": false,
  "error": {
    "code": "server_error",
    "message": "An error occurred on our side. If it happens again, contact support and mention reference E000000.",
    "reference": "E000000"
  }
}

Error codes

CodeHTTPDescription
invalid_key401

The key is unknown (renewed, revoked, mistyped, or of another type).

unauthenticated401

No key in an Authorization: Bearer header.

insufficient_balance402

The wallet balance is not enough (the message gives the required and available amounts). Send wait_for_balance: true to keep the parcel as awaiting_funds instead.

account_inactive403

The user or the merchant profile is suspended or missing.

api_not_allowed403

The merchant belongs to a delivery company (the API is for platform merchants only).

plan_feature403

The plan does not include the API (or, for POST /webhooks, the webhooks feature), or the merchant has no active plan.

not_found404

No parcel with this tracking number or reference in your account (a cancelled parcel no longer exists).

not_cancellable409

The parcel left the Pending or Received Warehouse status, or it went to an external carrier.

carrier_error422

The external carrier rejected the parcel or the carrier account is not usable.

delivery_failed422

The message did not arrive (non-2xx answer, redirect, timeout or connection error). response_status has the status your system returned, or null.

limit_reached422

You already have the maximum number of endpoints.

quote_failed422

The price could not be calculated.

validation_failed422

company_id missing or not available to you, or a field the pricing needs is missing.

rate_limited429
server_error500

More resources