Search by

Unified PHP SDK for Algerian delivery carriers (Yalidine, ZRExpress, Maystro, Ecotrack/DHD, GuepEx) — SDK livraison Algérie: parcels, tracking, fees, wilayas. Laravel-friendly.

v0.1.8 2026-10-24 08:17 UTC

This package is auto-updated.

Last update: 2026-10-24 08:22:18 UTC


README

Packagist CI License: MIT

Unified PHP client for Algerian delivery carriers — Yalidine, GuepEx, ZRExpress, Maystro, Ecotrack (DHD, Anderson, World Express, …).

🇩🇿 SDK PHP livraison Algérie : créer, suivre et tarifer vos colis avec le même code pour chaque transporteur. Docs : https://developing-off.github.io/dz-delivery/ · TypeScript / Node.js : @dz-delivery/core

Implements the shared contract in docs/CONTRACT.md; provider mappings are in docs/PROVIDERS.md.

Provider Key Status
ZRExpress zrexpress contrat unifié (getStopDesks, listParcels, …) + extras (bulk, labels, webhooks)
Yalidine yalidine contrat unifié, checked against the developer docs
Maystro Delivery maystro createParcel, getFees, getWilayas, getCommunes, getStopDesks — track / cancelParcel throw NOT_SUPPORTED (webhooks only)
Ecotrack (DHD, Anderson, World Express, …) ecotrack contrat unifié — one provider for every courier on the Ecotrack platform (courier or baseUrl)

Requires PHP ≥ 8.2 and Guzzle 7.

Install

composer require dz-delivery/sdk

Full guide (configuration, every method with input/output tables, errors, Laravel, Maystro webhooks, testing): docs/guide-php.md.

Usage

use DzDelivery\ClientFactory;
use DzDelivery\Errors\ShippingError;

$client = ClientFactory::create('zrexpress', [
    'apiKey' => getenv('ZREXPRESS_API_KEY'),
    'tenant' => getenv('ZREXPRESS_TENANT'),
    // 'baseUrl' => ..., 'timeoutMs' => 15000, 'includeRaw' => true,
]);
// Extras ZR (bulk / labels): new \DzDelivery\Providers\ZRExpressProvider([...])
// Webhooks: \DzDelivery\Providers\ZRExpress\Webhook::verify($rawBody, $headers, $secret)
// → docs/ZREXPRESS.md

// Yalidine also needs the origin wilaya (for createParcel and getFees):
// ClientFactory::create('yalidine', ['apiId' => ..., 'apiToken' => ..., 'fromWilayaId' => 16]);
// GuepEx (same API): add 'baseUrl' => YalidineProvider::GUEPEX_DEFAULT_BASE_URL

// Ecotrack couriers (DHD, Conexlog, ...): token + 'courier' key or the tenant 'baseUrl':
// ClientFactory::create('ecotrack', ['token' => getenv('ECOTRACK_TOKEN'), 'courier' => 'dhd']);

// Maystro only needs a store token (sent as `Authorization: Token <apiToken>`):
// ClientFactory::create('maystro', ['apiToken' => getenv('MAYSTRO_API_TOKEN')]);
// It has no tracking API: map webhook statuses with StatusMapper::maystro($payload['status']).

try {
    $parcel = $client->createParcel([
        'recipientName' => 'Client Test',
        'phone' => '0550123456',          // normalized per carrier (+213… for ZRExpress)
        'address' => '12 rue des Tests',
        'wilayaId' => 16,                  // provider id or official code 1–58, or 'wilayaName'
        'communeName' => 'Bab Ezzouar',    // or 'communeId'
        'productName' => 'T-shirt',        // or 'productList' => [['name' => ..., 'quantity' => 1]]
        'price' => 2500,                   // amount collected from the recipient (DZD)
        'orderId' => 'ORD-1001',
    ]);

    $result = $client->track($parcel->trackingNumber);
    echo $result->parcel->status;          // pending | created | in_transit | delivered | returned | cancelled | unknown
    foreach ($result->events as $event) {
        echo "$event->at $event->status $event->label\n";
    }

    // weight in kg; length/width/height in cm (Yalidine bills the volumetric weight when higher)
    $quote = $client->getFees(['toWilayaId' => 16, 'isStopDesk' => false, 'weight' => 2]);
    $wilayas = $client->getWilayas();
    $communes = $client->getCommunes(16);

    // Stop desks / pickup points: use $desk->id as 'stopDeskId' with 'isStopDesk' => true.
    // ZRExpress: prefer wilaya-only (commune filter often empty). Maystro: communeId required.
    $desks = $client->getStopDesks(['wilayaId' => 16]);

    // Account parcels + status (ZRExpress). No event history — use track() for that.
    $page = $client->listParcels(['page' => 1, 'pageSize' => 50]);

    // Connectivity / credentials probe (always hits the network)
    $health = $client->healthCheck(); // { ok: true, provider, latencyMs }

    $client->cancelParcel($parcel->trackingNumber);
} catch (ShippingError $e) {
    // $e->code: AUTH_FAILED | VALIDATION_ERROR | NOT_FOUND | RATE_LIMITED | NOT_SUPPORTED
    //           | PROVIDER_ERROR | NETWORK_ERROR | UNKNOWN
    echo "$e->code: {$e->getMessage()} (HTTP $e->httpStatus)";
    // RATE_LIMITED: $e->retryAfter = seconds to wait (from the Retry-After header), when sent.
}

Inputs accept either arrays or the model objects (ParcelInput, FeeQuery, StopDeskQuery). All returned models expose public readonly camelCase properties and an optional raw native payload (disable with 'includeRaw' => false).

A runnable example lives in examples/php/quickstart.php.

Development

composer install
composer test

Tests use Guzzle's MockHandler (no network) and the shared JSON fixtures in docs/fixtures/, so they must be run from the monorepo. Pass a Guzzle handler through the handler option to mock HTTP in your own tests:

$stack = GuzzleHttp\HandlerStack::create(new GuzzleHttp\Handler\MockHandler([...]));
$client = ClientFactory::create('zrexpress', ['apiKey' => 'k', 'tenant' => 't', 'handler' => $stack]);

Layout

src/
  ClientFactory.php
  Contracts/ShippingProviderInterface.php
  Models/        ParcelInput, ProductItem, Parcel, ParcelStatus, TrackingEvent, TrackResult,
                 FeeQuery, FeeQuote, Location, StopDeskQuery, StopDesk, ClientOptions
  Errors/ShippingError.php
  Http/HttpClient.php
  Providers/     ZRExpressProvider, YalidineProvider, MaystroProvider
  Support/       StatusMapper, Phone, Text, Dates
tests/