sonnenglas / mydhl-php-sdk
Unofficial PHP SDK for MyDHL REST API (DHL Express)
Requires
- php: ^8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.5
- ramsey/uuid: ^4.7
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-06 11:01:53 UTC
README
Unofficial PHP SDK for the DHL Express MyDHL REST API (currently aligned with spec 3.2.2, April 2026).
Note: Only the modern REST API is supported. The legacy SOAP API is not.
Requirements
- PHP ^8.2
- ext-json
guzzlehttp/guzzle^7.5ramsey/uuid^4.7
Installation
composer require sonnenglas/mydhl-php-sdk
Supported services
| Service | Supported |
|---|---|
| RATING | |
| Retrieve Rates for a one-piece Shipment | ✅ |
| Retrieve Rates for Multi-piece Shipments | ❌ |
| Landed Cost | ❌ |
| PRODUCT | |
| Retrieve DHL Express products | ❌ |
| SHIPMENT | |
| Create Shipment | ✅ |
| Customs / international shipments (export declaration) | ✅ |
| Re-download archived shipment documents | ✅ |
| Electronic Proof of Delivery | ✅ |
| Upload updated customs docs for shipment | ❌ |
| Upload Commercial Invoice Data for shipment | ❌ |
| TRACKING | |
| Track a single DHL Express Shipment | ✅ |
| Track multiple DHL Express Shipments (batch) | ✅ |
| PICKUP | |
| Create a DHL Express pickup booking request | ✅ |
| Cancel a DHL Express pickup booking request | ✅ |
| Update pickup information | ❌ |
| IDENTIFIER | |
| Allocate identifiers upfront | ❌ |
| ADDRESS | |
| Validate DHL Express pickup/delivery capability | ❌ |
| INVOICE | |
| Upload Commercial Invoice data | ❌ |
| SERVICE POINTS / REFERENCE DATA | |
| Look up servicepoints / reference data | ❌ |
Design
The SDK splits responsibilities between value objects (immutable, validated request payloads) and services (thin transport that talk to DHL):
RateRequest,ShipmentRequest,PickupRequest,Pickup,ExportDeclaration, … — immutable inputs, validated in their constructors.RateService::getRates(RateRequest)— returnsRate[].ShipmentService::createShipment(ShipmentRequest)— returnsShipment.TrackingService::track(...)/trackBatch(...)PickupService::book(...)/cancel(...)ImageService::getImages(...)— re-download archived customs/waybill PDFs.ProofOfDeliveryService::getProofOfDelivery(...)
Every required field is a constructor parameter, so missing data fails at request-build time, not somewhere inside the API call.
Quick start
use Sonnenglas\MyDHL\MyDHL; $myDhl = new MyDHL( username: getenv('DHL_EXPRESS_USERNAME'), password: getenv('DHL_EXPRESS_PASSWORD'), testMode: true, // false → production );
The base URLs are baked in:
| Environment | URL |
|---|---|
| Sandbox | https://express.api.dhl.com/mydhlapi/test/ |
| Production | https://express.api.dhl.com/mydhlapi/ |
Sandbox is rate-limited to 500 calls/day per credential set.
Usage
Retrieve rates
use DateTimeImmutable; use Sonnenglas\MyDHL\ValueObjects\Package; use Sonnenglas\MyDHL\ValueObjects\RateAddress; use Sonnenglas\MyDHL\ValueObjects\RateRequest; $request = new RateRequest( accountNumber: '99999999', originAddress: new RateAddress( countryCode: 'DE', postalCode: '10117', cityName: 'Berlin', ), destinationAddress: new RateAddress( countryCode: 'DE', postalCode: '20099', cityName: 'Hamburg', ), package: new Package(weight: 10, height: 20, length: 10, width: 30), shippingDate: new DateTimeImmutable('tomorrow'), ); $rates = $myDhl->getRateService()->getRates($request);
Create a domestic shipment
use DateTimeImmutable; use Sonnenglas\MyDHL\ValueObjects\Account; use Sonnenglas\MyDHL\ValueObjects\Address; use Sonnenglas\MyDHL\ValueObjects\Contact; use Sonnenglas\MyDHL\ValueObjects\Incoterm; use Sonnenglas\MyDHL\ValueObjects\Package; use Sonnenglas\MyDHL\ValueObjects\Pickup; use Sonnenglas\MyDHL\ValueObjects\ShipmentRequest; $request = new ShipmentRequest( plannedShippingDateAndTime: new DateTimeImmutable('tomorrow 14:00'), productCode: 'N', shipperAddress: new Address( addressLine1: 'Karl-Liebknecht-Straße 13', countryCode: 'DE', postalCode: '10178', cityName: 'Berlin', ), shipperContact: new Contact( phone: '+49301234567', companyName: 'Acme Lab', fullName: 'John Shipper', email: 'shipper@example.com', ), receiverAddress: new Address( addressLine1: 'Hamburger Str. 1', countryCode: 'DE', postalCode: '20099', cityName: 'Hamburg', ), receiverContact: new Contact( phone: '+49401234567', companyName: 'Acme Hamburg', fullName: 'Jane Receiver', email: 'receiver@example.com', ), accounts: [new Account(typeCode: 'shipper', number: '123456789')], packages: [new Package(weight: 5, height: 20, length: 10, width: 30)], pickup: Pickup::notRequested(), incoterm: new Incoterm('DAP'), ); $shipment = $myDhl->getShipmentService()->createShipment($request); file_put_contents('label.pdf', $shipment->getLabelPdf());
Customs / international shipments
International shipments need declaredValue, an Incoterm, an ExportDeclaration with line items, and (recommended) a VAT/EORI/IOSS RegistrationNumber:
use Sonnenglas\MyDHL\ValueObjects\CustomerReference; use Sonnenglas\MyDHL\ValueObjects\ExportDeclaration; use Sonnenglas\MyDHL\ValueObjects\Invoice; use Sonnenglas\MyDHL\ValueObjects\LineItem; use Sonnenglas\MyDHL\ValueObjects\OutputImageProperties; use Sonnenglas\MyDHL\ValueObjects\RegistrationNumber; $request = new ShipmentRequest( // …same shipper / receiver / packages as above… productCode: 'P', // EXPRESS WORLDWIDE isCustomsDeclarable: true, incoterm: new Incoterm('DAP'), shipperRegistrationNumbers: [ new RegistrationNumber( typeCode: RegistrationNumber::TYPE_VAT, number: 'DE123456789', issuerCountryCode: 'DE', ), ], customerReferences: [ new CustomerReference(value: 'PO-12345', typeCode: CustomerReference::TYPE_BUYER_ORDER), ], declaredValue: 50.0, declaredValueCurrency: 'EUR', exportDeclaration: new ExportDeclaration( lineItems: [new LineItem( number: 1, description: 'Glass jar with embedded solar panel', price: 50.0, quantityValue: 1, quantityUnit: LineItem::UNIT_PIECES, manufacturerCountry: 'DE', netWeight: 2.0, grossWeight: 2.5, exportReasonType: LineItem::REASON_PERMANENT, )], invoice: new Invoice( number: 'INV-1001', date: new DateTimeImmutable('today'), ), ), outputImageProperties: new OutputImageProperties( printerDPI: 300, encodingFormat: OutputImageProperties::ENCODING_PDF, ), );
Track a shipment
$tracked = $myDhl->getTrackingService()->track('1234567890'); if ($tracked !== null) { echo $tracked->status, "\n"; foreach ($tracked->events as $event) { echo $event->date, ' ', $event->time, ' — ', $event->description, "\n"; } } // Batch — DHL accepts hundreds of waybills per call. $tracked = $myDhl->getTrackingService()->trackBatch([ '1234567890', '0987654321', ]);
Book / cancel a courier pickup separately
Use this when the shipment was created with Pickup::notRequested() and the pickup needs to be booked (or cancelled) independently — typical when an order is cancelled hours before pickup time.
use Sonnenglas\MyDHL\ValueObjects\PickupRequest; use Sonnenglas\MyDHL\ValueObjects\PickupShipmentSummary; $booking = $myDhl->getPickupService()->book(new PickupRequest( plannedPickupDateAndTime: new DateTimeImmutable('+1 day 14:00'), accounts: [new Account('shipper', '123456789')], shipperAddress: $shipperAddress, shipperContact: $shipperContact, shipmentDetails: [new PickupShipmentSummary( productCode: 'N', isCustomsDeclarable: false, packages: [new Package(weight: 5, height: 20, length: 10, width: 30)], )], closeTime: '18:00', location: 'reception', locationType: PickupRequest::LOCATION_BUSINESS, )); $myDhl->getPickupService()->cancel( dispatchConfirmationNumber: $booking->getFirstConfirmationNumber(), requestorName: 'John Smith', reason: 'wrongdate', );
Re-download archived documents (waybill, customs invoice)
use Sonnenglas\MyDHL\Services\ImageService; $documents = $myDhl->getImageService()->getImages( shipmentTrackingNumber: '1234567890', shipperAccountNumber: '123456789', typeCodes: [ImageService::TYPE_WAYBILL, ImageService::TYPE_COMMERCIAL_INVOICE], pickupYearAndMonth: '2026-05', ); foreach ($documents as $doc) { file_put_contents("{$doc->typeCode}.pdf", $doc->content); }
/get-imagedoes not return the transport label. The label is returned inline only atcreateShipmenttime. SaveShipment::getLabelPdf()then.
Proof of Delivery
$pods = $myDhl->getProofOfDeliveryService()->getProofOfDelivery( shipmentTrackingNumber: '1234567890', shipperAccountNumber: '123456789', );
Full examples:
Development
composer install composer test # unit tests (PHPUnit) composer test:integration # live sandbox tests (require DHL_EXPRESS_* env vars) composer phpstan # PHPStan level 9 composer lint # PHP-CS-Fixer (dry run) composer lint:fix # PHP-CS-Fixer (apply fixes)
Integration tests against the DHL sandbox
Copy tests/Integration/.env.example and export your sandbox credentials, then:
DHL_EXPRESS_USERNAME=… \ DHL_EXPRESS_PASSWORD=… \ DHL_EXPRESS_ACCOUNT_NUMBER=… \ composer test:integration
Without these env vars the integration suite auto-skips, so contributor laptops and CI without secrets stay green. Each integration run consumes one or two of the daily 500 sandbox calls — keep them deliberate.
Upgrading
- From 1.x → 2.0: see
UPGRADE-1.x-to-2.0.md. Most callers only need to update theShipmentresponse getters that became nullable; international shipments need the newExportDeclaration/declaredValuearguments. - From 0.x → 1.0: the fluent setter API on services was replaced by immutable Request VOs. See the 1.0 release notes.