ernadoo / mondial-relay
Mondial Relay API client — V2 REST (label creation) + SOAP (relay point search)
Requires
- php: ^8.2
- ext-simplexml: *
- ext-soap: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^2.0 || ^3.0
Requires (Dev)
- nyholm/psr7: ^1.8.2
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
Suggests
- guzzlehttp/guzzle: PSR-18 implementation with GuzzleHttp\Psr7\HttpFactory for PSR-17
- symfony/http-client: PSR-18 implementation — Psr18Client also implements PSR-17 factories
Provides
None
Conflicts
None
Replaces
None
README
PHP client for the Mondial Relay shipping API. Framework-agnostic.
- Label creation: V2 REST API (production and sandbox)
- Relay point search: V1 SOAP API (Mondial Relay has not published a V2 REST endpoint for it yet)
Using Symfony? The mondial-relay-bundle wires everything for you and adds a relay point picker.
Requirements
- PHP 8.2+
ext-soap,ext-simplexml- Any PSR-18 HTTP client with PSR-17 factories
(e.g.
symfony/http-client+nyholm/psr7, orguzzlehttp/guzzle)
Installation
composer require ernadoo/mondial-relay symfony/http-client nyholm/psr7
API credentials
The names follow MR Connect, Mondial Relay's customer area:
| Argument | In MR Connect | Used for |
|---|---|---|
brandCode |
Code enseigne (brand code, 8 characters) | Label creation and relay point search |
apiLogin |
Login of an API user: Administration → User management → API configuration (French UI: « Gestion des utilisateurs → Configuration des API ») | Label creation (V2 REST) |
apiPassword |
Password of that API user | Label creation (V2 REST) |
privateKey |
Clé privée (private key) of the brand | Relay point search only (V1 SOAP signature) |
Creating the client
use Ernadoo\MondialRelay\MondialRelayClient; // Symfony's Psr18Client implements the PSR-18 client and both PSR-17 factories. $psr18 = new \Symfony\Component\HttpClient\Psr18Client(); $client = MondialRelayClient::create( httpClient: $psr18, requestFactory: $psr18, streamFactory: $psr18, apiLogin: 'YOUR_API_LOGIN', apiPassword: 'YOUR_API_PASSWORD', brandCode: 'YOUR_BRAND_CODE', privateKey: 'YOUR_PRIVATE_KEY', sandbox: false, );
To assemble the clients yourself (e.g. with Guzzle, or a mock PSR-18 client in tests):
use Ernadoo\MondialRelay\Client\RestShipmentClient; use Ernadoo\MondialRelay\Client\SoapParcelShopClient; use Ernadoo\MondialRelay\MondialRelayClient; $client = new MondialRelayClient( new RestShipmentClient($httpClient, $requestFactory, $streamFactory, 'API_LOGIN', 'API_PASSWORD', 'BRAND_CODE', sandbox: false), new SoapParcelShopClient('BRAND_CODE', 'PRIVATE_KEY'), );
MondialRelayClientInterface is the single entry point: type-hint it and mock it in your tests.
Sandbox
sandbox: true sends label creation to https://connect-api-sandbox.mondialrelay.com/api/shipment:
labels are generated ("SANDBOX MODE") but nothing is recorded. It needs a valid API user: invalid
credentials are rejected (error 10001), and the public BDTEST brand used in older examples is no
longer active. Relay point search always hits the production SOAP endpoint (Mondial Relay provides
no sandbox for it).
Creating a label
use Ernadoo\MondialRelay\Shipment\Address; use Ernadoo\MondialRelay\Shipment\Parcel; use Ernadoo\MondialRelay\Shipment\ShipmentRequest; $request = new ShipmentRequest( sender: new Address( countryCode: 'FR', postCode: '59510', city: 'Hem', streetName: '4 Avenue Antoine Pinay', firstName: 'Erwan', lastName: 'Nader', mobileNo: '+33600000000', email: 'sender@example.com', ), recipient: new Address( countryCode: 'FR', postCode: '75001', city: 'Paris', streetName: '1 Rue de la Paix', firstName: 'Jane', lastName: 'Doe', mobileNo: '+33600000001', email: 'recipient@example.com', ), parcels: [new Parcel(weightGrams: 500, content: 'Clothes')], ); $response = $client->createShipment($request); $response->shipmentNumber; // e.g. "12345678" $response->labelOutput; // URL of the PDF label $response->trackingUrl; // public tracking link
Choosing the relay point
-
Specific relay point: pass its ID as
deliveryLocation, e.g. from a relay point search ($shop->locationCode()) or from the bundle's picker:$request = new ShipmentRequest( // ... deliveryLocation: 'FR-066974', );
-
"Notif Destinataire": leave
deliveryLocationempty (the default). Mondial Relay sends the recipient an SMS/email with a link to choose their relay point: no coordination needed between sender and recipient.
Delivery modes
deliveryMode |
API code | Description |
|---|---|---|
DeliveryMode::RELAY (default) |
24R |
Relay point |
DeliveryMode::RELAY_XL |
24L |
XL relay point (multi-parcel) |
DeliveryMode::HOME |
LCC |
Home delivery |
DeliveryMode::HOME_PLUS |
HOM |
Home delivery (variant) |
DeliveryMode::HOME_APPOINTMENT |
LD1 |
Home delivery with appointment |
DeliveryMode::HOME_APPOINTMENT_HEAVY |
LDS |
Home delivery with appointment, heavy parcels |
Collection modes
collectionMode |
API code | Description |
|---|---|---|
CollectionMode::DROP_OFF (default) |
CCC |
You drop the parcels at a Mondial Relay point |
CollectionMode::RELAY_PICKUP |
REL |
Mondial Relay picks up at your relay point |
CollectionMode::HOME_PICKUP |
CDR |
Mondial Relay picks up at your address |
CollectionMode::HOME_PICKUP_HEAVY |
CDS |
Pickup at your address, heavy parcels |
Label output
outputType |
Result in labelOutput |
|---|---|
OutputType::PDF_URL (default) |
URL of the PDF label |
OutputType::ZPL / OutputType::IPL |
Raw printer code (thermal printers) |
OutputType::QR_CODE |
QR code, for label-less returns |
outputFormat: OutputFormat::SIZE_10X15 (default), A4, A5, or THERMAL_ZPL / THERMAL_IPL
for thermal printers.
Multiple parcels
$request = new ShipmentRequest( // ... deliveryMode: DeliveryMode::RELAY_XL, // 24L supports multi-parcel parcels: [ new Parcel(weightGrams: 800, content: 'Shoes'), new Parcel(weightGrams: 600, content: 'Clothes'), ], );
Searching relay points
Relay point search uses the V1 SOAP API (WSI4_PointRelais_Recherche).
use Ernadoo\MondialRelay\ParcelShop\ParcelShopSearchRequest; use Ernadoo\MondialRelay\Shipment\DeliveryMode; $shops = $client->searchParcelShops(new ParcelShopSearchRequest( countryCode: 'FR', postCode: '75001', deliveryMode: DeliveryMode::RELAY, // only relay points compatible with 24R weightGrams: 500, searchDistanceKm: 10, maxResults: 7, )); foreach ($shops as $shop) { $shop->name; // "Tabac du Centre" $shop->distanceKm; // 0.5 $shop->locker; // true for an automated parcel locker $shop->locationCode(); // "FR-066974": use it as deliveryLocation }
Search around coordinates instead of a post code, e.g. the browser's geolocation or the centre of a map:
$shops = $client->searchParcelShops(ParcelShopSearchRequest::around('FR', 47.9944, -4.0915, maxResults: 10));
Opening hours, day by day (ISO-8601 days, 1 = Monday … 7 = Sunday):
$hours = $shop->schedule(); $hours->on(1); // [['09:30', '13:00'], ['14:00', '19:00']] $hours->on(7); // [] — closed $hours->isAlwaysOpen(); // true for a 24/7 locker; lockers inside a shop follow its opening hours
ParcelShop property |
Type | Description |
|---|---|---|
id |
string |
6-digit relay point ID |
name |
string |
Business name |
address1, address2 |
string |
Street address and complement |
postCode, city, countryCode |
string |
Postal code, city, ISO 2-letter country code |
latitude, longitude |
float |
GPS coordinates |
distanceKm |
float |
Distance from the searched location |
openingHours |
array |
Raw day-indexed opening hours (['Lundi' => '0930-1300 1400-1900']); prefer schedule() |
pictureUrl |
string |
Photo URL |
locker |
bool |
Automated parcel locker rather than a shop |
directions |
string |
Directions to find the relay point, when given |
Error handling
use Ernadoo\MondialRelay\Exception\ApiException; use Ernadoo\MondialRelay\Exception\ConfigurationException; use Ernadoo\MondialRelay\Exception\TransportException; try { $response = $client->createShipment($request); } catch (ApiException $e) { // Mondial Relay rejected the request (invalid phone number, unknown relay point…) foreach ($e->getErrors() as $code => $message) { echo "[$code] $message\n"; } } catch (TransportException $e) { // Mondial Relay unreachable, HTTP or SOAP error, malformed or incomplete response: retry later } catch (ConfigurationException $e) { // A credential needed by the operation is missing }
All three extend MondialRelayException: catch it alone to handle every failure. The same applies
to searchParcelShops().
Exception messages are meant for developers and logs, in English. The messages inside
getErrors() come from Mondial Relay itself, in the language of the request culture (fr-FR by
default). To show a message to your users, translate from the exception class and the codes:
ernadoo/mondial-relay-bundle does it for Symfony applications.
Codes seen on the label creation API: 10001 invalid API login or password, 10034 parcel weight
out of range, 10051 invalid phone number (international format expected), 10055 no sorting plan
for this relay point (unknown relay point, or unavailable for this delivery mode), 99999 generic
error. The relay point search uses the codes of the former API (STAT, 1–99).
Logging
Pass any PSR-3 logger (Monolog…):
$client->setLogger($logger);
| Level | Logged |
|---|---|
info |
Shipment created (number, delivery mode, relay point), relay point search (result count) |
warning |
Non-blocking warnings returned by Mondial Relay (code and message) |
error |
Rejections with the Mondial Relay codes and messages, HTTP failures |
Credentials, request and response bodies, and addresses are never logged.
Tests
composer install vendor/bin/phpunit