dz-delivery

dz-deliveryAlgeria delivery SDK

One SDK to create, track and price parcels with Yalidine, ZRExpress, Maystro, Ecotrack (DHD…), NoEST, E-com, MDM and Elogistia — TypeScript and PHP, same contract.

Getting started

Install

Public packages · npm @dz-delivery/core · Composer dz-delivery/sdk.

TypeScript · Node ≥ 18
npm install @dz-delivery/core
PHP ≥ 8.2 · Guzzle 7
composer require dz-delivery/sdk

Zero runtime dependencies on the TypeScript side (native fetch). PHP: Guzzle only. Reference: models, unified statuses and error codes.

First parcel

Quickstart

Create a ZRExpress parcel in a few lines.

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);

Configuration

Clients & credentials

One client per carrier. Shared options: baseUrl, timeoutMs (15s), includeRaw (true).

KeyConfigNotes
zrexpress apiKey, tenant X-Api-Key + X-Tenant. Extras: createZRExpress
yalidine apiId, apiToken, fromWilayaId GuepEx: same provider + GuepEx baseUrl
maystro apiToken Authorization: Token …. Webhooks: receive them yourself, mapMaystroStatus converts the status code
ecotrack token + courier or baseUrl DHD, Anderson, World Express, 48Hr, EcoRapid, Conexlog…
import { createClient, createZRExpress, GUEPEX_DEFAULT_BASE_URL } from '@dz-delivery/core';

const zr = createClient('zrexpress', { apiKey, tenant });
const zrExtra = createZRExpress({ apiKey, tenant }); // typed bulk / labels

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' });

ShippingProvider

Unified API

The same interface for every carrier. Not a 1:1 wrapper — just the operations a store actually needs.

  1. 01CreatecreateParcel
  2. 02Tracktrack
  3. 03PricegetFees
  4. 04LocationsgetWilayas · getCommunes · getStopDesks
  5. 05List (account)listParcels
  6. 06CancelcancelParcel
  7. 07HealthhealthCheck

createParcel

Validated client-side before any HTTP call. Accepts wilaya codes 1–58, carrier ids or names. price = COD amount in DZD.

const parcel = await shipping.createParcel({
  recipientName: 'Client',
  phone: '0550123456',       // normalized per carrier
  phone2: '0770000000',
  address: 'Cité 221 lots',
  wilayaName: 'Alger',        // or wilayaId: 16
  communeName: 'Bab Ezzouar',
  productName: 'T-shirt',     // or 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? }

track

Returns the current parcel plus its event history (oldest → newest). Maystro: pass parcel.id (Maystro UUID) or the tracking number, not your orderId (webhooks optional).

const { parcel, events } = await shipping.track(trackingNumber);
// events: [{ status, label, at, raw? }, …]

getFees

One quote for a destination + delivery mode. Yalidine uses the client’s fromWilayaId.

const quote = await shipping.getFees({
  toWilayaId: 16,
  toCommuneId: '1630', // useful for Maystro / Yalidine
  isStopDesk: false,
  weight: 2,
});
// { deliveryFee, stopDeskFee?, returnFee?, currency: 'DZD' }

getWilayas · getCommunes · getStopDesks

Locations are cached for the lifetime of the client. Stop desks: pass desk.id as stopDeskId.

const wilayas = await shipping.getWilayas();
const communes = await shipping.getCommunes(16);

// ZRExpress: prefer wilaya only (the commune filter often returns nothing)
const desks = await shipping.getStopDesks({ wilayaId: 16 });
// Maystro: communeId is required

await shipping.createParcel({
  /* … */
  isStopDesk: true,
  stopDeskId: desks[0].id,
});

listParcels

Paginated list of the account’s parcels with their unified status. No event history — use track() for that. Available on ZRExpress, Yalidine, Maystro, E-com and MDM; Ecotrack (not implemented yet), NoEST and Elogistia throw NOT_SUPPORTED (see the compatibility matrix). Prefer webhooks in production.

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 });

cancelParcel · healthCheck

cancelParcel accepts a tracking number or an internal id. healthCheck probes the credentials (wilayas, bypassing the cache) → { ok, provider, latencyMs }.

await shipping.cancelParcel(parcel.trackingNumber);
const health = await shipping.healthCheck();

Contract

Core models

camelCase fields in both TS and PHP. Amounts in DZD, weights in kg, dates in ISO 8601.

ParcelInput (creation)

FieldRequiredDescription
recipientName, phone, address, priceyesRecipient + COD
wilayaId or wilayaNameone of the twoCode 1–58, id or name
communeId or communeNameone of the twoId or name within the wilaya
productName or productListone of the twoProduct description(s)
isStopDesk + stopDeskIdfor stop desk deliverystopDeskId = id from getStopDesks
orderId, remark, weight, dimensionsnoMerchant reference, note, kg/cm
stockTypenoZRExpress: none (default) / local / warehouse

Parcel · TrackingEvent

ModelFields
Parcelid, trackingNumber, status, provider, raw?
TrackingEventstatus, label, at, raw?
FeeQuotedeliveryFee, stopDeskFee?, returnFee?, currency: 'DZD'
Locationid, name, parentId?, code?

Normalization

Unified statuses

An unknown native status maps to unknown (never an error). The native label is kept in label / raw.

pending created in_transit delivered returned cancelled unknown

Exported mappers: mapZRExpressStatus, mapYalidineStatus, mapMaystroStatus, mapEcotrackStatus (PHP: StatusMapper).

