Error 401 when fetching DHL rates? A typical scenario. A developer spends two days debugging, only to find the issue is using the wrong product: DHL Express and DHL eCommerce use different authorization mechanisms. An in-house programmer may study the documentation for weeks but still stumble on non-obvious constraints—maximum weight 70 kg, mandatory customs declaration for international shipments, strict address validation. We have accumulated experience integrating DHL Express API on 30+ projects, from simple rate calculation to full-cycle shipment creation with tracking. The result: transparent delivery, minimal errors, satisfied customers. Order such integration—it will save time and money.
Problems We Solve
- Authorization and API differences: DHL Express (Basic Auth) and DHL eCommerce (OAuth 2.0) are different products. Using the wrong API leads to 401 errors and incorrect rates. We select the correct API and configure Basic Auth.
- Address and customs errors: Incorrect postal code or missing customs declaration are common reasons for rejections. We validate addresses via Google Maps API and automate customs declaration with HS codes.
- Error handling: DHL returns detailed errors, but they need proper handling on the site side. Our implementation throws exceptions with clear messages, reducing debugging time by 50%.
How We Integrate
We use the Repository pattern to isolate the DHL API. All requests pass through a single client that handles authorization and errors. For a multi-brand electronics store shipping to 20 countries, we integrated DHL Express API, adding customs declarations with auto-filled HS codes. Result: order processing time decreased by 40%, shipment creation error rate dropped by 70%.
Comparison of DHL Express and DHL eCommerce
| Parameter | DHL Express | DHL eCommerce |
|---|---|---|
| Authorization type | Basic Auth (API Key/Secret) | OAuth 2.0 (Client ID/Secret) |
| Purpose | Express delivery (1-3 days) | Economy delivery (5-10 days) |
| Customs | Mandatory for international | Not always |
| Tracking | Full, with events | Limited |
DHL Express Product Codes
| Code | Product | Features |
|---|---|---|
| P | DHL Express Worldwide | Main international |
| K | DHL Express 9:00 | Delivery by 9 a.m. |
| T | DHL Express 12:00 | Delivery by noon |
| Y | DHL Express Envelope | Documents in envelope |
Technical Implementation
Authorization
DHL Express API uses Basic Auth with API key and API secret:
class DhlExpressClient
{
private const BASE_URL = 'https://express.api.dhl.com/mydhlapi';
public function __construct(
private string $apiKey,
private string $apiSecret,
private bool $sandbox = false
) {
if ($sandbox) {
// Sandbox: different URL
// https://express.api.dhl.com/mydhlapi/test
}
}
public function request(string $method, string $path, array $params = []): array
{
$url = ($this->sandbox
? 'https://express.api.dhl.com/mydhlapi/test'
: self::BASE_URL) . $path;
$response = Http::withBasicAuth($this->apiKey, $this->apiSecret)
->withHeaders(['Content-Type' => 'application/json'])
->{strtolower($method)}($url, $params);
if ($response->clientError()) {
$error = $response->json();
throw new DhlApiException(
$error['detail'] ?? $error['title'] ?? 'DHL API error',
$response->status()
);
}
return $response->json();
}
}
Sandbox credentials: apiKey = demo-key, apiSecret = demo-secret for testing. Real keys are obtained from the DHL Developer Portal.
Rate Calculation and Delivery Times
public function getRates(
array $from, // ['countryCode'=>'RU','cityName'=>'Moscow','postalCode'=>'101000']
array $to, // ['countryCode'=>'DE','cityName'=>'Berlin','postalCode'=>'10115']
float $weightKg,
array $dimensions,
string $plannedShipDate
): array {
$data = $this->request('GET', '/rates', [
'accountNumber' => config('services.dhl.account_number'),
'originCountryCode' => $from['countryCode'],
'originCityName' => $from['cityName'],
'originPostalCode' => $from['postalCode'],
'destinationCountryCode' => $to['countryCode'],
'destinationCityName' => $to['cityName'],
'destinationPostalCode' => $to['postalCode'],
'weight' => $weightKg,
'length' => $dimensions['length'],
'width' => $dimensions['width'],
'height' => $dimensions['height'],
'plannedShippingDateAndTime' => $plannedShipDate . 'T10:00:00 GMT+03:00',
'isCustomsDeclarable' => true,
'unitOfMeasurement' => 'metric',
]);
return collect($data['products'] ?? [])
->map(fn($p) => [
'product_code' => $p['productCode'],
'product_name' => $p['productName'],
'currency' => $p['totalPrice'][0]['priceCurrency'],
'total_price' => $p['totalPrice'][0]['price'],
'delivery_time'=> $p['deliveryCapabilities']['deliveryTypeCode'],
'delivery_date'=> $p['deliveryCapabilities']['estimatedDeliveryDateAndTime'] ?? null,
])
->toArray();
}
Creating a Shipment
public function createShipment(Order $order): array
{
$payload = [
'plannedShippingDateAndTime' => now()->addDay()->format('Y-m-d') . 'T10:00:00 GMT+03:00',
'pickup' => [
'isRequested' => false, // false = drop-off at DHL location
],
'productCode' => $order->dhl_product_code ?? 'P',
'accounts' => [
['number' => config('services.dhl.account_number'), 'typeCode' => 'shipper'],
],
'customerDetails' => [
'shipperDetails' => [
'postalAddress' => [
'postalCode' => config('services.dhl.shipper_zip'),
'cityName' => config('services.dhl.shipper_city'),
'countryCode' => 'RU',
'addressLine1'=> config('services.dhl.shipper_address'),
],
'contactInformation' => [
'email' => config('services.dhl.contact_email'),
'phone' => config('services.dhl.contact_phone'),
'companyName' => config('services.dhl.company_name'),
'fullName' => config('services.dhl.contact_name'),
],
],
'receiverDetails' => [
'postalAddress' => [
'postalCode' => $order->shipping_zip,
'cityName' => $order->shipping_city,
'countryCode' => $order->shipping_country_code,
'addressLine1'=> $order->shipping_address,
],
'contactInformation' => [
'email' => $order->recipient_email,
'phone' => $order->recipient_phone,
'fullName' => $order->recipient_name,
],
],
],
'content' => [
'packages' => [[
'weight' => $order->total_weight_kg,
'dimensions' => [
'length' => $order->package_length,
'width' => $order->package_width,
'height' => $order->package_height,
],
]],
'isCustomsDeclarable' => $order->is_international,
'description' => 'E-commerce goods',
'incoterm' => 'DAP',
'unitOfMeasurement' => 'metric',
// Customs declaration for international shipments
'exportDeclaration' => $order->is_international ? $this->buildExportDeclaration($order) : null,
],
];
$response = $this->request('POST', '/shipments', $payload);
return [
'shipment_id' => $response['shipmentTrackingNumber'],
'shipment_number' => $response['shipmentDetails'][0]['shipmentTrackingNumber'],
'label_pdf' => base64_decode($response['documents'][0]['content'] ?? ''),
];
}
Customs Declaration
Mandatory for international shipments:
private function buildExportDeclaration(Order $order): array
{
return [
'lineItems' => $order->items->map(fn($item, $i) => [
'number' => $i + 1,
'description' => $item->product->name_en, // in English
'price' => $item->price,
'priceCurrency' => 'USD',
'grossWeight' => [
'weight' => $item->product->weight_kg,
'unitOfMeasurement' => 'kg',
],
'quantity' => [
'value' => $item->quantity,
'unitOfMeasurement' => 'PCS',
],
'manufacturerCountry' => 'CN',
'hsCode' => $item->product->hs_code ?? '6109100000',
])->toArray(),
'invoice' => [
'number' => 'INV-' . $order->id,
'date' => now()->format('Y-m-d'),
'signedBy' => config('services.dhl.contact_name'),
'function' => 'Seller',
'customerReference' => (string)$order->id,
],
'exportReason' => 'PERMANENT',
'exportReasonType'=> 'PERMANENT',
'placeOfIncoterm' => 'Destination',
'shipmentType' => 'commercial',
];
}
Tracking
public function trackShipment(string $trackingNumber): array
{
$response = $this->request('GET', '/tracking', [
'trackingNumber' => $trackingNumber,
]);
$shipment = $response['shipments'][0] ?? null;
if (!$shipment) {
return [];
}
return [
'status' => $shipment['status'],
'description' => $shipment['description'],
'location' => $shipment['location']['address']['cityName'] ?? '',
'events' => collect($shipment['events'])->map(fn($e) => [
'timestamp' => $e['timestamp'],
'location' => $e['location']['address']['cityName'] ?? '',
'description'=> $e['description'],
])->toArray(),
'estimated_delivery' => $shipment['estimatedTimeOfDelivery'] ?? null,
];
}
Limitations and Common Errors
DHL rigorously checks recipient addresses. An inaccurate postal code returns an error. Maximum weight per package is 70 kg, and maximum side length is 300 cm. A typical mistake is using an invalid account number. We validate addresses via Google Maps API before submission. Always test each function in the sandbox.
How to Automate Customs Declaration?
Customs declaration is mandatory for all DHL Express international shipments. We automate its filling: HS codes are pulled from the product database; description and value are generated based on the order. This eliminates manual entry and reduces error risk. In the sandbox, test the declaration filling before production.
What Is Included in the Work?
- API integration documentation.
- Access keys for sandbox and production.
- Operator training on order handling.
- Technical support for 3 months after deployment.
- Guarantee of correct functionality for all features.
Process
- Analysis: examine product range, destinations, order volume.
- Design: choose DHL product, shipment scheme.
- Implementation: integrate API, configure customs declarations.
- Testing: in sandbox with real keys.
- Deployment: go live on production environment.
Timeline
Integration of DHL Express for an e-commerce store: from 5 to 7 working days. Additional customs configuration: another 2–3 days.
How to Avoid Authorization Errors?
Check that you are using the correct API: DHL Express (Basic Auth) or DHL eCommerce (OAuth 2.0). Ensure the request includes the correct apiKey and apiSecret. In sandbox, use demo-key and demo-secret. For production, use keys from your DHL Developer account.
Contact us to discuss DHL API integration for your project. We guarantee correct operation and provide support. Order integration today.







