Looking to hire Laravel developers? Try LaraJobs

laravel-dhl-express maintained by beziworld

Description
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.
Last update
2026/11/04 20:46 (dev-master)
License
Downloads
1

Comments
comments powered by Disqus

Laravel DHL Express

tests Latest Version License

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:check command

Polska wersja dokumentacji

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.

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.