Integrate your system with Shipper to create and track orders programmatically.
The Shipper Open API is a REST API that allows you to:
https://server.shipper.network/api/v1To get started, generate an API key from your Shipper dashboardunder Integrations > API.
All API requests require a Bearer token in the Authorization header.
Authorization: Bearer YOUR_API_KEY
Accept: application/json
Content-Type: application/jsonRequests are rate-limited per API key. Exceeding the limit returns HTTP 429 (Too Many Requests).
| Endpoint Type | Limit | Endpoints |
|---|---|---|
| Read | 120req/min | GET /products, /orders, /orders/:id, /webhooks |
| Dashboard | 30req/min | GET /dashboard/* |
| Reference | 60req/min | GET /countries, /countries/:code/divisions |
| Write | 30req/min | POST /orders, /webhooks |
| Destructive | 20req/min | POST /orders/:id/cancel, DELETE /webhooks/:id |
Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) are included in every response.
GET /countriesReturns all supported countries with their geographic structure. Use this to understand which address division levels (governorate, delegation, etc.) are required for each country.
curl https://server.shipper.network/api/v1/countries \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
[
{
"code": "TN",
"name": "Tunisia",
"geo_structure": {
"fields": [
{ "id": "division_1_id", "label": "Governorate", "level": 1, "required": true, "depends_on": null },
{ "id": "division_2_id", "label": "Delegation", "level": 2, "required": true, "depends_on": "division_1_id" }
]
}
}
]The geo_structure.fields array tells you how many division levels exist, their labels, and dependencies. Use depends_on to build cascading dropdowns.
GET /countries/{code}/divisionsReturns division names for a country. Call this to get valid values for address.division_1 and address.division_2 when creating orders.
| Parameter | Type | Required | Description |
|---|---|---|---|
level | integer | - | Division level (default: 1 = top level) |
parent_id | integer | - | Filter by parent division ID to get children |
curl https://server.shipper.network/api/v1/countries/TN/divisions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"{
"country": "TN",
"geo_structure": { ... },
"divisions": [
{ "id": 1, "name": "Tunis", "level": 1, "parent_id": null },
{ "id": 2, "name": "Sfax", "level": 1, "parent_id": null },
{ "id": 3, "name": "Sousse", "level": 1, "parent_id": null }
]
}curl "https://server.shipper.network/api/v1/countries/TN/divisions?level=2&parent_id=1" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"{
"country": "TN",
"geo_structure": { ... },
"divisions": [
{ "id": 10, "name": "Bab El Bhar", "level": 2, "parent_id": 1 },
{ "id": 11, "name": "Bab Souika", "level": 2, "parent_id": 1 },
{ "id": 12, "name": "Carthage", "level": 2, "parent_id": 1 }
]
}GET /productsReturns your product catalog with UUIDs. You need product UUIDs to create orders via the API.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | - | Page number (default: 1) |
per_page | integer | - | Items per page (default: 20, max: 100) |
search | string | - | Search by product name or SKU |
curl "https://server.shipper.network/api/v1/products?per_page=10&search=shirt" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"data": [
{
"uuid": "2e97b37a-3118-4905-842c-19706bcf1455",
"name": "Blue T-Shirt",
"sku": "SKU-001",
"price": 190,
"cost": 75,
"status": "active",
"link": "https://app.shipper.market/products/2e97b37a-3118-4905-842c-19706bcf1455",
"supplier_identifier": "RU836",
"available_qte": 42,
"variants": [
{
"uuid": "a1b2c3d4-5678-9012-abcd-ef0123456789",
"name": "Size L",
"sku": "SKU-001-L",
"price": 190,
"cost": 75,
"available_qte": 15
}
]
}
],
"pagination": { "total": 50, "current_page": 1, "per_page": 10, "last_page": 5 }
}price — your selling price (what the customer pays), in your organization currency.cost — the supplier cost (what you pay the supplier per unit). Available on both the product and per-variant.link — shareable Shipper Market URL for this product (useful for your team or marketing).supplier_identifier — the supplier's Shipper username (e.g. RU836). Identifies which supplier fulfills the product.available_qte — total stock units available across active warehouses (sum of public_stocks.qte).GET /products/{uuid}Returns a single product by UUID, including its variants and available stock quantities.
curl https://server.shipper.network/api/v1/products/2e97b37a-3118-4905-842c-19706bcf1455 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"uuid": "2e97b37a-3118-4905-842c-19706bcf1455",
"name": "Blue T-Shirt",
"sku": "SKU-001",
"price": 190,
"cost": 75,
"status": "active",
"link": "https://app.shipper.market/products/2e97b37a-3118-4905-842c-19706bcf1455",
"supplier_identifier": "RU836",
"available_qte": 42,
"variants": [
{ "uuid": "a1b2c3d4-...", "name": "Size L", "sku": "SKU-001-L", "price": 190, "cost": 75, "available_qte": 15 },
{ "uuid": "e5f6a7b8-...", "name": "Size M", "sku": "SKU-001-M", "price": 190, "cost": 75, "available_qte": 27 }
]
}GET /ordersReturns a paginated list of your orders. Use filters to narrow results by status, date range, or search terms.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | - | Page number (default: 1) |
per_page | integer | - | Items per page (default: 20, max: 100) |
status | string | - | Filter by shipment status (e.g. delivered, awaitingpackaging, canceled) |
start_date | string | - | Filter orders created on or after this datetime. Accepts YYYY-MM-DD (interpreted as 00:00:00) or YYYY-MM-DD HH:MM[:SS] for minute precision. |
end_date | string | - | Filter orders created on or before this datetime. Date-only inputs (YYYY-MM-DD) include the entire day; YYYY-MM-DD HH:MM[:SS] for minute precision. |
search | string | - | Search by order ID, customer name, or phone number |
URL-encoding for datetime values:the space between the date and time must be encoded as %20 (the standard URL-encoded space). A common mistake is to use %10 or %15 — those are ASCII control characters and will be rejected.
Date inputs without an explicit timezone are interpreted in UTC (the server timezone). Order timestamps are also stored in UTC, so YYYY-MM-DD filters a UTC calendar day. Use ISO 8601 with an offset for region-local boundaries (e.g. 2026-05-14T00:00:00+01:00).
Unparseable start_date or end_date values return HTTP 422 with a hint pointing at the encoding mistake — they are not silently ignored. Example:
# WRONG — %10 is not a space
https://server.shipper.network/api/v1/orders?start_date=2026-03-29%1009:00
# → 422
{
"error": "Invalid start_date format.",
"value": "2026-03-29\u001009:00",
"expected_formats": [
"YYYY-MM-DD",
"YYYY-MM-DD HH:MM",
"YYYY-MM-DD HH:MM:SS",
"ISO 8601 (e.g. 2026-05-14T10:30:00Z)"
],
"hint": "A space in the URL must be encoded as %20, not %10 or +."
}
# RIGHT — %20 is the space
https://server.shipper.network/api/v1/orders?start_date=2026-03-29%2009:00curl "https://server.shipper.network/api/v1/orders?status=delivered&start_date=2026-05-01%2009:00&per_page=10" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"data": [
{
"id": 555,
"status": "Paid",
"store_name": "My Store",
"is_cod": true,
"is_paid": true,
"created_at": "2026-05-10T10:00:00.000000Z",
"address": { "first_name": "Omar", "last_name": "Ben Ali", "phone1": "+21628177188" },
"items_count": 3,
"shipments_count": 1,
"products": [
{ "id": 442, "name": "Smart Sensor Light Toilet Lid" }
],
"revenue": "59",
"external_order_id": "ORD-123",
"external_order_url": "https://mystore.com/orders/123"
}
],
"pagination": { "total": 150, "current_page": 1, "per_page": 10, "last_page": 15 }
}status — computed from the order's shipments. Possible values: Pending, Fulfilled, In Transit, Delivered, Partial Delivery, Returned, Partial Return, Canceled, Paid. May also be null for orders without shipments yet.items_count — total units ordered (sum of quantities across cart items).products — distinct retailer products referenced by the order. Empty for orders without fulfillment yet.revenue — sum of retailer-shipment revenue transactions in Successful or Under Review status (i.e. realized + earned-but-settling). Reported in your organization currency, as a plain decimal string. Zero until at least one shipment is delivered.POST /ordersCreates a new order in Shipper. Returns the order ID which you can use to track the order.
| Parameter | Type | Required | Description |
|---|---|---|---|
address | object | * | Recipient address object (see fields below) |
address.name | string | * | Full name - automatically split into first and last name |
address.phone1 | string | * | Primary phone number |
address.phone2 | string | - | Secondary phone number |
address.address1 | string | * | Street address line 1 |
address.address2 | string | - | Street address line 2 |
address.division_1 | string | - | State / Governorate - use exact name from GET /countries/{code}/divisions |
address.division_2 | string | - | City / Delegation - use exact name from GET /countries/{code}/divisions?level=2&parent_id=... |
address.country | string | * | Two-letter country code (e.g. TN, DZ, MA) |
items | array | * | Array of order line items |
items[].id | string | * | Product or variant UUID from GET /products |
items[].quantity | integer | - | Quantity (default: 1) |
items[].total_price | number | - | Total price for this line. Defaults to product price x quantity |
shipping_total | number | - | Shipping cost charged to the customer (default: 0) |
is_cod | boolean | - | ) |
auto_fulfill | boolean | - | Send directly to fulfillment without creating a draft (default: false) |
with_confirmation | boolean | - | Require a phone confirmation call before shipping (default: false) |
store_name | string | - | Source store name - appears in order tracking for your reference |
external_order_id | string | - | Your system order reference ID - displayed in the Shipper dashboard |
external_order_url | string | - | URL to the order in your system - clickable from the Shipper dashboard |
curl -X POST https://server.shipper.network/api/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"address": {
"name": "Omar Ben Ali",
"division_1": "Tunis",
"division_2": null,
"phone1": "28177188",
"address1": "Rue de la Liberte",
"country": "TN"
},
"items": [
{ "quantity": 1, "id": "YOUR_PRODUCT_UUID", "total_price": 190 }
],
"shipping_total": 9,
"is_cod": true,
"auto_fulfill": true,
"with_confirmation": true,
"store_name": "My Store",
"external_order_id": "ORD-12345",
"external_order_url": "https://mystore.com/orders/12345"
}'201Created
{
"id": 555
}GET /orders/{id}Returns full order details including current status, shipment tracking numbers with delivery logs, and the recipient address.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | * | The order ID returned when the order was created |
curl https://server.shipper.network/api/v1/orders/555 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"id": 555,
"status": "Paid",
"contact_task_status": "called",
"shipments": [
{
"uuid": "11111111-2222-3333-4444-555555555555",
"status": "delivered",
"tracking_numbers": [
{
"number": "99999999999",
"logs": [
{ "status": "at_carrier_origin_facility", "created_at": "2025-01-15T02:02:59.000000Z", "external_timestamp": "2025-01-14 22:48:00" },
{ "status": "in_transit", "created_at": "2025-01-15T02:02:59.000000Z", "external_timestamp": "2025-01-14 23:05:00" },
{ "status": "delivered", "created_at": "2025-01-18T02:09:19.000000Z", "external_timestamp": "2025-01-17 14:16:00" }
]
}
]
}
],
"address": {
"first_name": "Omar", "last_name": "Ben Ali",
"phone1": "+21628177188", "phone2": "",
"address1": "Rue de la Liberte", "address2": "",
"division_1": "Tunis", "division_2": null, "country": "TN"
}
}POST /orders/{id}/cancelCancels an order and its associated shipments. This works for orders that are still in the confirmation phase (awaiting phone confirmation) or in the preparation phase (awaiting packaging). Orders that have already been shipped cannot be canceled via the API.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | * | The order ID to cancel |
curl -X POST https://server.shipper.network/api/v1/orders/555/cancel \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"success": true,
"canceled_shipments": 1
}422If no shipments can be canceled:
{
"error": "No shipments available to cancel"
}Aggregated stats endpoints powering the retailer dashboard. All routes require a retailer organization API token; non-retailer tokens get HTTP 403.
start_date, end_date — optional date filters. Accept YYYY-MM-DD or YYYY-MM-DD HH:MM[:SS]. The space between date and time must be encoded as %20 — see the URL-encoding note in the /orders section.retailer_product_id — optional: scope the stats to a single retailer product (by its sequential id).GET /dashboard/overviewRevenue / expense / profit breakdown for shipments, grouped by status (pending, delivered-paid, delivered-unpaid, failed-delivery). Each group exposes per-line-item expense breakdowns and per-order-type sub-rollups.
curl "https://server.shipper.network/api/v1/dashboard/overview?start_date=2026-04-01&end_date=2026-04-30" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"shipments_pending": { /* StatusGroup, see below */ },
"shipments_delivered_paid": { ... },
"shipments_delivered_non_paid": { ... },
"shipments_failed_delivery": { ... },
"stats_by_order_type": [
{
"order_type": "dropshipping",
"shipments_pending": { ... },
"shipments_delivered_paid": { ... },
"shipments_delivered_non_paid": { ... },
"shipments_failed_delivery": { ... }
}
]
}{
"status": "shipments_delivered_paid",
"count": 133,
"orders_with_revenue_count": 129,
"revenue": [{ "amount": "9750.00", "currency": { "id": "1", "ISO_4217": "TND", "symbol": "د.ت", "decimals": 3 } }],
"revenue_expected": [...],
"revenue_transferred": [...],
"expenses": [...],
"expenses_transferred": [...],
"expenses_expected": [...],
"net_profits": [...],
"expenses_breakdown": {
"shipping": [...],
"packaging": [...],
"service": [...],
"qc": [...],
"payment_facility": [...],
"confirmation": [...],
"call_forwarding": [...],
"follow_up": [...],
"upsell": [...],
"prepaid_cost": [...],
"product_cost": [...],
"dropshipping_fee": [...],
"return_exchange_fee": [...]
},
"expenses_breakdown_transferred": { /* same 13 keys */ }
}Money fields are arrays of {amount, currency} for forward-compatibility with multi-currency retailers; today a single entry is returned in the organization's default currency. Amounts are decimal strings to preserve precision (transactions store decimal(65,30)).
GET /dashboard/delivery-statsShipment-status counts, delivery-failure reasons breakdown, and business-day delivery-speed KPIs. Scoped to dropshipping orders.
curl "https://server.shipper.network/api/v1/dashboard/delivery-stats?start_date=2026-04-01&end_date=2026-04-30" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"total_by_status": [
{ "status": "delivered", "count": 4843, "on_hold_count": 0 },
{ "status": "canceled", "count": 238, "on_hold_count": 0 },
{ "status": "returnedtosender", "count": 28, "on_hold_count": 0 }
],
"total_delivery_failures_by_reason": [
{ "reason": "Customer unavailable for delivery", "count": 993 },
{ "reason": "Trust or credibility issues at delivery", "count": 136 },
{ "reason": null, "count": 565 }
],
"avg_delivery_speed_business_days": 1.6,
"max_delivery_speed_business_days": 28,
"delivered_within_1bd_pct": 66.3,
"delivered_within_2bd_pct": 21,
"delivered_over_2bd_pct": 12.7
}GET /dashboard/confirmation-statsLead-funnel and delivery-conversion stats for orders confirmed by an external Shipper confirmation service.
curl "https://server.shipper.network/api/v1/dashboard/confirmation-stats?start_date=2026-04-01&end_date=2026-04-30" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"has_stats": true,
"stats": {
"leads": {
"total": 5484,
"confirmed": 3371,
"failed": 599,
"closed_unreachable": 1068,
"closed_unconfirmed": 445,
"under_process": 1,
"no_attempt": 0
},
"delivery_rates": {
"delivered": 2240,
"failed": 1072,
"returning": 13,
"in_transit": 8,
"not_shipped": 12,
"total_pipeline": 3345,
"total_final": 3312,
"overall_rate": 66.97,
"absolute_rate": 67.63
}
}
}has_stats is false when the organization has no orders routed through external confirmation; stats will be null in that case.
GET /dashboard/internal-confirmation-statsSame shape as /dashboard/confirmation-stats, but scoped to orders the retailer confirmed in-house (no Shipper confirmation service involved). Useful for retailers who run both flows in parallel.
curl "https://server.shipper.network/api/v1/dashboard/internal-confirmation-stats" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200 — identical shape to /dashboard/confirmation-stats. When the retailer has no in-house-confirmed orders, has_stats is false and stats is null:
{
"has_stats": false,
"stats": null
}GET /dashboard/product-performancePaginated, sortable per-product performance: shipment counts and units across statuses, plus per-product confirmation funnel and the same status-group rollups as /dashboard/overview.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | - | Page number (default: 1) |
per_page | integer | - | Items per page (default: 20, max: 50) |
sort_by | string | - | One of: shipped_orders, delivered_orders, failed_orders, confirmed_return_orders, in_transit_orders, returning_orders, leads_total, leads_confirmed, overall_confirmation_rate, absolute_confirmation_rate, conversion_rate. Default: shipped_orders. Invalid values return 422. |
start_date | string | - | Optional date filter (see Dashboard intro). |
end_date | string | - | Optional date filter (see Dashboard intro). |
retailer_product_id | integer | - | Scope to a single retailer product. |
curl "https://server.shipper.network/api/v1/dashboard/product-performance?per_page=10&sort_by=delivered_orders" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{
"products": [
{
"retailer_product_id": "550",
"name": "SMOKURE STOP TABAC 40 CAPSULES",
"shipped_orders": 2351, "shipped_units": 2418,
"delivered_orders": 1442, "delivered_units": 1487,
"failed_orders": 898, "failed_units": 921,
"in_transit_orders": 12, "in_transit_units": 13,
"returning_orders": 4, "returning_units": 4,
"confirmed_return_orders": 320, "confirmed_return_units": 327,
"leads_total": 3531,
"leads_confirmed": 2374,
"leads_failed": 121,
"leads_unreachable": 715,
"leads_duplicate_wrong": 18,
"leads_closed_other": 303,
"overall_confirmation_rate": 67.2,
"absolute_confirmation_rate": 70.5,
"conversion_rate": 40.8,
"shipments_pending": { /* StatusGroup */ },
"shipments_delivered_paid": { ... },
"shipments_delivered_non_paid": { ... },
"shipments_failed_delivery": { ... }
}
],
"total": 127
}Pagination is page/per_page-driven; the response carries only the total count. Compute last_page as Math.ceil(total / per_page).
Webhooks let you receive real-time HTTP notifications when order or shipment statuses change, instead of polling the API.
POST /webhooksRegister a URL to receive webhook events. You can register up to 5 webhooks per API key. The URL must use HTTPS.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | * | HTTPS URL that will receive POST requests with event data |
events | array | * | Events to subscribe to: "order.status_changed" and/or "shipment.status_changed" |
secret | string | - | Secret key (min 16 chars) used to sign payloads with HMAC-SHA256 for verification |
curl -X POST https://server.shipper.network/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mystore.com/webhooks/shipper",
"events": ["order.status_changed", "shipment.status_changed"],
"secret": "my-webhook-secret-key"
}'201
{
"id": 1,
"url": "https://mystore.com/webhooks/shipper",
"events": ["order.status_changed", "shipment.status_changed"],
"status": "active",
"created_at": "2025-03-29T10:00:00.000000Z"
}GET /webhooksReturns all webhooks registered for the current API key, including their status and failure count.
curl https://server.shipper.network/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
[
{
"id": 1,
"url": "https://mystore.com/webhooks/shipper",
"events": ["order.status_changed", "shipment.status_changed"],
"status": "active",
"last_triggered_at": "2025-03-29T15:30:00.000000Z",
"failure_count": 0,
"created_at": "2025-03-29T10:00:00.000000Z"
}
]DELETE /webhooks/{id}Removes a webhook registration. The webhook will stop receiving events immediately.
curl -X DELETE https://server.shipper.network/api/v1/webhooks/1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"200
{ "success": true }When an event occurs, Shipper sends a POST request to your registered URL with a JSON body:
{
"event": "order.status_changed",
"timestamp": "2025-01-15T10:30:00.000000Z",
"data": {
"order_id": 555,
"status": "Paid",
"previous_status": "Pending"
}
}{
"event": "shipment.status_changed",
"timestamp": "2025-01-15T10:30:00.000000Z",
"data": {
"order_id": 555,
"shipment_uuid": "11111111-2222-3333-4444-555555555555",
"status": "delivered",
"previous_status": "onitsway"
}
}If you provided a secret when registering the webhook, each request includes an X-Shipper-Signature header. Verify it by computing the HMAC-SHA256 of the raw request body using your secret:
// Node.js example
const crypto = require('crypto');
const signature = crypto.createHmac('sha256', 'my-webhook-secret-key')
.update(rawBody)
.digest('hex');
const isValid = signature === req.headers['x-shipper-signature'];The API uses standard HTTP status codes. All error responses include a JSON body with details.
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Resource created successfully |
| 401 | Unauthorized - missing or invalid API key |
| 403 | Forbidden - API key is disabled or revoked |
| 404 | Not found - the requested resource does not exist |
| 422 | Validation error - check the errors object in the response body for details |
| 429 | Too Many Requests - rate limit exceeded. Wait and retry |