Démarrage
Installer
Packages publics · npm @dz-delivery/core · Composer dz-delivery/sdk.
TypeScript · Node ≥ 18
Copier
npm install @dz-delivery/core
PHP ≥ 8.2 · Guzzle 7
Copier
composer require dz-delivery/sdk
Zéro dépendance runtime côté TypeScript (fetch natif). PHP : Guzzle uniquement.
Source de vérité : CONTRACT.md .
Premier colis
Quickstart
Créer un colis ZRExpress en quelques lignes.
TypeScript
PHP
import { createClient } from '@dz-delivery/core';
const shipping = createClient ('zrexpress' , {
apiKey: process.env.ZREXPRESS_API_KEY!,
tenant: process.env.ZREXPRESS_TENANT!,
});
const parcel = await shipping.createParcel ({
recipientName: 'Amine B.' ,
phone: '0550123456' ,
address: '12 rue Didouche Mourad' ,
wilayaId: 16 ,
communeName: 'Alger Centre' ,
productName: 'T-shirt' ,
price: 2500 ,
});
const { parcel: current, events } = await shipping.track (parcel.trackingNumber);
use DzDelivery\ClientFactory;
$shipping = ClientFactory::create ('zrexpress' , [
'apiKey' => getenv('ZREXPRESS_API_KEY' ),
'tenant' => getenv('ZREXPRESS_TENANT' ),
]);
$parcel = $shipping->createParcel ([
'recipientName' => 'Amine B.' ,
'phone' => '0550123456' ,
'address' => '12 rue Didouche Mourad' ,
'wilayaId' => 16 ,
'communeName' => 'Alger Centre' ,
'productName' => 'T-shirt' ,
'price' => 2500 ,
]);
$result = $shipping->track ($parcel->trackingNumber);
Configuration
Clients & credentials
Un client par transporteur. Options communes : baseUrl , timeoutMs (15s), includeRaw (true).
Clé Config Notes
zrexpress
apiKey, tenant
X-Api-Key + X-Tenant. Extras : createZRExpress
yalidine
apiId, apiToken, fromWilayaId
GuepEx : même provider + baseUrl GuepEx
maystro
apiToken
Authorization: Token … — pas de track/cancel
ecotrack
token + courier ou baseUrl
DHD, Anderson, World Express, 48Hr, EcoRapid, Conexlog…
TypeScript
PHP
import { createClient, createZRExpress, GUEPEX_DEFAULT_BASE_URL } from '@dz-delivery/core';
const zr = createClient ('zrexpress' , { apiKey, tenant });
const zrExtra = createZRExpress ({ apiKey, tenant }); // bulk / labels typés
const yalidine = createClient ('yalidine' , {
apiId, apiToken, fromWilayaId: 16 ,
});
const guepex = createClient ('yalidine' , {
apiId, apiToken, fromWilayaId: 16 , baseUrl: GUEPEX_DEFAULT_BASE_URL,
});
const maystro = createClient ('maystro' , { apiToken });
const dhd = createClient ('ecotrack' , { token, courier: 'dhd' });
use DzDelivery\ClientFactory;
use DzDelivery\Providers\ZRExpressProvider;
use DzDelivery\Providers\YalidineProvider;
$zr = ClientFactory::create ('zrexpress' , ['apiKey' => $key, 'tenant' => $tenant]);
$zrExtra = new ZRExpressProvider(['apiKey' => $key, 'tenant' => $tenant]);
$yalidine = ClientFactory::create ('yalidine' , [
'apiId' => $id, 'apiToken' => $token, 'fromWilayaId' => 16 ,
]);
$guepex = ClientFactory::create ('yalidine' , [
'apiId' => $id, 'apiToken' => $token, 'fromWilayaId' => 16 ,
'baseUrl' => YalidineProvider::GUEPEX_DEFAULT_BASE_URL,
]);
ShippingProvider
API unifiée
Même interface pour tous les transporteurs. Pas un wrapper 1:1 — les opérations dont une boutique a besoin.
01 Créer createParcel
02 Suivre track
03 Tarifer getFees
04 Lieux getWilayas · getCommunes · getStopDesks
05 Lister (compte) listParcels
06 Annuler cancelParcel
07 Santé healthCheck
createParcel
Valide côté client avant HTTP. Accepte codes wilaya 1–58 , ids transporteur, ou noms. price = montant COD en DZD.
TypeScript
PHP
const parcel = await shipping.createParcel ({
recipientName: 'Client' ,
phone: '0550123456' , // normalisé par carrier
phone2: '0770000000' ,
address: 'Cité 221 lots' ,
wilayaName: 'Alger' , // ou wilayaId: 16
communeName: 'Bab Ezzouar' ,
productName: 'T-shirt' , // ou productList: [{ name, quantity }]
price: 2500 ,
weight: 0.5 ,
orderId: 'ORD-1001' ,
remark: 'fragile' ,
isStopDesk: false ,
// stockType: 'none' | 'local' | 'warehouse' (ZRExpress)
});
// → { id, trackingNumber, status, provider, raw? }
$parcel = $shipping->createParcel ([
'recipientName' => 'Client' ,
'phone' => '0550123456' ,
'address' => 'Cité 221 lots' ,
'wilayaId' => 16 ,
'communeName' => 'Bab Ezzouar' ,
'productName' => 'T-shirt' ,
'price' => 2500 ,
'orderId' => 'ORD-1001' ,
]);
// $parcel->id · trackingNumber · status · provider
track
Retourne le colis courant + historique d’événements (plus ancien → plus récent). Maystro : parcel.id (UUID Maystro) ou numéro de suivi, pas votre orderId (webhooks en option).
TypeScript
PHP
const { parcel, events } = await shipping.track (trackingNumber);
// events: [{ status, label, at, raw? }, …]
$result = $shipping->track ($trackingNumber);
foreach ($result->events as $event) {
echo "$event->at $event->status $event->label\n" ;
}
getFees
Une cotation pour destination + mode. Yalidine utilise fromWilayaId du client.
TypeScript
PHP
const quote = await shipping.getFees ({
toWilayaId: 16 ,
toCommuneId: '1630' , // utile Maystro / Yalidine
isStopDesk: false ,
weight: 2 ,
});
// { deliveryFee, stopDeskFee?, returnFee?, currency: 'DZD' }
$quote = $shipping->getFees ([
'toWilayaId' => 16 ,
'toCommuneId' => 1630 ,
'isStopDesk' => false ,
'weight' => 2 ,
]);
getWilayas · getCommunes · getStopDesks
Lieux mis en cache sur la durée de vie du client. Stop desks : passer desk.id comme stopDeskId .
TypeScript
PHP
const wilayas = await shipping.getWilayas ();
const communes = await shipping.getCommunes (16 );
// ZRExpress : préférer wilaya seule (filtre commune souvent vide)
const desks = await shipping.getStopDesks ({ wilayaId: 16 });
// Maystro : communeId obligatoire
await shipping.createParcel ({
/* … */
isStopDesk: true ,
stopDeskId: desks[0 ].id,
});
$wilayas = $shipping->getWilayas ();
$communes = $shipping->getCommunes (16 );
$desks = $shipping->getStopDesks (['wilayaId' => 16 ]);
listParcels
Liste paginée des colis du compte + statut unifié. Pas d’historique d’événements — utiliser track() . ZRExpress via parcels/search ; autres : NOT_SUPPORTED pour l’instant. Préférer les webhooks en production.
TypeScript
PHP
const page = await zr.listParcels ({ page: 1 , pageSize: 50 });
// page.parcels · page.hasNext · page.totalCount
await zr.listParcels ({ orderId: 'ORD-1001' });
await zr.listParcels ({ keyword: '0550' , status: 'pending' });
await zr.listParcels ({ updatedSince: '2026-10-01T00:00:00+00:00' , allPages: true });
$page = $zr->listParcels (['page' => 1 , 'pageSize' => 50 ]);
$zr->listParcels (['orderId' => 'ORD-1001' ]);
cancelParcel · healthCheck
cancelParcel accepte tracking ou id interne. healthCheck sonde les credentials (wilayas, hors cache) → { ok, provider, latencyMs } .
TypeScript
PHP
await shipping.cancelParcel (parcel.trackingNumber);
const health = await shipping.healthCheck ();
$shipping->cancelParcel ($parcel->trackingNumber);
$health = $shipping->healthCheck ();
Contrat
Modèles principaux
Champs camelCase en TS et PHP. Montants en DZD, poids en kg, dates ISO 8601.
ParcelInput (création)
Champ Requis Description
recipientName, phone, address, priceoui Identité + COD
wilayaId ou wilayaNameun des deux Code 1–58, id, ou nom
communeId ou communeNameun des deux Id ou nom dans la wilaya
productName ou productListun des deux Description produit(s)
isStopDesk + stopDeskIdsi relais stopDeskId = id de getStopDesks
orderId, remark, weight, dimsnon Réf. marchand, note, kg/cm
stockTypenon ZRExpress : none (défaut) / local / warehouse
Parcel · TrackingEvent
Modèle Champs
Parcelid, trackingNumber, status, provider, raw?
TrackingEventstatus, label, at, raw?
FeeQuotedeliveryFee, stopDeskFee?, returnFee?, currency: 'DZD'
Locationid, name, parentId?, code?
Normalisation
Statuts unifiés
Un statut natif inconnu → unknown (jamais une erreur). Le libellé natif reste dans label / raw .
pending
created
in_transit
delivered
returned
cancelled
unknown
Mappers exportés : mapZRExpressStatus , mapYalidineStatus , mapMaystroStatus , mapEcotrackStatus (PHP : StatusMapper ).
ShippingError
Erreurs
Toute échec lève ShippingError . Ne catch pas sans lire code .
Code Quand
VALIDATION_ERRORInput invalide (souvent avant HTTP) ou 400/422
AUTH_FAILED401 / 403
NOT_FOUND404, wilaya/commune inconnue, colis absent
RATE_LIMITED429 — voir retryAfter
NOT_SUPPORTEDOpération absente chez le carrier
PROVIDER_ERROR5xx / réponse inattendue
NETWORK_ERRORTimeout, DNS, abort
UNKNOWNAutres
TypeScript
PHP
import { isShippingError } from '@dz-delivery/core';
try {
await shipping.createParcel (input);
} catch (err) {
if (isShippingError (err)) {
console.error(err.code, err.message, err.httpStatus, err.details);
}
throw err;
}
use DzDelivery\Errors\ShippingError;
try {
$shipping->createParcel ($input);
} catch (ShippingError $e) {
echo $e->code . ': ' . $e->getMessage ();
}
Compatibilité
Ce que chaque transporteur expose
NOT_SUPPORTED = l’API carrier n’a pas l’opération (pas un bug SDK).
Méthode
ZRExpress
Yalidine
Maystro
Ecotrack
NoEST
E-com
MDM
Elogistia
createParcel
oui oui oui oui oui oui catalogue oui
track
oui oui webhooks oui oui oui oui oui
getFees
oui oui oui oui oui oui par commune oui
getWilayas / getCommunes
oui oui oui oui oui oui oui oui
getStopDesks
hubs centers communeId oui stations bureaux bureaux non
listParcels
oui bientôt webhooks bientôt non oui oui non
cancelParcel
oui si En prép. non si non validé si non validé si en prép. non non
healthCheck
oui oui oui oui oui oui oui oui
bulk / labels / webhooks verify
— — — — — — —
ZR-only · 0.1.7+
ZRExpress extras
Hors contrat unifié : méthodes sur ZRExpressProvider + helpers webhooks. createClient('zrexpress') reste l’interface simple.
Matrice complète et backlog :
docs/ZREXPRESS.md .
Webhooks (receive)
Svix / Standard Webhooks — corps brut obligatoire (avant JSON.parse). Headers svix-id / svix-timestamp / svix-signature (aliases webhook-* ). Fenêtre ~5 min.
TypeScript
PHP
import { verifyZRExpressWebhook } from '@dz-delivery/core';
app.post('/webhooks/zrexpress' , express.raw({ type: '*/*' }), (req, res) => {
const event = verifyZRExpressWebhook (
req.body, req.headers, process.env.ZREXPRESS_WEBHOOK_SECRET!,
);
// event.type · parcelId · trackingNumber · status · raw
res.sendStatus(200 );
});
use DzDelivery\Providers\ZRExpress\Webhook;
$raw = file_get_contents('php://input' ); // pas Request::json()
$event = Webhook::verify ($raw, getallheaders(), getenv('ZREXPRESS_WEBHOOK_SECRET' ));
Labels
HTML via URL SAS. Auth : X-Api-Key + X-Tenant (comme le reste du SDK).
TypeScript
PHP
const multi = await zr.generateLabels (['16-AAA111-ZR' , '16-BBB222-ZR' ]);
// { fileUrl, failedTrackingNumbers } — max 250
const one = await zr.generateLabels (['16-AAA111-ZR' ], { mode: 'individual' });
// { labels: [{ trackingNumber, fileUrl }], failedTrackingNumbers } — max 100
$multi = $zr->generateLabels (['16-AAA111-ZR' ]);
$one = $zr->generateLabels (['16-AAA111-ZR' ], ['mode' => 'individual' ]);
Bulk create / delete
Création : max 100, orderId obligatoire par item. Suppression : max 200, UUID ou tracking (détection auto). Réponses 200 / 207 / 400 → résultat structuré, pas d’exception si le corps est un rapport bulk.
TypeScript
PHP
const created = await zr.createParcelsBulk ([
{ ...input, orderId: 'ORD-1' },
{ ...input, orderId: 'ORD-2' },
]);
// { successes: Parcel[], failures: [{ index, orderId?, error }] }
const deleted = await zr.deleteParcelsBulk ([
'8c1a4c53-0000-4000-8000-000000000001' , // UUID
'16-AAA111-ZR' , // tracking
]);
$created = $zr->createParcelsBulk ([
array_merge($input, ['orderId' => 'ORD-1' ]),
]);
$deleted = $zr->deleteParcelsBulk ([$uuid, '16-AAA111-ZR' ]);
Domaine SDK Notes
create / track / fees / lieux / list / cancel / health contrat ShippingProvider
Webhooks receive verify + parse
Labels multiple / individual
Bulk create / delete orderId requis en create
Update / pickup bags / catalog / claims backlog prochain lot
Particularités
Notes par transporteur
Montant ≤ 150 000 DZD (frais inclus). Téléphones en international (+213… ). Stop desks = hubs ; préfère wilaya seule. Sans stateId , le colis reste en OrderReceived jusqu’à passage « prêt à expédier » côté portail.
fromWilayaId obligatoire à la construction. Annulation seulement en « En préparation ». GuepEx = même API, autre baseUrl .
track / cancelParcel / listParcels et bordereaux PDF (getLabels ) ; webhooks en option avec mapMaystroStatus . getStopDesks exige communeId .
Un provider, plusieurs coursiers via courier (dhd , anderson , worldexpress …) ou baseUrl HTTPS du tenant. Option autoValidate pour valider juste après création.
apiToken + userGuid . Commandes non validées par défaut (autoValidate ). Pas de listParcels . Noms de communes tolérants (« Bab Ezzouar » / « Bab Azzouar »).
apiKey + apiToken (API v2). Contrat complet, y compris listParcels avec updatedSince . Nom ≤ 60 caractères, adresse ≤ 100.
apiKey + storeId . Produits stockés chez MDM (id catalogue PRD-… ). listParcels filtré côté serveur ; pas d’annulation par API.
apiKey (dans l’URL). Création, suivi, tarifs, wilayas, communes. Pas de liste de bureaux, de liste de colis ni d’annulation.
Mapping natif détaillé :
PROVIDERS.md .
Guide PHP (Laravel, Maystro webhooks) :
guide-php.md .
Questions fréquentes
FAQ
Comment utiliser l’API Yalidine en Node.js ou en PHP ?
Installez @dz-delivery/core (npm) ou dz-delivery/sdk (Composer), puis créez un client yalidine avec apiId , apiToken et votre wilaya d’origine. Guide Yalidine .
Comment intégrer ZR Express dans ma boutique ?
Un client zrexpress avec votre apiKey et votre tenant suffit pour créer, suivre, tarifer, lister et annuler des colis — plus les étiquettes, le bulk et les webhooks signés. Guide ZRExpress .
DHD, Anderson, World Express… sont-ils supportés ?
Oui : ce sont des transporteurs de la plateforme Ecotrack. Un seul provider ecotrack les couvre, avec une clé courier intégrée ou l’URL de votre tenant. Guide Ecotrack .
Puis-je changer de transporteur sans réécrire mon code ?
Oui. Tous les transporteurs renvoient les mêmes objets et les mêmes statuts normalisés (pending , in_transit , delivered …). Seule la configuration du client change.
Ça marche avec Laravel, WooCommerce ou Shopify ?
Le SDK PHP s’installe dans n’importe quel projet Composer (exemple Laravel dans le guide PHP ). Pour un backend Node.js ou une app Shopify, utilisez le package npm.
Est-ce gratuit ?
Oui, le SDK est open source sous licence MIT. Vous utilisez vos propres identifiants API chez chaque transporteur.
Aller plus loin
Références
Sources dans le dépôt et packages publiés.