Parcel statuses, models and error codes
Every Algerian carrier has its own statuses: French labels at Yalidine, numeric codes at Maystro, their own states at ZRExpress, Ecotrack or NoEST. dz-delivery maps them to seven unified statuses, with the same models and error codes in TypeScript and PHP.
The 7 unified parcel statuses
Parcel.status and TrackingEvent.status always hold one of these values, whatever the carrier:
pending | created | in_transit | delivered | returned | cancelled | unknown
| Status | Meaning | Yalidine examples | Maystro examples (code) |
|---|---|---|---|
pending | Order registered but not yet confirmed or ready for pickup (draft, “order received”). | “En préparation” (preparing), “A vérifier” (to check) | 4 CREATED, 11 PENDING, 12 OUT_OF_STOCK |
created | Confirmed and ready to be picked up or dispatched by the carrier. | “Prêt à expédier” (ready to ship), “Pas encore ramassé” (not picked up yet), “Pas encore expédié”, “En passation” | 5 PICK_UP_REQUESTED, 6 IN_PROCESS |
in_transit | Picked up and moving: in hub, in transfer, out for delivery, failed attempt, etc. | “Ramassé” (picked up), “En transit”, “Vers Wilaya”, “Sorti en livraison” (out for delivery), “Tentative échouée” (failed attempt), “En alerte”… | 8 WAITING_TRANSIT, 9 IN_TRANSIT_TO_BE_SHIPPED, 15 READY_TO_SHIP, 22 ASSIGNED, 31 SHIPPED, 32 ALERTED, 42 POSTPONED |
delivered | Delivered to the recipient (COD collected). | “Livré” (delivered) | 41 DELIVERED |
returned | Returned or being returned to the sender. | “Retour …”, “Retourné …” (returned), “Echange échoué” (exchange failed), “Colis abandonné” (abandoned) | 10 IN_TRANSIT_TO_BE_RETURNED, 51 READY_TO_RETURN, 52 TAKEN_BY_STORE, 53 NOT_RECEIVED |
cancelled | Cancelled / deleted before delivery. | “Annulé” (cancelled), “Supprimé” (deleted) | 50 ABORTED |
unknown | Native status not mapped. The SDK never throws on an unknown native status: it returns unknown and keeps the native label in TrackingEvent.label / raw. | any other label | any other code |
The examples above are excerpts. Each carrier’s full native → unified table is in PROVIDERS.md; it is identical in TypeScript and PHP, enforced by shared fixtures (docs/fixtures/).
Mapping a native status (webhooks, imports)
The mappers are exported: use them when you receive a native status outside track(), for example in a Maystro webhook.
import { mapYalidineStatus, mapMaystroStatus, PARCEL_STATUSES } from '@dz-delivery/core'; mapYalidineStatus('Sorti en livraison'); // 'in_transit' mapMaystroStatus(41); // 'delivered' (code or name) PARCEL_STATUSES; // the 7 statuses, in order // Also: mapZRExpressStatus, mapEcotrackStatus, mapNoestStatus, mapEcomStatus, mapMdmStatus, mapElogistiaStatus
use DzDelivery\Support\StatusMapper; use DzDelivery\Models\ParcelStatus; StatusMapper::map('yalidine', 'Sorti en livraison'); // 'in_transit' StatusMapper::maystro(41); // 'delivered' (code or name) ParcelStatus::DELIVERED; // constants, ParcelStatus::ALL for the list
Error codes
Every failure is thrown as a ShippingError (fields in Models). Its code is one of:
code | When |
|---|---|
AUTH_FAILED | 401/403, missing or invalid credentials. |
VALIDATION_ERROR | 400/422, or SDK-side input validation failure (before any HTTP call). |
NOT_FOUND | 404, parcel / location not found, or name resolution (wilayaName, communeName) failed. |
RATE_LIMITED | 429. |
NOT_SUPPORTED | The carrier does not support this operation (e.g. cancellation). |
PROVIDER_ERROR | 5xx or an unexpected/unparseable carrier response. |
NETWORK_ERROR | Timeout, DNS, connection refused. |
UNKNOWN | Anything else. |
Default HTTP → code mapping: 400, 422 → VALIDATION_ERROR, 401, 403 → AUTH_FAILED, 404 → NOT_FOUND, 429 → RATE_LIMITED, 5xx → PROVIDER_ERROR, other 4xx → UNKNOWN.
Handling examples (retry on RATE_LIMITED, showing validation errors): PHP guide.
Conventions
| Topic | Rule |
|---|---|
| Naming | camelCase for fields and methods in both TS and PHP (PHP objects expose public readonly properties with the same names). |
| IDs | All IDs returned by the SDK are strings (carriers use UUIDs, integers or codes). Inputs accept string | int. |
| Money | Amounts are in DZD, as numbers (dinars, not centimes). currency is always "DZD". |
| Weight / size | Weight in kg, dimensions in cm. |
| Dates | ISO 8601 strings with timezone (e.g. 2025-12-23T15:23:54+00:00) in both TS and PHP. |
| Phones | Strings. Providers normalize to the carrier’s format (e.g. 0550123456 ↔ +213550123456). |
| Optional | ? = optional. Absent optional values are undefined (TS) / null (PHP). |
| Async | TS methods return Promise<T>. PHP methods are synchronous and return T. |
| Errors | All failures are thrown as ShippingError (see Error codes). Never null / false to signal an error. |
The raw field
Every returned model (Parcel, TrackingEvent, FeeQuote, Location, ShippingError) may carry an optional raw field with the unmodified native API response (or the relevant fragment).
rawis for power users and debugging. Its shape is not part of the contract and may change when the carrier changes its API.- Application code should not depend on
rawfor core logic. rawis populated by default; the client optionincludeRaw: falsedisables it.
Models
The same models, with the same field names, in TypeScript and PHP.
ParcelInput (input to createParcel)
| Field | Type | Required | Description |
|---|---|---|---|
recipientName | string | yes | Full name of the recipient. |
phone | string | yes | Primary phone number. |
phone2 | string | no | Secondary phone number. |
address | string | yes | Street address / free text. |
wilayaId | string | int | one of¹ | Provider Location.id or official wilaya code 1–58. |
wilayaName | string | one of¹ | Wilaya name (resolved by the provider, case- and accent-insensitive). |
communeId | string | int | one of² | Provider Location.id of the commune. |
communeName | string | one of² | Commune name (resolved within the wilaya). |
productName | string | one of³ | Single free-text product description. |
productList | ProductItem[] | one of³ | Detailed list of products. |
price | number | yes | Amount to collect from the recipient (COD), in DZD. 0 for prepaid. |
weight | number | no | Total weight in kg. |
length, width, height | number | no | Package dimensions in cm. |
isStopDesk | boolean | no | true = delivery to a stop desk / pickup point. Default false (home delivery). |
stopDeskId | string | if isStopDesk | Provider ID of the stop desk / hub. |
remark | string | no | Note for the delivery agent. |
orderId | string | no | Your own order reference (stored as external ID when supported). |
stockType | "none" | "local" | "warehouse" | no | Default stock mode for products that omit ProductItem.stockType. Used by ZRExpress, ignored by the other carriers. Default "none" (no catalog). |
¹ Exactly one of wilayaId / wilayaName. ² Exactly one of communeId / communeName. ³ Exactly one of productName / productList: providers that need a list convert productName to [{ name, quantity: 1, unitPrice: price }]; providers that need a string join the productList names.
Whether price includes the delivery fee depends on the carrier (documented in PROVIDERS.md).
ProductItem
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Product name. |
quantity | int | yes | Quantity (≥ 1). |
unitPrice | number | no | Unit price in DZD. |
sku | string | no | Merchant SKU. |
productId | string | no | Carrier catalog product id (ZRExpress when stockType is local / warehouse). |
stockType | "none" | "local" | "warehouse" | no | Per-item stock mode; overrides ParcelInput.stockType. |
Parcel
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Carrier’s internal parcel ID (may equal trackingNumber for some carriers). |
trackingNumber | string | yes | Tracking number shown to customers. |
status | ParcelStatus | yes | Unified status (see Statuses). |
provider | string | yes | Provider key, e.g. "zrexpress", "yalidine". |
labelUrl | string | no | Shipping label (bordereau) URL when the carrier returns one with the parcel (Yalidine). Carriers whose labels need a separate call (ZRExpress, Maystro PDF) expose them as a provider extra instead. |
raw | unknown / mixed | no | Native response. |
TrackingEvent and TrackResult
| Field | Type | Required | Description |
|---|---|---|---|
status | ParcelStatus | yes | Unified status after this event. |
label | string | yes | Human-readable native label (e.g. "Prêt à expédier", ready to ship). |
at | string | yes | ISO 8601 timestamp of the event. |
raw | unknown / mixed | no | Native event. |
track() returns a TrackResult = { parcel, events }: parcel is the current state, events the history sorted by at ascending (oldest first), possibly empty if the carrier exposes no history.
FeeQuery (input to getFees)
| Field | Type | Required | Description |
|---|---|---|---|
toWilayaId | string | int | yes | Destination wilaya (provider Location.id or code 1–58). |
toCommuneId | string | int | no | Destination commune, for carriers that price per commune. |
fromWilayaId | string | int | no | Origin wilaya. Defaults to the account’s origin when the carrier knows it. |
weight | number | no | Weight in kg (for overweight surcharges). |
length, width, height | number | no | Package dimensions in cm, for carriers that bill volumetric weight. |
isStopDesk | boolean | no | Quote for stop-desk delivery instead of home delivery. |
FeeQuote
| Field | Type | Required | Description |
|---|---|---|---|
deliveryFee | number | yes | Fee for the requested mode (home, or stop desk if isStopDesk: true). |
returnFee | number | no | Fee charged if the parcel is returned. |
stopDeskFee | number | no | Stop-desk fee, when the carrier returns both prices. |
currency | "DZD" | yes | Always "DZD". |
raw | unknown / mixed | no | Native response. |
Location (wilayas and communes)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Provider ID: pass it back as wilayaId / communeId. |
name | string | yes | Display name. |
parentId | string | no | For communes: the wilaya id. Absent for wilayas. |
code | string | no | Official code when known (wilaya "1"–"58", or postal code for communes). Lets apps match wilayas across providers, since some carriers (e.g. ZRExpress) use UUIDs as IDs. |
raw | unknown / mixed | no | Native object. |
ShippingError
Thrown as an exception (TS: class ShippingError extends Error; PHP: class ShippingError extends \RuntimeException).
| Field | Type | Required | Description |
|---|---|---|---|
code | ShippingErrorCode | yes | Normalized code (see Error codes). |
message | string | yes | Human-readable message (native message when available). |
httpStatus | int | no | HTTP status of the failed call. |
details | object / array | no | Normalized details, e.g. validation errors [{ field?, message }]. |
raw | unknown / mixed | no | Native error body. |
provider | string | no | Provider key that raised the error. |
retryAfter | int | no | RATE_LIMITED only: seconds to wait, from the Retry-After header when the carrier sends it. |
StopDeskQuery and StopDesk
Input to getStopDesks: wilayaId (string | int, required: provider Location.id, code 1–58 or name) and communeId (string | int, optional: provider Location.id or name). Some carriers require communeId (Maystro); elsewhere it is an optional filter, and wilaya-only is the safer choice when unsure.
StopDesk field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Pass it back as ParcelInput.stopDeskId. |
name | string | yes | Display name. |
address | string | no | Street address, when the carrier returns it. |
communeId | string | no | Provider commune id. |
wilayaId | string | no | Provider wilaya id. |
phone | string | no | Primary phone, when the carrier returns it. |
openingHours | string | no | Opening hours text, when available. |
lat, lng | number | no | Latitude, longitude. |
type | string | no | Carrier hub/desk type label when exposed (e.g. ZRExpress "stopdesk"). |
raw | unknown / mixed | no | Native object. |
ParcelListQuery and ParcelListPage
ParcelListQuery field | Type | Description |
|---|---|---|
page | int | 1-based page. Default 1. |
pageSize | int | Page size. Default 50. Carriers may clamp it (ZRExpress ≤ 1000). |
keyword | string | Free-text search (tracking, customer name, phone… when the carrier supports it). |
orderId | string | Merchant order reference (ParcelInput.orderId / carrier external id). |
status | ParcelStatus | Unified status filter, applied after mapping when the carrier has no equivalent filter (the usual case). |
updatedSince | string | ISO 8601: only parcels whose status changed at or after this instant (when the carrier exposes an update timestamp). |
allPages | boolean | true: follow hasNext and merge pages (with a safety cap). Default false. |
ParcelListPage field | Type | Description |
|---|---|---|
parcels | Parcel[] | Mapped parcels for this response (or all pages with allPages). |
page | int | Page that was fetched (1 when allPages merged everything). |
pageSize | int | Requested/effective page size. |
totalCount | int | Carrier-reported total matching the query (before client-side filters such as status, or E-com orderId). When the carrier reports no total for a page (E-com after page 1), a lower bound: (page - 1) * pageSize + items on the page. |
hasNext | boolean | More pages available (false when allPages finished or the carrier has no more). |
raw | unknown / mixed | Native page payload (last page with allPages). |
The ShippingProvider interface
Every client implements the same interface: ShippingProvider in TypeScript, ShippingProviderInterface in PHP.
| Method | Input | Output |
|---|---|---|
createParcel(input) | ParcelInput | Parcel |
track(trackingNumber) | string | TrackResult = { parcel, events } |
getFees(query) | FeeQuery | FeeQuote |
getWilayas() | — | Location[] |
getCommunes(wilayaId) | string | int (provider id or code 1–58) | Location[] |
cancelParcel(trackingNumberOrId) | string | void |
getStopDesks(query) | StopDeskQuery | StopDesk[] |
listParcels(query?) | ParcelListQuery | ParcelListPage |
healthCheck() | — | HealthCheckResult = { ok: true, provider, latencyMs } |
export type ParcelStatus = | 'pending' | 'created' | 'in_transit' | 'delivered' | 'returned' | 'cancelled' | 'unknown'; export interface HealthCheckResult { ok: true; provider: string; /** Round-trip duration in milliseconds. */ latencyMs: number; } export interface ShippingProvider { readonly name: string; // 'zrexpress' | 'yalidine' | ... createParcel(input: ParcelInput): Promise<Parcel>; track(trackingNumber: string): Promise<TrackResult>; getFees(query: FeeQuery): Promise<FeeQuote>; getWilayas(): Promise<Location[]>; getCommunes(wilayaId: string | number): Promise<Location[]>; getStopDesks(query: StopDeskQuery): Promise<StopDesk[]>; listParcels(query?: ParcelListQuery): Promise<ParcelListPage>; cancelParcel(trackingNumberOrId: string): Promise<void>; healthCheck(): Promise<HealthCheckResult>; }
namespace DzDelivery\Contracts; use DzDelivery\Models\FeeQuery; use DzDelivery\Models\FeeQuote; use DzDelivery\Models\HealthCheckResult; use DzDelivery\Models\Location; use DzDelivery\Models\Parcel; use DzDelivery\Models\ParcelInput; use DzDelivery\Models\ParcelListPage; use DzDelivery\Models\ParcelListQuery; use DzDelivery\Models\StopDesk; use DzDelivery\Models\StopDeskQuery; use DzDelivery\Models\TrackResult; interface ShippingProviderInterface { public function getName(): string; public function createParcel(ParcelInput|array $input): Parcel; public function track(string $trackingNumber): TrackResult; public function getFees(FeeQuery|array $query): FeeQuote; /** @return Location[] */ public function getWilayas(): array; /** @return Location[] */ public function getCommunes(string|int $wilayaId): array; /** @return StopDesk[] */ public function getStopDesks(StopDeskQuery|array $query): array; public function listParcels(ParcelListQuery|array|null $query = null): ParcelListPage; public function cancelParcel(string $trackingNumberOrId): void; public function healthCheck(): HealthCheckResult; }
Method behavior
createParcel: validates required fields client-side (VALIDATION_ERRORbefore any HTTP call) and resolveswilayaName/communeName/ wilaya codes to provider IDs. If the carrier only returns an ID on creation, the provider may do a follow-up read to filltrackingNumberandstatus.track: returns the currentParcelplus history. If the carrier needs an internal ID for history, the provider resolves it from the tracking number.getFees: returns a single quote for the requested destination and mode.NOT_FOUNDif the destination has no rate.getWilayas/getCommunes: return only deliverable locations when the carrier exposes that information. Results may be cached in memory for the client’s lifetime.cancelParcel: accepts a tracking number or an internal ID.NOT_SUPPORTEDif the carrier cannot cancel,VALIDATION_ERROR/PROVIDER_ERRORif the parcel is past the cancellable state. Returnsvoidbecause some carriers answer204 No Content: calltrack()afterwards if you need the new status.getStopDesks: lists the stop desks / pickup points of a wilaya (optionally one commune).VALIDATION_ERRORwhen a carrier needscommuneIdand it is missing,NOT_SUPPORTEDwhen the carrier exposes no listing. May throwNOT_FOUNDwith an actionabledetailshint when a commune filter yields no desks but the wilaya still has some (typical for ZRExpress hubs).listParcels: paginated list of the account’s parcels with their unifiedstatus, without history (calltrack()for that).NOT_SUPPORTEDwhen the carrier exposes no listing. Prefer webhooks over aggressive polling when available.healthCheck: probes credentials and connectivity with a lightweight authenticated request (the wilayas listing). Always hits the network (bypasses the cache). Returns{ ok: true, provider, latencyMs }, otherwise throwsShippingError(AUTH_FAILED,NETWORK_ERROR,RATE_LIMITED, …). Never throwsNOT_SUPPORTED.
Creating a client
const client = createClient('zrexpress', { apiKey, tenant, /* baseUrl?, timeoutMs?, includeRaw? */ });
$client = ClientFactory::create('zrexpress', ['apiKey' => ..., 'tenant' => ..., /* 'baseUrl', 'timeoutMs', 'includeRaw' */]);
| Common option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | provider default | Override the API base URL (sandbox, proxy, tests). |
timeoutMs | int | 15000 | HTTP timeout. |
includeRaw | boolean | true | Populate raw fields. |
Provider-specific credentials:
| Provider | Config keys | Env vars (.env.example) |
|---|---|---|
zrexpress | apiKey, tenant | ZREXPRESS_API_KEY, ZREXPRESS_TENANT |
yalidine | apiId, apiToken, fromWilayaId or fromWilayaName (origin wilaya, required by createParcel and getFees) | YALIDINE_API_ID, YALIDINE_API_TOKEN, YALIDINE_FROM_WILAYA_ID |
maystro | apiToken | MAYSTRO_API_TOKEN |
ecotrack | token, courier or baseUrl (https) | ECOTRACK_TOKEN, ECOTRACK_COURIER / ECOTRACK_BASE_URL |
noest | apiToken, userGuid, autoValidate? | NOEST_API_TOKEN, NOEST_USER_GUID |
ecom | apiKey, apiToken, autoValidate? | ECOM_API_KEY, ECOM_API_TOKEN |
mdm | apiKey, storeId (required by createParcel) | MDM_API_KEY, MDM_STORE_ID |
elogistia | apiKey, email? | ELOGISTIA_API_KEY |
A baseUrl override must be an https: URL, because credentials go with every request (in headers, or in the query string for Elogistia). The Ecotrack, NoEST, E-com, MDM and Elogistia clients reject any other URL with VALIDATION_ERROR. Detailed PHP configuration: PHP guide.
Outside the unified contract
Intentionally not part of the common interface: catalog endpoints (products, categories, stock), parcel updates (address, amount, products), exchanges, refunds, pickup bags, claims, customers, treasury and payments, reports, and webhook endpoint management. They may come later as optional, carrier-specific modules that do not change ShippingProvider.
Richer carrier features ship as extras on the concrete provider class, for example ZRExpress bulk create/delete, labels and webhook verification (ZRExpress page, ZREXPRESS.md).
This contract is versioned (v0.1): breaking changes to the models or the interface require a version bump and a matching release of both packages.