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.
https://deliverysyria.com/docs/api/merchant-v1.jsonOverview
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/v1ReplaceYOUR_DOMAINwith your platform address.- Authentication
Your merchant developer API key in the
Authorizationheader: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) giveinvalid_key.Authorization: Bearer YOUR_API_KEY- Accept-Language
Language of error messages and labels (status labels, event notes, names). If the value starts with
enthe 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
/apirequest. (2) Route limits, counted per IP address on ONE shared counter: the group limit of 120 per minute, and 60 / 30 / 20 / 10 forPOST /parcels,GET /statement,POST /webhooksandPOST /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_limitedwithRetry-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
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
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 ( |
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 ( |
Example
{
"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
Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/me" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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.
Responses
200 Companies you can use.
Companies you can use.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
array<object> |
required | |
id |
integer |
required |
Send it as |
name |
string |
required |
Company name. |
type |
string |
required |
One of: |
pricing_mode |
string |
required |
How the price is calculated: One of: |
supports_own_account |
boolean |
required |
True when the carrier allows merchant accounts and you have linked yours. |
coverage |
object | null |
required |
|
Example
{
"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
Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/companies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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.
Responses
200 Active governorates.
Active governorates.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
array<object> |
required | |
id |
integer |
required |
Governorate id. When you send it in |
name |
string |
required |
English name. |
name_ar |
string |
required |
Arabic name. |
Example
{
"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
Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/governorates" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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.
Responses
200 Your pickup locations.
Your pickup locations.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
array<object> |
required | |
id |
integer |
required |
Send it as |
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 |
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
{
"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
Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/pickup-locations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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.
Responses
200 Stock per warehouse allocation.
Stock per warehouse allocation.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
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 |
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
{
"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
Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/warehouse/stock" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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 body
application/json · required
| Field | Type | Required | Description |
|---|---|---|---|
company_id |
integer |
required |
Delivery company or carrier, an |
recipient |
object |
optional |
The recipient. Each field may instead be sent flat at the top level ( |
name |
string |
optional |
Recipient name. max length 191 |
phone |
string |
optional |
Recipient phone number: digits, spaces, max length 30pattern |
address |
string |
optional |
Recipient address. max length 1000 |
governorate |
string |
optional |
Governorate name (English max length 120also accepted as |
lat |
number |
optional |
Recipient latitude. Required for distance pricing ( minimum -90maximum 90 |
lng |
number |
optional |
Recipient longitude. Required for distance pricing ( minimum -180maximum 180also accepted as |
reference |
string |
optional |
Your own reference (order number). It does not affect the price. max length 191also accepted as |
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 |
currency |
string |
optional |
Currency code of One of: default |
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 |
pickup_location_id |
integer |
optional |
One of your pickup locations ( also accepted as |
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 |
shipping_paid_by |
string |
optional |
Who pays the shipping fee, for carriers that support the choice: One of: |
Example
{
"company_id": 3,
"recipient": {
"governorate": "Damascus"
},
"weight": 2
}Responses
200 The quote.
The quote.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
oneOf |
required |
The shape depends on the company: a delivery company gives |
Alternative QuoteDeliveryCompany Quote for a delivery company. | |||
pricing_mode |
string |
required |
The pricing mode used. One of: |
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 |
string |
required |
Carrier name. |
account |
string |
required |
Which account would be used: the platform account or your own account at the carrier. One of: |
carrier_fee |
number |
required |
The carrier's price in the carrier's currency. |
carrier_currency |
string |
required |
Currency of |
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 ( |
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 |
Example
{
"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
{
"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
{
"success": false,
"error": {
"code": "quote_failed",
"message": "Could not calculate the price."
}
}Code samples
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
$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
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
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
| Name | Type | Required | Description |
|---|---|---|---|
reference |
string |
optional |
Exact match on the Example |
from |
string (date) |
optional |
First creation day, Example |
to |
string (date) |
optional |
Last creation day, Example |
stage |
string |
optional |
Filter by the One of: Example |
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 minimum 1maximum 100default |
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 |
Responses
200 A page of parcels.
A page of parcels.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
array<Parcel> |
required | |
meta |
Meta |
required |
Pagination block. There are no links and no cursor. |
Example
{
"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
Code samples
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
$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
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
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
| Field | Type | Required | Description |
|---|---|---|---|
company_id |
integer |
required |
Delivery company or carrier, an |
recipient |
object |
required |
The recipient. Each field may instead be sent flat at the top level ( |
name |
string |
required |
Recipient name. max length 191 |
phone |
string |
required |
Recipient phone number: digits, spaces, max length 30pattern |
address |
string |
required |
Recipient address. max length 1000 |
governorate |
string |
optional |
Governorate name (English max length 120also accepted as |
lat |
number |
optional |
Recipient latitude. Required for distance pricing ( minimum -90maximum 90 |
lng |
number |
optional |
Recipient longitude. Required for distance pricing ( minimum -180maximum 180also accepted as |
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 max length 191also accepted as |
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 |
currency |
string |
optional |
Currency code of One of: default |
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 |
pickup_location_id |
integer |
optional |
One of your pickup locations ( also accepted as |
warehouse_items |
array<object> |
optional |
Products shipped from your stock. Required when the pickup location is a warehouse; ignored otherwise. Also accepted as also accepted as |
sku |
string |
optional |
Product code (SKU) of the stock item. |
item_id |
integer |
optional |
Stock item id (alternative to |
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 |
shipping_paid_by |
string |
optional |
Who pays the shipping fee, for carriers that support the choice: One of: |
wait_for_balance |
boolean |
optional |
When the wallet balance is short, keep the parcel as |
Example
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
Parcel |
required |
A parcel as returned by the API and carried by webhooks (same fields every time). |
duplicate |
boolean |
required |
constant |
Example
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
Parcel |
required |
A parcel as returned by the API and carried by webhooks (same fields every time). |
Example
{
"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
{
"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
{
"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
{
"success": false,
"error": {
"code": "carrier_error",
"message": "Localized message from the carrier."
}
}Code samples
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
$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
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
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.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
tracking |
string |
required |
A tracking number, or the Example |
Responses
200 The parcel with its events.
The parcel with its events.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
object |
required |
The parcel object plus its history ( |
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: |
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
{
"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
{
"success": false,
"error": {
"code": "not_found",
"message": "No parcel with this number for your account."
}
}Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/parcels/DS-000000" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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
| Name | Type | Required | Description |
|---|---|---|---|
tracking |
string |
required |
A tracking number, or the Example |
Responses
200 The parcel was cancelled and refunded.
The parcel was cancelled and refunded.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
object |
required |
Result of a cancellation. |
tracking_number |
string |
required |
Tracking number of the cancelled parcel. |
cancelled |
boolean |
required |
constant |
refunded |
boolean |
required |
constant |
Example
{
"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
{
"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
{
"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."
}
}Code samples
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
$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
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
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.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
from |
string (date) |
optional |
First day, Example |
to |
string (date) |
optional |
Last day, Example |
wallet |
string |
optional |
Only this wallet: One of: |
category |
string |
optional |
Only this movement type, for example pattern Example |
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 minimum 1maximum 500default |
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 |
Responses
200 The statement.
The statement.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
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 ( |
to |
string (date) |
required |
Last day ( |
currency |
string |
required |
constant |
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: |
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 |
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
{
"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
Code samples
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
$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
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
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
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
array<object> |
required | |
event |
string |
required |
Event name. One of: |
name |
string |
required |
Short name in the request language. |
description |
string |
required |
When it is sent, in the request language. |
Example
{
"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
Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/webhooks/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
array<Webhook> |
required |
Example
{
"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
Code samples
curl
curl -X GET "https://YOUR_DOMAIN/api/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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
| Field | Type | Required | Description |
|---|---|---|---|
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 |
events |
array<string> |
required |
Events to receive: at least one entry, each One of: min items 1 |
active |
boolean |
optional |
Whether the endpoint is enabled (default true). |
Example
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
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; |
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 ( |
Example
{
"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
{
"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
{
"success": false,
"error": {
"code": "limit_reached",
"message": "You have reached your plan limit: 10 webhook links."
}
}Code samples
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
$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
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
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
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer |
required |
Id of the webhook endpoint (from Example |
Responses
200 The endpoint was deleted.
The endpoint was deleted.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
object |
required |
Result of a deletion. |
deleted |
boolean |
required |
constant |
Example
{
"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
{
"success": false,
"error": {
"code": "not_found",
"message": "Link not found."
}
}Code samples
curl
curl -X DELETE "https://YOUR_DOMAIN/api/v1/webhooks/7" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer |
required |
Id of the webhook endpoint (from Example |
Request body
application/json · required
| Field | Type | Required | Description |
|---|---|---|---|
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 |
events |
array<string> |
optional |
New list of events (at least one). One of: min items 1 |
active |
boolean |
optional |
Turning it on again resets the failure counter and clears the suspension reason. |
Example
{
"active": true
}Responses
200 The updated endpoint.
The updated endpoint.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
Webhook |
required |
A webhook endpoint (the signing key is never included). |
Example
{
"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
{
"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
{
"success": false,
"error": {
"code": "validation_failed",
"message": "The URL must start with https://",
"fields": {
"url": [
"The URL must start with https://"
]
}
}
}Code samples
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
$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
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
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
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer |
required |
Id of the webhook endpoint (from Example |
Responses
200 Your system answered with a 2xx status.
Your system answered with a 2xx status.
Headers: X-RateLimit-Limit X-RateLimit-Remaining
| Field | Type | Required | Description |
|---|---|---|---|
success |
boolean |
required |
constant |
data |
object |
required |
Result of a successful test message. |
delivered |
boolean |
required |
constant |
response_status |
integer |
required |
HTTP status your endpoint answered with. |
Example
{
"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
{
"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
{
"success": false,
"error": {
"code": "delivery_failed",
"message": "Not delivered: your system answered 500",
"response_status": 500
}
}Code samples
curl
curl -X POST "https://YOUR_DOMAIN/api/v1/webhooks/7/test" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"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
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
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).
| Field | Type | Required | Description |
|---|---|---|---|
id |
integer |
required |
Parcel id. |
tracking_number |
string |
required |
Tracking number. Use it in |
reference |
string | null |
required |
Your own reference (order number) sent at creation. |
stage |
string |
required |
Simplified, stable state. Prefer it to One of: |
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.
| Field | Type | Required | Description |
|---|---|---|---|
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).
| Field | Type | Required | Description |
|---|---|---|---|
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; |
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
| Name | Type | Description |
|---|---|---|
X-DS-Event | string | The event name (for example |
X-DS-Event-Id | string | Event id, |
X-DS-Delivery | string (uuid) | UUID of this message to this endpoint. The same across attempts. |
X-DS-Attempt | integer | Attempt number, starting at 1. |
X-DS-Timestamp | string | Send time in Unix seconds, renewed on every attempt. It is part of the signed text. |
X-DS-Signature | string |
|
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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
created_at |
string (date-time) |
required |
Time the event was created (ISO 8601). |
data |
object |
required |
Data of |
id |
string |
required |
Identifier of the top-up ( |
method |
string | null |
required |
Payment method or gateway. |
source |
string |
required |
One of: |
reference |
string | null |
optional |
Payment reference. Present for |
amount |
number |
required |
Amount in US dollars. |
currency |
string |
required |
constant |
recharge_balance |
number |
required |
Shipping balance after the event, in US dollars. |
Example
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
created_at |
string (date-time) |
required |
Time the event was created (ISO 8601). |
data |
object |
required |
Data of |
id |
string |
required |
Identifier of the withdrawal ( |
status |
string |
required |
One of: |
reference |
string | null |
optional |
Payout reference. Present when |
note |
string | null |
optional |
Administrator note. Present when |
amount |
number |
required |
Amount in US dollars. |
currency |
string |
required |
constant |
recharge_balance |
number |
required |
Shipping balance after the event, in US dollars. |
Example
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
required |
Event id ( pattern |
event |
string |
required |
constant |
created_at |
string (date-time) |
required |
Time the event was created (ISO 8601). |
data |
object |
required |
Data of the |
message |
string |
required |
Localized test text. |
Example
{
"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
{
"success": false,
"error": {
"code": "unauthenticated",
"message": "Send the key in the header: Authorization: Bearer <API key>"
}
}Example — Unknown key
{
"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
{
"success": false,
"error": {
"code": "account_inactive",
"message": "Merchant account suspended or not found."
}
}Example — Company merchant
{
"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
{
"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
{
"success": false,
"error": {
"code": "rate_limited",
"message": "Localized text with the limit and the wait time",
"retry_after": 42
}
}Example — IP or route limit
{
"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
{
"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
| Code | HTTP | Description |
|---|---|---|
invalid_key | 401 | The key is unknown (renewed, revoked, mistyped, or of another type). |
unauthenticated | 401 | No key in an |
insufficient_balance | 402 | The wallet balance is not enough (the message gives the required and available amounts). Send |
account_inactive | 403 | The user or the merchant profile is suspended or missing. |
api_not_allowed | 403 | The merchant belongs to a delivery company (the API is for platform merchants only). |
plan_feature | 403 | The plan does not include the API (or, for |
not_found | 404 | No parcel with this tracking number or reference in your account (a cancelled parcel no longer exists). |
not_cancellable | 409 | The parcel left the Pending or Received Warehouse status, or it went to an external carrier. |
carrier_error | 422 | The external carrier rejected the parcel or the carrier account is not usable. |
delivery_failed | 422 | The message did not arrive (non-2xx answer, redirect, timeout or connection error). |
limit_reached | 422 | You already have the maximum number of endpoints. |
quote_failed | 422 | The price could not be calculated. |
validation_failed | 422 |
|
rate_limited | 429 | |
server_error | 500 |
More resources
- Authentication and API keys
- Merchant API v1 reference
- Pagination and filters in the API
- API versions and safe retries (idempotency)
- Outgoing webhooks: instant notifications to your system
- Webhook retries, failures and automatic suspension
- Verify the webhook signature (X-DS-Signature)
- Delivery Syria Company API
- API reference