ZR Express API for Node.js and PHP
Create, track, list and cancel your ZRExpress parcels with the dz-delivery SDK — plus PDF labels, bulk creation and webhook verification.
Install
npm install @dz-delivery/core
composer require dz-delivery/sdk
Node.js ≥ 18 (zero dependencies) or PHP ≥ 8.2 (Guzzle 7). Every carrier implements the same interface, so the same code works with the others once the client is configured; some operations are not available on every carrier and throw NOT_SUPPORTED (see the capability matrix).
ZRExpress credentials
| Option | Header sent | Where to find it |
|---|---|---|
apiKey | X-Api-Key | ZRExpress portal (secret API key) |
tenant | X-Tenant | ZRExpress portal (tenant ID) |
Official documentation: docs.zrexpress.app.
ZRExpress with TypeScript / Node.js
import { createClient, isShippingError } from '@dz-delivery/core'; const zr = createClient('zrexpress', { apiKey: process.env.ZREXPRESS_API_KEY!, tenant: process.env.ZREXPRESS_TENANT!, }); try { const wilayas = await zr.getWilayas(); // [{ id, name, code: '16' }] const communes = await zr.getCommunes(16); // ZR id or official code 1–58 const hubs = await zr.getStopDesks({ wilayaId: 16 }); // pickup points const quote = await zr.getFees({ toWilayaId: 16, isStopDesk: false }); // { deliveryFee: 400, stopDeskFee: 250, currency: 'DZD' } const parcel = await zr.createParcel({ recipientName: 'Mohamed Amine', phone: '0550 12 34 56', // normalized to +213… address: 'Cité 221 lots, n°98', wilayaName: 'Alger', communeName: 'Bab Ezzouar', productName: 'T-shirt', price: 2500, // amount collected, fees included (≤ 150,000 DZD) orderId: 'ORDER-1001', }); const { parcel: current, events } = await zr.track(parcel.trackingNumber); // Sync the account's parcels (pull) const page = await zr.listParcels({ updatedSince: '2026-10-01T00:00:00+00:00', allPages: true }); await zr.cancelParcel(parcel.trackingNumber); // tracking number or UUID } catch (err) { if (isShippingError(err)) console.error(err.code, err.message); }
ZRExpress with PHP / Laravel
use DzDelivery\ClientFactory; use DzDelivery\Errors\ShippingError; $zr = ClientFactory::create('zrexpress', [ 'apiKey' => getenv('ZREXPRESS_API_KEY'), 'tenant' => getenv('ZREXPRESS_TENANT'), ]); try { $quote = $zr->getFees(['toWilayaId' => 16, 'isStopDesk' => false]); $hubs = $zr->getStopDesks(['wilayaId' => 16]); $parcel = $zr->createParcel([ 'recipientName' => 'Mohamed Amine', 'phone' => '0550123456', 'address' => 'Cité 221 lots, n°98', 'wilayaName' => 'Alger', 'communeName' => 'Bab Ezzouar', 'productName' => 'T-shirt', 'price' => 2500, 'orderId' => 'ORDER-1001', ]); $result = $zr->track($parcel->trackingNumber); $page = $zr->listParcels(['orderId' => 'ORDER-1001']); } catch (ShippingError $e) { echo "{$e->code}: {$e->getMessage()}"; }
Installation, Laravel, errors and testing: see the full PHP guide.
Supported methods
| Method | Support | Notes |
|---|---|---|
createParcel | yes | The SDK reads the new parcel back to return its tracking number. |
track | yes | Unified status + state history. |
getFees | yes | Home and pickup-point rates for the wilaya. |
getWilayas / getCommunes | yes | ZR territories; official code or name accepted. |
getStopDesks | yes | ZR hubs that act as pickup points. |
listParcels | yes | Paginated search, filters orderId, keyword, status, updatedSince. |
cancelParcel | yes | Tracking number or UUID. |
healthCheck | yes | Checks the credentials (real API call). |
ZRExpress labels, bulk shipping and webhooks
These ZRExpress-specific features are available through createZRExpress() (TS) or new ZRExpressProvider() (PHP).
import { createZRExpress, verifyZRExpressWebhook } from '@dz-delivery/core'; const zr = createZRExpress({ apiKey, tenant }); // PDF labels (max 250) const { fileUrl } = await zr.generateLabels(['16-AAA111-ZR', '16-BBB222-ZR']); // Bulk create (max 100, orderId required) const { successes, failures } = await zr.createParcelsBulk([ { ...parcelInput, orderId: 'ORD-1' }, { ...parcelInput, orderId: 'ORD-2' }, ]); // Signed webhooks (raw body required) app.post('/webhooks/zrexpress', express.raw({ type: '*/*' }), (req, res) => { const event = verifyZRExpressWebhook(req.body, req.headers, process.env.ZREXPRESS_WEBHOOK_SECRET!); // event.type · event.trackingNumber · event.status (unified) res.sendStatus(200); });
use DzDelivery\Models\ClientOptions; use DzDelivery\Providers\ZRExpressProvider; use DzDelivery\Providers\ZRExpress\Webhook; $zr = new ZRExpressProvider(ClientOptions::fromArray(['apiKey' => getenv('ZREXPRESS_API_KEY'), 'tenant' => getenv('ZREXPRESS_TENANT')])); $zr->createParcelsBulk([...]); $raw = file_get_contents('php://input'); $event = Webhook::verify($raw, getallheaders(), getenv('ZREXPRESS_WEBHOOK_SECRET'));
Details: ZREXPRESS.md.
ZRExpress statuses → unified statuses
ZRExpress names its states either as slugs (commande_recue) or in PascalCase (OutForDelivery): the SDK accepts both forms, case-insensitively. This mapping is still a draft and will be completed from real API responses.
| ZRExpress state (examples) | Unified status |
|---|---|
commande_recue, OrderReceived | pending |
pret_a_expedier, ReadyToDispatch | created |
confirme_au_bureau, OutForDelivery, other hub / transfer states | in_transit |
Delivered, livre, encaisse | delivered |
Returned, retour* | returned |
Cancelled, annule, supprimee | cancelled |
| Any other state | unknown |
What each unified status means: full list of unified statuses.
Good to know about the ZRExpress API
- Amount ≤ 150,000 DZD, delivery fee included (
price). - Phone numbers are sent in international format (
+213…): pass them as you have them, the SDK normalizes them. - Unless its state is changed in the portal, the parcel stays « Commande reçue » (order received,
pending) until it is marked ready to dispatch. - Stop desks = ZR hubs that act as pickup points; filtering by wilaya only gives the best results.
- Products:
stockTypedefaults tonone(no catalog).local/warehouserequireproductId+sku. - In production, prefer webhooks to follow status changes;
listParcelsis the pull alternative.
Detailed native mapping (endpoints, fields, statuses): PROVIDERS.md.
ZRExpress FAQ
Where do I find the ZRExpress API key and tenant?
In the ZRExpress portal: the API key goes into apiKey (header X-Api-Key) and the tenant ID into tenant (header X-Tenant).
How do I print ZR Express labels?
generateLabels([...trackings]) returns the URL of a combined PDF, or one PDF per parcel with { mode: 'individual' }.
How do I receive ZRExpress status changes?
Set up a webhook in the portal, then verify the signature with verifyZRExpressWebhook (TS) or Webhook::verify (PHP) on the raw request body.
Can I cancel a parcel with its tracking number?
Yes: cancelParcel accepts the tracking number or the ZR UUID.
Other carriers
Same interface, same unified statuses (pending, in_transit, delivered…).