Интеграция службы доставки DHL на сайт
DHL — глобальный логистический оператор, ключевой выбор для международной доставки. API DHL Express (для срочных отправлений) и DHL eCommerce (для интернет-торговли) — разные продукты с разными endpoint'ами и авторизацией. Для российского интернет-магазина с международными отправками чаще используют DHL Express API.
Авторизация
DHL Express API использует Basic Auth с API key и 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: другой 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 — реальные тестовые ключи получают при регистрации в Developer Portal DHL.
Расчёт стоимости и сроков
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();
}
Основные продукт-коды: P — DHL Express Worldwide (самый востребованный), K — DHL Express 9:00, T — DHL Express 12:00, Y — DHL Express Envelope.
Создание отправления
public function createShipment(Order $order): array
{
$payload = [
'plannedShippingDateAndTime' => now()->addDay()->format('Y-m-d') . 'T10:00:00 GMT+03:00',
'pickup' => [
'isRequested' => false, // false = самостоятельная сдача на склад DHL
],
'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',
// Таможенная декларация для международных отправлений
'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'] ?? ''),
];
}
Таможенная декларация
Для международных отправлений обязательна:
private function buildExportDeclaration(Order $order): array
{
return [
'lineItems' => $order->items->map(fn($item, $i) => [
'number' => $i + 1,
'description' => $item->product->name_en, // на английском
'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',
];
}
Отслеживание
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,
];
}
Вызов курьера
public function schedulePickup(
string $date,
string $timeFrom,
string $timeTo,
int $parcelCount,
float $totalWeight
): string {
$response = $this->request('POST', '/pickups', [
'plannedPickupDateAndTime' => $date . 'T' . $timeFrom . ':00 GMT+03:00',
'closeTime' => $timeTo,
'location' => 'reception',
'accounts' => [['number' => config('services.dhl.account_number'), 'typeCode' => 'shipper']],
'customerDetails' => ['shipperDetails' => [/* адрес */]],
'shipmentDetails' => [[
'packages' => [['weight' => ['netValue' => $totalWeight, 'grossValue' => $totalWeight]]],
'productCode'=> 'P',
'isCustomsDeclarable' => false,
]],
'pickupDetails' => [
'pickupRequestorDetails' => [
'fullName'=> config('services.dhl.contact_name'),
'phone' => config('services.dhl.contact_phone'),
],
],
]);
return $response['dispatchConfirmationNumber'];
}
Ограничения и ошибки
DHL строго проверяет адреса получателей. Неточный или несуществующий почтовый индекс вернёт ошибку ещё на этапе создания отправления, а не после сдачи посылки. Для ряда стран обязателен точный штат/провинция (stateOrProvince). Максимальный вес одного места — 70 кг, максимальный размер стороны — 300 см.
Сроки
Интеграция DHL Express для интернет-магазина — расчёт, создание отправлений, отслеживание — 5–7 дней. Добавление таможенных деклараций для международных отправлений — ещё 2–3 дня. Полное тестирование в sandbox среде перед боевым запуском — обязательно.







