beziworld / laravel-dhl-express
DHL Express MyDHL API client for Laravel: shipments with PDF/ZPL labels and commercial invoices, rates, products, tracking with typed events, proof of delivery, pickups and address validation.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/events: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 19:46:55 UTC
README
DHL Express MyDHL API client for Laravel:
- typed shipment builder: shipper, receiver, packages with weight and dimensions or DHL packaging, references, value added services (insurance, duties paid, paperless trade, ...), payer and duties accounts, pickup booking, e-mail notification
- dutiable shipments with an export declaration and a DHL-generated commercial invoice (VAT and EORI numbers included)
- labels in PDF, ZPL, EPL or LP2 and other documents decoded from base64; validation without creating the shipment
- rates and products for a lane, with billing currency prices and estimated delivery
- tracking with typed checkpoints and normalised statuses, up to 200 shipments per call; electronic proof of delivery; customs and waybill images
- pickups (book and cancel), address validation, reference data
- several DHL Express accounts (named connections and
DhlExpress::using()), Laravel events,dhl-express:checkcommand
DHL Express or DHL Parcel?
DHL Express and DHL Parcel Polska are separate DHL companies with separate contracts, account numbers and APIs.
This package covers DHL Express (the international time-definite courier network, MyDHL API at
express.api.dhl.com, HTTP Basic credentials issued by DHL Express and a DHL Express account number, product codes
such as P, D, U, N). For the Polish domestic parcel network (DHL24 WebAPI, DHL Parcel Polska) use
beziworld/laravel-dhl-parcel. Credentials, labels, tracking
numbers and pickups of the two services are not interchangeable.
Why this package
| Package | API | Laravel 12/13 | Notes |
|---|---|---|---|
korbeil/dhl-express-php-api, sonnenglas/mydhl-php-sdk |
MyDHL REST | not Laravel packages | framework-agnostic SDKs |
dhl/sdk-api-express, dreipunktnull/dhl-express |
legacy SOAP/XML | no | last releases 2018 to 2020 |
elitedevsolution/laravel-dhl-api, jackbayliss/laravel-dhl-api |
legacy XML-PI | no | not maintained |
This package targets MyDHL API 3.4.1 and adds a typed builder, decoded documents, both MyDHL error formats mapped to
one exception, multiple accounts, the Laravel HTTP client (Http::fake() works in your tests), a strict no-retry
policy for writes and Laravel events.
Installation
composer require beziworld/laravel-dhl-express
php artisan vendor:publish --tag=dhl-express-config # optional
DHL_EXPRESS_USERNAME=... # MyDHL API key (from your DHL Express onboarding) DHL_EXPRESS_PASSWORD=... # MyDHL API secret DHL_EXPRESS_ACCOUNT_NUMBER=123456789 DHL_EXPRESS_SANDBOX=true # express.api.dhl.com/mydhlapi/test, 500 calls a day DHL_EXPRESS_SHIPPER_NAME="Jan Nowak" DHL_EXPRESS_SHIPPER_COMPANY="ACME Sp. z o.o." DHL_EXPRESS_SHIPPER_PHONE=+48500600700 DHL_EXPRESS_SHIPPER_EMAIL=shop@example.com DHL_EXPRESS_SHIPPER_ADDRESS="ul. Prosta 1" DHL_EXPRESS_SHIPPER_CITY=Warszawa DHL_EXPRESS_SHIPPER_POSTAL_CODE=00-001 DHL_EXPRESS_SHIPPER_COUNTRY=PL DHL_EXPRESS_SHIPPER_VAT=PL5250000000 DHL_EXPRESS_SHIPPER_EORI=PL525000000000000 DHL_EXPRESS_UNITS=metric # metric (kg, cm) or imperial (lb, in) DHL_EXPRESS_LABEL_FORMAT=pdf # pdf, zpl, epl or lp2 DHL_EXPRESS_TIMEZONE=Europe/Warsaw # planned shipping times
Check the credentials with a read-only address validation call: php artisan dhl-express:check.
Create a shipment
use Beziworld\DhlExpress\Data\Money; use Beziworld\DhlExpress\Data\Parcel; use Beziworld\DhlExpress\Data\Party; use Beziworld\DhlExpress\Enums\LabelFormat; use Beziworld\DhlExpress\Facades\DhlExpress; $shipment = DhlExpress::shipment() ->to(new Party('Max Mustermann', 'Potsdamer Str. 1', 'Berlin', '10785', 'DE', phone: '+49301234', email: 'max@example.com')) ->product('U') // take the codes available for the lane from rates() ->parcel(new Parcel(weight: 1.5, length: 20, width: 15, height: 10, reference: 'ORDER-1')) ->description('Books') ->reference('ORDER-1') ->labelFormat(LabelFormat::Zpl) ->insurance(Money::of(200, 'EUR')) ->create(); $shipment->trackingNumber; // waybill number, e.g. 1234567890 $shipment->packageTrackingNumbers; // JD... piece numbers $shipment->label()->content; // decoded label $shipment->trackingUrl;
Store the documents when the shipment is created: MyDHL has no endpoint to print a label again and no endpoint to cancel a shipment (an unused shipment is simply not handed over).
Other builder options: shipper(), plannedAt() (defaults to 15 minutes from now, MyDHL rejects past times),
incoterm(), service($code, ?Money), payer(), dutiesAndTaxesAccount(), requestPickup($closeTime, $location),
withWaybill(), withRateEstimates(), notify($email) and with([...]) for any other root element. validate()
checks the shipment with validateDataOnly=true without creating it; toArray() returns the request body.
Dutiable shipments
use Beziworld\DhlExpress\Data\Customs; use Beziworld\DhlExpress\Data\CustomsItem; $shipment = DhlExpress::shipment() ->to(new Party('John Smith', '5th Avenue 1', 'New York', '10001', 'US', phone: '+12125550100', provinceCode: 'NY')) ->product('P') ->parcel(new Parcel(1.0, 30, 20, 10)) ->description('Cotton T-shirts') ->incoterm('DAP') ->customs(new Customs( items: [new CustomsItem('Cotton T-shirt', quantity: 3, unitPrice: '12.00', netWeight: 0.6, originCountry: 'PL', hsCode: '610910')], currency: 'USD', invoiceNumber: 'FV/1/2026', exportReasonType: 'commercial_purpose_or_sale', )) ->create(); $shipment->document('invoice')?->content; // commercial invoice generated by DHL
Rates and products
$rates = DhlExpress::shipment()->to($receiver)->parcel(new Parcel(1.5, 20, 15, 10))->rates(); // cheapest first $rates[0]->productCode; // U $rates[0]->productName; // EXPRESS WORLDWIDE EU $rates[0]->total; // Money in the billing currency (BILLC) $rates[0]->estimatedDelivery; // CarbonImmutable $rates[0]->transitDays; DhlExpress::products($shipper, $receiver, new Parcel(1.5, 20, 15, 10)); // products without prices
Tracking
$shipment = DhlExpress::track('9356579890'); $shipment->status; // TrackingStatus::Delivered, OutForDelivery, PickedUp, InTransit, Exception, Returned, Unknown $shipment->deliveredAt(); $shipment->signedBy; foreach ($shipment->events as $event) { // newest first $event->occurredAt; // local time with the GMT offset sent by DHL $event->typeCode; // checkpoint code, e.g. PU, WC, OK $event->description; $event->location; // service area, e.g. Berlin-DE } DhlExpress::trackMany(['9356579890', '4818240420']); DhlExpress::proofOfDelivery('9356579890')->content; // PDF DhlExpress::shipmentImages('9356579890', now(), 'commercial-invoice'); DhlExpress::referenceData('trackingEventCode'); // full list of checkpoint codes
The test environment only tracks the waybills listed by DHL (for example 9356579890); shipments created in the
test environment are not trackable.
Pickups and addresses
use Beziworld\DhlExpress\Data\PickupRequest; $pickup = DhlExpress::schedulePickup(new PickupRequest( readyAt: now('Europe/Warsaw')->addDay()->setTime(14, 0), closeTime: '18:00', parcels: [new Parcel(1.5, 20, 15, 10)], productCode: 'U', location: 'reception', )); DhlExpress::cancelPickup($pickup->number(), requestorName: 'Jan Nowak', reason: 'not needed'); DhlExpress::validateAddress('PL', '00-001', 'Warszawa', 'pickup'); // list<AddressMatch> with service areas
Multiple accounts
DhlExpress::connection('de')->shipment()->...; // config/dhl-express.php connections DhlExpress::using(['username' => $tenant->dhl_key, 'password' => $tenant->dhl_secret, 'account_number' => $tenant->dhl_account])->track('9356579890');
Events
ShipmentCreated, PickupScheduled and PickupCancelled (namespace Beziworld\DhlExpress\Events).
Errors
| Exception | When |
|---|---|
ApiException |
MyDHL rejected the request or could not be reached: errorCode() (numeric DHL code from the detail, e.g. 998), detail(), details(), httpStatus(), isValidationError() (400/422), isAuthenticationError(), isNotFound(), isRateLimited() |
InvalidConfiguration |
missing credentials, account number, shipper, receiver, product, description or parcels |
Both extend DhlExpressException. Read-only calls are retried on connection errors and HTTP 5xx
(DHL_EXPRESS_RETRIES); shipments and pickups are never retried automatically.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration? Get in touch through beziworld.eu.
Other packages by the author
laravel-dhl-parcel · laravel-ups · laravel-fedex · laravel-dpd · laravel-inpost
License
MIT. See LICENSE.md. This package is not affiliated with, endorsed or sponsored by DHL or Deutsche Post DHL Group. DHL is a registered trademark of Deutsche Post AG.