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:

ParcelStatus
pending | created | in_transit | delivered | returned | cancelled | unknown
StatusMeaningYalidine examplesMaystro examples (code)
pendingOrder 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
createdConfirmed 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_transitPicked 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
deliveredDelivered to the recipient (COD collected).“Livré” (delivered)41 DELIVERED
returnedReturned 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
cancelledCancelled / deleted before delivery.“Annulé” (cancelled), “Supprimé” (deleted)50 ABORTED
unknownNative 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 labelany 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.

TypeScript
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
PHP
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:

codeWhen
AUTH_FAILED401/403, missing or invalid credentials.
VALIDATION_ERROR400/422, or SDK-side input validation failure (before any HTTP call).
NOT_FOUND404, parcel / location not found, or name resolution (wilayaName, communeName) failed.
RATE_LIMITED429.
NOT_SUPPORTEDThe carrier does not support this operation (e.g. cancellation).
PROVIDER_ERROR5xx or an unexpected/unparseable carrier response.
NETWORK_ERRORTimeout, DNS, connection refused.
UNKNOWNAnything 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

TopicRule
NamingcamelCase for fields and methods in both TS and PHP (PHP objects expose public readonly properties with the same names).
IDsAll IDs returned by the SDK are strings (carriers use UUIDs, integers or codes). Inputs accept string | int.
MoneyAmounts are in DZD, as numbers (dinars, not centimes). currency is always "DZD".
Weight / sizeWeight in kg, dimensions in cm.
DatesISO 8601 strings with timezone (e.g. 2025-12-23T15:23:54+00:00) in both TS and PHP.
PhonesStrings. Providers normalize to the carrier’s format (e.g. 0550123456 ↔ +213550123456).
Optional? = optional. Absent optional values are undefined (TS) / null (PHP).
AsyncTS methods return Promise<T>. PHP methods are synchronous and return T.
ErrorsAll 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).

  • raw is 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 raw for core logic.
  • raw is populated by default; the client option includeRaw: false disables it.

Models

The same models, with the same field names, in TypeScript and PHP.

ParcelInput (input to createParcel)

FieldTypeRequiredDescription
recipientNamestringyesFull name of the recipient.
phonestringyesPrimary phone number.
phone2stringnoSecondary phone number.
addressstringyesStreet address / free text.
wilayaIdstring | intone of¹Provider Location.id or official wilaya code 1–58.
wilayaNamestringone of¹Wilaya name (resolved by the provider, case- and accent-insensitive).
communeIdstring | intone of²Provider Location.id of the commune.
communeNamestringone of²Commune name (resolved within the wilaya).
productNamestringone of³Single free-text product description.
productListProductItem[]one of³Detailed list of products.
pricenumberyesAmount to collect from the recipient (COD), in DZD. 0 for prepaid.
weightnumbernoTotal weight in kg.
length, width, heightnumbernoPackage dimensions in cm.
isStopDeskbooleannotrue = delivery to a stop desk / pickup point. Default false (home delivery).
stopDeskIdstringif isStopDeskProvider ID of the stop desk / hub.
remarkstringnoNote for the delivery agent.
orderIdstringnoYour own order reference (stored as external ID when supported).
stockType"none" | "local" | "warehouse"noDefault 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

FieldTypeRequiredDescription
namestringyesProduct name.
quantityintyesQuantity (≥ 1).
unitPricenumbernoUnit price in DZD.
skustringnoMerchant SKU.
productIdstringnoCarrier catalog product id (ZRExpress when stockType is local / warehouse).
stockType"none" | "local" | "warehouse"noPer-item stock mode; overrides ParcelInput.stockType.

Parcel

FieldTypeRequiredDescription
idstringyesCarrier’s internal parcel ID (may equal trackingNumber for some carriers).
trackingNumberstringyesTracking number shown to customers.
statusParcelStatusyesUnified status (see Statuses).
providerstringyesProvider key, e.g. "zrexpress", "yalidine".
labelUrlstringnoShipping 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.
rawunknown / mixednoNative response.

TrackingEvent and TrackResult

FieldTypeRequiredDescription
statusParcelStatusyesUnified status after this event.
labelstringyesHuman-readable native label (e.g. "Prêt à expédier", ready to ship).
atstringyesISO 8601 timestamp of the event.
rawunknown / mixednoNative 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)

FieldTypeRequiredDescription
toWilayaIdstring | intyesDestination wilaya (provider Location.id or code 1–58).
toCommuneIdstring | intnoDestination commune, for carriers that price per commune.
fromWilayaIdstring | intnoOrigin wilaya. Defaults to the account’s origin when the carrier knows it.
weightnumbernoWeight in kg (for overweight surcharges).
length, width, heightnumbernoPackage dimensions in cm, for carriers that bill volumetric weight.
isStopDeskbooleannoQuote for stop-desk delivery instead of home delivery.

FeeQuote

FieldTypeRequiredDescription
deliveryFeenumberyesFee for the requested mode (home, or stop desk if isStopDesk: true).
returnFeenumbernoFee charged if the parcel is returned.
stopDeskFeenumbernoStop-desk fee, when the carrier returns both prices.
currency"DZD"yesAlways "DZD".
rawunknown / mixednoNative response.

Location (wilayas and communes)

FieldTypeRequiredDescription
idstringyesProvider ID: pass it back as wilayaId / communeId.
namestringyesDisplay name.
parentIdstringnoFor communes: the wilaya id. Absent for wilayas.
codestringnoOfficial 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.
rawunknown / mixednoNative object.

