GLS for omnisense/omnibus: shipments with their PDF
labels and cancellations through GLS's ShipIT web API (HTTP basic authentication), tracking
through GLS's public parcel tracking (no credentials). ShipIT quotes no prices: they come from the
rates option. The gateway calls:
| Operation | API | Request |
|---|---|---|
| ship | ShipIT | POST /shipments |
| cancel | ShipIT | POST /shipments/cancel/{trackId} |
| track | public tracking | GET https://gls-group.eu/app/service/open/rest/EU/{language}/rstt001?match={number} |
ShipIT paths are on https://shipit.gls-group.eu/backend/rs (live) or
https://shipit-wbm-test01.gls-group.eu:8443/backend/rs (the test system, sandbox: true). The
public tracking has one address, whatever sandbox says.
Installation
composer require omnibus/gls
Requires PHP 8.2, omnisense/omnibus (2.x) and symfony/http-client (6.4, 7 or 8); given no
HTTP client, the factory makes one with HttpClient::create(). In a Symfony application with
Omnibus\Bridge\Symfony\OmnibusBundle, the factory is registered as soon as the package is
installed.
Credentials
- A GLS customer account with ShipIT web API access: the ShipIT username and password, and the customer's contact id at GLS. The GLS sales contact gives the test login and the contact id, then the production login.
Tracking needs none of them.
Options
| Option | Type | Default | Required | |
|---|---|---|---|---|
username |
string | - | yes | the ShipIT login |
password |
string | - | yes | its password |
contact_id |
string | - | yes | the shipper's contact id (Shipper.ContactID) |
sandbox |
bool | false |
no | true: the ShipIT test system |
rates |
list | - | no | the only way this gateway rates (the rates option) |
Shipment options (Shipment::$options):
| Option | Default | |
|---|---|---|
flex_delivery |
true |
adds the FlexDelivery service when the recipient has an email; false leaves it out |
Capabilities
| Request | Supported |
|---|---|
Rating |
only with rates (configured prices): ShipIT quotes none |
Shipping |
yes |
Tracking |
yes (public tracking) |
Pickup |
no: ShipIT has no ParcelShop search (GLS's finder is a separate contract) |
GetSlip |
no |
Cancel |
yes |
Paperless |
no |
Behaviour
Calls. ShipIT calls use basic authentication and the media type
application/glsVersion1+json, with a 20-second timeout.
Ship. One POST /shipments:
- the product is
Shipment::$servicein capitals, elsePARCEL(GLS's products includeEXPRESSandFREIGHT); - the shipping date
Shipment::$shippingDate, else today; - the recipient is the consignee, the sender the shipper's alternative address, under the configured contact id;
- an address is
Name1(the company, else the person, 40 characters),Name2(the person when there is a company), country, postcode, city,Street(the first street line, 40 characters), the email, and the phone asMobilePhoneNumber; when the address has a single street line starting with a number (12 rue...,21b ...), that number is also sent asStreetNumber; - each parcel is a shipment unit: its weight in kilograms rounded to two decimals (at least 0.1),
its
referencewhen it has one; dimensions are not sent; - the shipment's
reference(40 characters) asShipmentReference; - services:
ShopDeliverywith thepickupPointasParcelShopIDwhen the shipment has one;FlexDeliverywhen the recipient has an email andflex_deliveryis notfalse; - labels as PDF, template set
NONE.
The tracking number is the first parcel's TrackID (else its ParcelNumber); the label the first
PrintData document, base64-decoded, application/pdf. A shipment of several parcels gives one
Label with the first parcel's number and document. trackingUrl is
https://gls-group.eu/EU/en/parcel-tracking?match=<number>. A response without a number is a
CarrierException (GLS booked no shipment.).
Track. The public tracking in French when the locale starts with fr, in German for de,
else in English. The first parcel's history gives the events: date and time (in PHP's default
time zone), GLS's description, city countryName as location; no event code is kept. Events are
sorted oldest first; the status is the latest event's, or DELIVERED when the progress bar's
statusInfo is DELIVERED. GLS sends no status code, so an event's status is read from its
description, in this order:
| The description contains | TrackingStatus |
|---|---|
not delivered, not be delivered, undeliver, non livré, pas pu être livré, pas été livré, nicht zugestellt, exception |
EXCEPTION |
delivered, livré or zugestellt, with parcelshop or parcel shop |
AVAILABLE_FOR_PICKUP |
delivered, livré or zugestellt |
DELIVERED |
out for delivery, en cours de livraison, in zustellung |
OUT_FOR_DELIVERY |
return, retour, rück |
RETURNED |
data, données, daten |
PENDING |
| anything else | IN_TRANSIT |
| no description | UNKNOWN |
The failed deliveries come first: their texts ("The parcel could not be delivered...", "Das Paket konnte nicht zugestellt werden...") contain the words of a delivery.
Cancel. POST /shipments/cancel/{trackId}: true when GLS's result is CANCELLED or
CANCELLATION_PENDING, false for any other result.
Example
<?php
require __DIR__.'/vendor/autoload.php';
use Omnibus\Gls\GlsGatewayFactory;
use Omnibus\Model\Address;
use Omnibus\Model\Parcel;
use Omnibus\Model\Shipment;
use Symfony\Component\HttpClient\HttpClient;
$gls = (new GlsGatewayFactory(HttpClient::create()))->create([
'username' => getenv('GLS_USERNAME'),
'password' => getenv('GLS_PASSWORD'),
'contact_id' => getenv('GLS_CONTACT_ID'),
'sandbox' => true,
'rates' => [
['service' => 'PARCEL', 'label' => 'GLS', 'days' => 2, 'countries' => ['FR', 'BE', 'DE'],
'bands' => [2000 => 690, 5000 => 890, 10000 => 1190]],
],
]);
$shipment = new Shipment(
new Address('Glitch Art', ['12 rue de la Paix'], '75002', 'Paris', 'FR', company: 'Glitch Art'),
new Address('Émile Zola', ['21 rue de Bruxelles'], '75009', 'Paris', 'FR', email: 'emile@example.org', phone: '+33612345678'),
[new Parcel(800, reference: 'BOX-1')],
reference: 'CMD-1042',
);
foreach ($gls->rate($shipment) as $rate) {
printf("%s %s %5.2f %s\n", $rate->service, $rate->label, $rate->amount / 100, $rate->currency);
}
$label = $gls->ship($shipment);
file_put_contents($label->trackingNumber.'.pdf', $label->content);
echo $label->trackingNumber, ' ', $label->trackingUrl, "\n";
$tracking = $gls->track($label->trackingNumber, 'fr');
echo $tracking->status->value, ' ', $tracking->latest()?->description, "\n";
var_dump($gls->cancel($label->trackingNumber));
In Symfony:
omnibus:
gateways:
gls:
factory: gls
options:
username: '%env(GLS_USERNAME)%'
password: '%env(GLS_PASSWORD)%'
contact_id: '%env(GLS_CONTACT_ID)%'
sandbox: '%env(bool:GLS_SANDBOX)%'
rates:
- { service: PARCEL, label: 'GLS', bands: { 2000: 690, 5000: 890 } }
A rate needs its bands: an entry with an amount and no bands is never offered.
Errors
Every failure is an Omnibus\Exception\CarrierException whose carrier is gls:
- a ShipIT HTTP status of 400 or more: the
messageheader GLS sends, else the body'smessage, else its first error'sdescription, elseHTTP <status>; theerrorheader incarrierCode; - a ShipIT answer that is not JSON:
GLS answered with a body that is not JSON.; a network error:GLS request failed: ...; - a tracking HTTP status of 400 or more: its
message, elseGLS tracking answered HTTP <status>.; a network error or unreadable body:GLS tracking failed: ...; GLS booked no shipment.when no parcel number comes back.
A missing username, password or contact_id is an InvalidConfigException when the gateway
is built; Pickup, GetSlip, Paperless, and Rating without rates, raise
RequestNotSupportedException before any call.
No failure carries the password: a network error's message has them masked as *** (the key
parameters of a URL too), its previous is a copy of the HTTP client's exception (the same class and
code), and no trace of the chain keeps its arguments, where the request's options stood (No secret
in a
failure).
Verified, and not
Tested offline: Tests/GlsGatewayTest.php (a shipment created with its PDF label, the contact id,
product PARCEL, the reference, the company-less consignee, the weight in kilograms and
FlexDelivery; ShopDelivery for a ParcelShop; tracking with the progress bar's DELIVERED, its
location and the English path; failed deliveries in English, German and French read as
exceptions; cancellation; configured rates; no pickup points) and
Tests/GlsContractTest.php, the family's conformance suite on recorded answers. The example above
was run in the core's harness on those recorded answers.
Not verified: the README says the package is built from GLS's published ShipIT documentation and was not yet run against the test system; that needs the credentials above.
Responsibility
An independent, unofficial client, not affiliated with GLS (General Logistics Systems B.V.). Whoever runs it complies with GLS's terms (those of its developer portal and of the contract, dev-portal.gls-group.net): see the README's Responsibility section.