ShippingError

Errors

Every failure throws a ShippingError. Don’t catch it without reading code.

CodeWhen
VALIDATION_ERRORInvalid input (usually before HTTP) or 400/422
AUTH_FAILED401 / 403
NOT_FOUND404, unknown wilaya/commune, parcel not found
RATE_LIMITED429 — see retryAfter
NOT_SUPPORTEDThe carrier doesn’t offer this operation
PROVIDER_ERROR5xx / unexpected response
NETWORK_ERRORTimeout, DNS, abort
UNKNOWNAnything else
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;
}

Compatibility

What each carrier exposes

NOT_SUPPORTED = the carrier’s API doesn’t have the operation (not an SDK bug).

Method ZRExpress Yalidine Maystro Ecotrack NoEST E-com MDM Elogistia
createParcel yesyesyesyesyesyescatalogyes
track yesyesyesyesyesyesyesyes
getFees yesyesyesyesyesyesper communeyes
getWilayas / getCommunes yesyesyesyesyesyesyesyes
getStopDesks hubscenterscommuneIdyesstationsofficesofficesno
listParcels yesyesyessoonnoyesyesno
cancelParcel yesif “En préparation”yesif not validatedif not validatedif preparingnono
healthCheck yesyesyesyesyesyesyesyes
bulk / labels / webhook verification extras———————

ZR-only · 0.1.7+

ZRExpress extras

Outside the unified contract: methods on ZRExpressProvider plus webhook helpers. createClient('zrexpress') remains the simple interface.

Full matrix and backlog: ZR Express page (backlog: ZREXPRESS.md).

Webhooks (receive)

Svix / Standard Webhooks — the raw body is required (before JSON.parse). Headers svix-id / svix-timestamp / svix-signature (aliases webhook-*). Tolerance window ~5 min.

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);
});

Labels

HTML through a SAS URL. Auth: X-Api-Key + X-Tenant (same as the rest of the SDK).

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

Bulk create / delete

Create: max 100, orderId required on each item. Delete: max 200, UUID or tracking number (auto-detected). 200 / 207 / 400 responses → a structured result, no exception when the body is a bulk report.

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
]);
AreaSDKNotes
create / track / fees / locations / list / cancel / healthcontractShippingProvider
Webhooks (receive)helpersverify + parse
LabelsgenerateLabelsmultiple / individual
Bulk create / deletecreateParcelsBulk / deleteParcelsBulkorderId required on create
Update / pickup bags / catalog / claimsbacklognext batch

Quirks

Carrier notes

ZRExpress

Amount ≤ 150,000 DZD (fees included). Phone numbers in international format (+213…). Stop desks are hubs; filter by wilaya only. Without a stateId, the parcel stays in OrderReceived until it is switched to “prêt à expédier” (ready to ship) in the portal.

Yalidine / GuepEx

fromWilayaId (or fromWilayaName), the origin wilaya, goes in the client config: createParcel and getFees need it. Cancellation only while the parcel is “En préparation” (preparing). GuepEx = same API, different baseUrl.

Maystro

track / cancelParcel / listParcels and PDF labels (getLabels); for webhooks (an endpoint you write yourself), mapMaystroStatus converts the pushed status code. getStopDesks requires communeId.

Ecotrack

One provider, many couriers via courier (dhd, anderson, worldexpress…) or the tenant’s HTTPS baseUrl. The autoValidate option validates the order right after creation.

NoEST

apiToken + userGuid. Orders are not validated by default (autoValidate). No listParcels. Commune names are matched loosely (“Bab Ezzouar” / “Bab Azzouar”).

E-com Delivery

apiKey + apiToken (API v2). Full contract, including listParcels with updatedSince. Name ≤ 60 characters, address ≤ 100.

MDM Express

apiKey + storeId. Products are stocked at MDM (catalog id PRD-…). listParcels filters status and dates server-side (orderId page by page in the SDK); no cancellation through the API.

Elogistia

apiKey (in the URL). Create, track, fees, wilayas, communes. No stop desk list, parcel list or cancellation.

Detailed native mapping: PROVIDERS.md. PHP guide (Laravel, Maystro webhooks): integrating delivery in PHP.

Frequently asked questions

FAQ

How do I use the Yalidine API from Node.js or PHP?

Install @dz-delivery/core (npm) or dz-delivery/sdk (Composer), then create a yalidine client with your apiId, apiToken and origin wilaya. Yalidine guide.

How do I integrate ZR Express into my store?

A zrexpress client with your apiKey and tenant is all you need to create, track, price, list and cancel parcels — plus labels, bulk operations and signed webhooks. ZRExpress guide.

Are DHD, Anderson, World Express… supported?

Yes: they are carriers on the Ecotrack platform. A single ecotrack provider covers them all, using a built-in courier key or your tenant’s URL. Ecotrack guide.

Can I switch carriers without rewriting my code?

Yes. Every carrier returns the same objects and the same normalized statuses (pending, in_transit, delivered…). Only the client configuration changes, except for operations a carrier’s API does not offer: those throw NOT_SUPPORTED (see the compatibility matrix).

Does it work with Laravel, WooCommerce or Shopify?

The PHP SDK installs in any Composer project (see the Laravel example in the PHP guide). For a Node.js backend or a Shopify app, use the npm package.

Is it free?

Yes, the SDK is open source under the MIT license. You use your own API credentials with each carrier.