ShippingError

Thrown as an exception (TS: class ShippingError extends Error; PHP: class ShippingError extends \RuntimeException).

FieldTypeRequiredDescription
codeShippingErrorCodeyesNormalized code (see Error codes).
messagestringyesHuman-readable message (native message when available).
httpStatusintnoHTTP status of the failed call.
detailsobject / arraynoNormalized details, e.g. validation errors [{ field?, message }].
rawunknown / mixednoNative error body.
providerstringnoProvider key that raised the error.
retryAfterintnoRATE_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 fieldTypeRequiredDescription
idstringyesPass it back as ParcelInput.stopDeskId.
namestringyesDisplay name.
addressstringnoStreet address, when the carrier returns it.
communeIdstringnoProvider commune id.
wilayaIdstringnoProvider wilaya id.
phonestringnoPrimary phone, when the carrier returns it.
openingHoursstringnoOpening hours text, when available.
lat, lngnumbernoLatitude, longitude.
typestringnoCarrier hub/desk type label when exposed (e.g. ZRExpress "stopdesk").
rawunknown / mixednoNative object.

ParcelListQuery and ParcelListPage

ParcelListQuery fieldTypeDescription
pageint1-based page. Default 1.
pageSizeintPage size. Default 50. Carriers may clamp it (ZRExpress ≤ 1000).
keywordstringFree-text search (tracking, customer name, phone… when the carrier supports it).
orderIdstringMerchant order reference (ParcelInput.orderId / carrier external id).
statusParcelStatusUnified status filter, applied after mapping when the carrier has no equivalent filter (the usual case).
updatedSincestringISO 8601: only parcels whose status changed at or after this instant (when the carrier exposes an update timestamp).
allPagesbooleantrue: follow hasNext and merge pages (with a safety cap). Default false.
ParcelListPage fieldTypeDescription
parcelsParcel[]Mapped parcels for this response (or all pages with allPages).
pageintPage that was fetched (1 when allPages merged everything).
pageSizeintRequested/effective page size.
totalCountintCarrier-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.
hasNextbooleanMore pages available (false when allPages finished or the carrier has no more).
rawunknown / mixedNative page payload (last page with allPages).

The ShippingProvider interface

Every client implements the same interface: ShippingProvider in TypeScript, ShippingProviderInterface in PHP.

MethodInputOutput
createParcel(input)ParcelInputParcel
track(trackingNumber)stringTrackResult = { parcel, events }
getFees(query)FeeQueryFeeQuote
getWilayas()—Location[]
getCommunes(wilayaId)string | int (provider id or code 1–58)Location[]
cancelParcel(trackingNumberOrId)stringvoid
getStopDesks(query)StopDeskQueryStopDesk[]
listParcels(query?)ParcelListQueryParcelListPage
healthCheck()—HealthCheckResult = { ok: true, provider, latencyMs }
TypeScript
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>;
}
PHP
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_ERROR before any HTTP call) and resolves wilayaName / communeName / wilaya codes to provider IDs. If the carrier only returns an ID on creation, the provider may do a follow-up read to fill trackingNumber and status.
  • track: returns the current Parcel plus 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_FOUND if 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_SUPPORTED if the carrier cannot cancel, VALIDATION_ERROR / PROVIDER_ERROR if the parcel is past the cancellable state. Returns void because some carriers answer 204 No Content: call track() afterwards if you need the new status.
  • getStopDesks: lists the stop desks / pickup points of a wilaya (optionally one commune). VALIDATION_ERROR when a carrier needs communeId and it is missing, NOT_SUPPORTED when the carrier exposes no listing. May throw NOT_FOUND with an actionable details hint 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 unified status, without history (call track() for that). NOT_SUPPORTED when 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 throws ShippingError (AUTH_FAILED, NETWORK_ERROR, RATE_LIMITED, …). Never throws NOT_SUPPORTED.

Creating a client

TypeScript
const client = createClient('zrexpress', { apiKey, tenant, /* baseUrl?, timeoutMs?, includeRaw? */ });
PHP
$client = ClientFactory::create('zrexpress', ['apiKey' => ..., 'tenant' => ..., /* 'baseUrl', 'timeoutMs', 'includeRaw' */]);
Common optionTypeDefaultDescription
baseUrlstringprovider defaultOverride the API base URL (sandbox, proxy, tests).
timeoutMsint15000HTTP timeout.
includeRawbooleantruePopulate raw fields.

Provider-specific credentials:

ProviderConfig keysEnv vars (.env.example)
zrexpressapiKey, tenantZREXPRESS_API_KEY, ZREXPRESS_TENANT
yalidineapiId, apiToken, fromWilayaId or fromWilayaName (origin wilaya, required by createParcel and getFees)YALIDINE_API_ID, YALIDINE_API_TOKEN, YALIDINE_FROM_WILAYA_ID
maystroapiTokenMAYSTRO_API_TOKEN
ecotracktoken, courier or baseUrl (https)ECOTRACK_TOKEN, ECOTRACK_COURIER / ECOTRACK_BASE_URL
noestapiToken, userGuid, autoValidate?NOEST_API_TOKEN, NOEST_USER_GUID
ecomapiKey, apiToken, autoValidate?ECOM_API_KEY, ECOM_API_TOKEN
mdmapiKey, storeId (required by createParcel)MDM_API_KEY, MDM_STORE_ID
elogistiaapiKey, 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.

Further reading