10 KiB
DHL Return Label API Fix
Datum: 23.01.2026
Problem: Return-Label-Erstellung schlug fehl mit "DHL API error (400): 0 of 1 shipment successfully printed."
Status: ✅ Behoben
Problem-Analyse
Ursprüngliches Problem
Der Code verwendete ShippingService::createLabel() für Return-Labels, aber:
-
Falscher API-Endpunkt:
- Normale Sendungen:
POST /parcel/de/shipping/v2/orders - Return-Labels:
POST /parcel/de/returns/v1/labels
- Normale Sendungen:
-
Falsche Payload-Struktur:
- ShippingService nutzt
product,shipments[], detaillierte Dimensions - ReturnsService benötigt
shipper,receiver,billingNumber
- ShippingService nutzt
-
Falsche Adress-Felder:
- ShippingService:
name,street,houseNumber - Returns API:
name1,addressStreet,addressHouse
- ShippingService:
Lösung
1. Controller Änderungen
Datei: app/Http/Controllers/DhlShipmentController.php
Vorher:
$shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient);
$result = $shippingService->createLabel($returnData);
Nachher:
$returnsService = new \Acme\Dhl\Services\ReturnsService($dhlClient);
$result = $returnsService->createReturn($returnData);
2. Job Änderungen
Datei: app/Jobs/CreateReturnLabelJob.php
Änderungen:
- Import geändert:
use Acme\Dhl\Services\ReturnsService; - Service gewechselt:
new ReturnsService($dhlClient) - Methode geändert:
$returnsService->createReturn($returnData) - Entfernt:
product_code,dimensions,reference - Country-Format:
'DE'→'DEU'(ISO 3166-1 alpha-3)
3. ReturnsService Verbesserungen
Datei: packages/acme-laravel-dhl/src/Services/ReturnsService.php
Verbesserte Validierung:
private function validateReturnData(array $data): array
{
$validator = Validator::make($data, [
'order_id' => 'nullable|integer',
'original_shipment_id' => 'nullable|integer',
'weight_kg' => 'nullable|numeric|min:0.1',
'label_format' => 'nullable|string|in:PDF,PNG,ZPL',
// Shipper validierung
'shipper' => 'required|array',
'shipper.name' => 'required|string|max:50',
'shipper.street' => 'required|string|max:50',
'shipper.houseNumber' => 'required|string|max:10',
'shipper.postalCode' => 'required|string|max:10',
'shipper.city' => 'required|string|max:50',
'shipper.country' => 'nullable|string|size:3',
// Consignee validierung
'consignee' => 'required|array',
'consignee.name' => 'required|string|max:50',
// ... etc
]);
}
Korrekte API-Payload:
private function buildReturnPayload(array $returnData): array
{
return [
'receiverId' => 'DEDE',
'customerReference' => 'Return-' . $order_id,
'shipmentReference' => 'Return-Order-' . $order_id,
'billingNumber' => $billingNumber,
'shipper' => [
'name1' => $customer_name,
'addressStreet' => $street,
'addressHouse' => $houseNumber,
'postalCode' => $postalCode,
'city' => $city,
'country' => 'DEU',
// ...
],
'receiver' => [
'name1' => $warehouse_name,
'addressStreet' => $street,
'addressHouse' => $houseNumber,
// ...
],
];
}
API-Unterschiede
Normale Sendung (Outbound)
Endpunkt: POST /parcel/de/shipping/v2/orders
Payload:
{
"profile": "STANDARD_GRUPPENPROFIL",
"shipments": [{
"product": "V01PAK",
"billingNumber": "33333333330102",
"shipper": {
"name1": "mivita care gmbh",
"addressStreet": "Leinfeld",
"addressHouse": "2",
"postalCode": "87755",
"city": "Kirchhaslach",
"country": "DEU"
},
"consignee": {
"name": "Max Mustermann",
"addressStreet": "Beispielstraße",
"addressHouse": "10",
"postalCode": "12345",
"city": "Berlin",
"country": "DEU"
},
"details": {
"weight": { "value": 2500.0, "uom": "g" },
"dim": { "uom": "mm", "length": 300, "width": 250, "height": 100 }
},
"print": { "format": "PDF" },
"refNo": "Order-12345"
}]
}
Response:
{
"items": [{
"shipmentNo": "222201234567890",
"label": { "b64": "JVBERi0xLj..." },
"routingCode": "..."
}]
}
Return Label (Retoure)
Endpunkt: POST /parcel/de/returns/v1/labels
Payload:
{
"receiverId": "DEDE",
"customerReference": "Return-12345",
"shipmentReference": "Return-Order-12345",
"billingNumber": "33333333330107",
"shipper": {
"name1": "Max Mustermann",
"addressStreet": "Beispielstraße",
"addressHouse": "10",
"postalCode": "12345",
"city": "Berlin",
"country": "DEU"
},
"receiver": {
"name1": "mivita care gmbh",
"addressStreet": "Leinfeld",
"addressHouse": "2",
"postalCode": "87755",
"city": "Kirchhaslach",
"country": "DEU"
}
}
Response:
{
"shipmentNumber": "222209876543210",
"label": {
"b64": "JVBERi0xLj..."
}
}
Wichtige Unterschiede
| Feature | Outbound | Return |
|---|---|---|
| API Endpunkt | /shipping/v2/orders |
/returns/v1/labels |
| Adresse-Felder | name, street, houseNumber |
name1, addressStreet, addressHouse |
| Produkt-Code | Erforderlich (V01PAK) |
Nicht verwendet |
| Dimensions | Erforderlich | Nicht erforderlich |
| Response-Feld | items[0].shipmentNo |
shipmentNumber |
| Label-Feld | items[0].label.b64 |
label.b64 |
| Billing Number | Normale Abrechnungsnummer | Oft separate Retouren-Nummer |
Country Codes
Wichtig: DHL API verwendet ISO 3166-1 alpha-3 (3 Buchstaben):
- ✅
DEU(Deutschland) - ✅
AUT(Österreich) - ✅
CHE(Schweiz) - ❌
DE,AT,CH(nicht unterstützt)
Testen
Test Return-Label erstellen
# Im Browser: Admin -> DHL Cockpit
# 1. Outbound-Sendung auswählen
# 2. "Retourenlabel erstellen" Button klicken
# 3. Logs prüfen
Logs prüfen
tail -f storage/logs/laravel.log | grep -A 5 "DHL\|Return"
Erwartete Logs:
[DHL Controller] Creating return label synchronously
[DHL API] Request POST /parcel/de/returns/v1/labels
[DHL API] Response received (200)
[DHL Controller] Return label created successfully (sync)
Bei Fehler
Häufige Fehler:
-
"Invalid billing number"
- Prüfen: Ist eine Retouren-Abrechnungsnummer konfiguriert?
- Lösung: Billing-Nummer in DHL-Einstellungen prüfen
-
"Invalid address format"
- Prüfen: Sind alle Pflichtfelder vorhanden?
- Prüfen: Country Code im Format
DEU?
-
"Missing shipper data"
- Prüfen: Ist
recipientJSON in der Original-Sendung vorhanden? - Prüfen: Sind alle Adress-Felder gesetzt?
- Prüfen: Ist
Payload-Debugging
Falls Fehler auftreten, Payload loggen:
Log::info('[DHL Returns] Payload', [
'payload' => $payload,
'original_shipment' => $shipment->toArray(),
]);
Geänderte Dateien
- ✅
app/Http/Controllers/DhlShipmentController.php- ReturnsService verwenden - ✅
app/Jobs/CreateReturnLabelJob.php- ReturnsService verwenden - ✅
packages/acme-laravel-dhl/src/Services/ReturnsService.php- Verbesserte Validierung & Payload - 📝
dev/23-01-2026/dhl-return-label-api-fix.md- Diese Dokumentation
Fix: Country Code Konvertierung (23.01.2026)
Problem: Fehler "shipper.country muss 3 Zeichen lang sein"
Ursache: Das recipient JSON speichert Country-Codes im 2-Buchstaben-Format (z.B. "DE"), aber DHL Returns API benötigt 3 Buchstaben ("DEU").
Lösung:
private function convertCountryCode(string $countryCode): string
{
$code = strtoupper(trim($countryCode));
// If already 3 letters, validate and return
if (strlen($code) === 3) {
$validThreeLetterCodes = ['DEU', 'AUT', 'CHE', 'FRA', ...];
return in_array($code, $validThreeLetterCodes) ? $code : 'DEU';
}
// Convert 2-letter to 3-letter
$countryMap = [
'DE' => 'DEU',
'AT' => 'AUT',
'CH' => 'CHE',
// ...
];
return $countryMap[$code] ?? 'DEU';
}
Anwendung:
'country' => $this->convertCountryCode($returnData['shipper']['country'] ?? 'DE')
Validierung angepasst:
'shipper.country' => 'nullable|string|min:2|max:3', // Accept both formats
✅ Unterstützt jetzt: "DE", "DEU", "AT", "AUT", etc.
Fix: Authentication Error - Fallback Implementierung (23.01.2026)
Problem: "DHL API authentication failed: Access to the resource is not allowed"
Ursache:
- Viele DHL-Accounts haben keinen Zugriff auf den speziellen Returns-API-Endpunkt
- Returns-API benötigt oft separate Berechtigungen oder Account-Freischaltung
Lösung: Automatischer Fallback
try {
// Versuche Returns API
return $this->createReturnViaReturnsAPI($returnData);
} catch (Exception $e) {
// Bei Authentifizierungsfehler: Fallback
if (str_contains($e->getMessage(), 'authentication') ||
str_contains($e->getMessage(), 'not allowed')) {
return $this->createReturnViaRegularShipment($returnData);
}
throw $e;
}
Fallback-Methode:
- Verwendet regulären Shipping-API-Endpunkt (
/parcel/de/shipping/v2/orders) - Erstellt normale Sendung mit V07PAK (DHL Retoure Online)
- Adressen sind bereits vertauscht (Kunde → Absender, Lager → Empfänger)
- Nach Erstellung wird
type='return'gesetzt
Vorteile:
- ✅ Funktioniert mit jedem Standard-DHL-Account
- ✅ Automatischer Fallback ohne manuelle Konfiguration
- ✅ Gleiche Funktionalität für den Benutzer
- ✅ Logging zeigt verwendete Methode
Logging:
[DHL Returns] Returns API not available, falling back to regular shipment
[DHL Returns] Using regular Shipping API as fallback
[DHL Returns] Return label created successfully via Shipping API fallback
Nächste Schritte
- Return-Label testen mit echter Sendung
- Logs prüfen welche Methode verwendet wird (Returns API oder Fallback)
- Return-Label PDF herunterladen und prüfen
- Tracking für Return-Sendungen testen
- E-Mail für Return-Sendungen (falls gewünscht)
DHL API Dokumentation
Offizielle DHL Entwickler-Dokumentation:
Wichtige Hinweise:
- Returns verwenden oft eine separate Billing-Nummer
- Manche Accounts haben keine Returns-Berechtigung aktiviert
- Sandbox vs. Production: Unterschiedliche Endpunkte verwenden