Skip to content

Developer notes

Everything the add-on sends to DHL comes from the official OpenAPI file “DHL Express APIs (MyDHL API)” version 3.4.1 (x-release-date: 2026-11-01, published on developer.dhl.com, page “DHL Express - MyDHL API”), and from the ExpressReferenceData 3.4.1 workbook of the same page (product codes, service codes, tracking event codes, label templates). A copy of the specification is kept in the repository’s test folder; the unit tests record every request the add-on builds and check it against that file (node tests/check-requests.js), and the mock server used by the end-to-end tests rejects any request that does not follow it.

The core DHL module of CS-Cart (app/Tygh/Shippings/Services/Dhl.php, still the same in 4.21.2) posts an XML DCTRequest to https://xmlpi-ea.dhl.com/XMLShippingServlet (the legacy XML-PI interface). DHL Express has deprecated XML-PI in favour of the MyDHL API; integrators report the deprecation for non-enterprise accounts from 2025-06-03 (sources: research/sources/dhl-xmlpi-deprecation.md), so the core module cannot be relied on for rates. This add-on registers its own carrier module kv_dhl_express; the core dhl services and their shipping methods are not touched.

Item Value (source: OpenAPI servers, user guide “Environments” / “Authentication”)
Test https://express.api.dhl.com/mydhlapi/test (daily limit of 500 calls per credential)
Production https://express.api.dhl.com/mydhlapi
Authentication HTTP Basic (securitySchemes.basicAuth), sent pre-emptively; user name and password are issued by DHL Express
x-version header 3.4.1 on every call (parameter xVersionHeader, “do not change this field value”)
Message-Reference random UUID per call (1-36 characters)
3PV headers Plugin-Name: Kavio DHL Express (max 20), Plugin-Version (max 15), Webstore-Platform-Name: CS-Cart, Webstore-Platform-Version
Errors body instance, title, detail (often NNNN: text), message, additionalDetails[], status; the add-on shows detail + additionalDetails
Operation (operationId) Used for Fields the add-on sends
GET /address-validate (exp-api-address-validate) “Test connection” type=pickup, countryCode, postalCode, cityName of the ship-from address
POST /rates (exp-api-rates-many) rates at checkout customerDetails.shipperDetails / receiverDetails (postalCode, cityName, countryCode, provinceCode for US/CA/AU/BR/MX/IN/CN/AR, addressLine1), accounts[{typeCode: shipper, number}] (no productCode: one answer with all products), plannedShippingDateAndTime, unitOfMeasurement: metric, isCustomsDeclarable, monetaryAmount[declaredValue / insuredValue], valueAddedServices[II] (insurance setting), nextBusinessDay: true, estimatedDeliveryDate {isRequested, typeCode: QDDC}, packages[{weight, dimensions{length, width, height}}]
POST /shipments (exp-api-shipments) label plannedShippingDateAndTime, pickup {isRequested, closeTime, location}, productCode, accounts (shipper, plus duties-taxes for DDP), valueAddedServices (WY paperless trade, DD duty tax paid for DDP, II insurance), outputImageProperties {printerDPI 300, encodingFormat pdf/zpl, imageOptions[label ECOM26_84_A4_001 / ECOM26_84_001 / ECOM26_64_001, invoice COMMERCIAL_INVOICE_P_10 (commercial, eng), waybillDoc ARCH_8X4_A4_002 / ARCH_8X4], splitTransportAndWaybillDocLabels, allDocumentsInOneImage, splitDocumentsByPages, splitInvoiceAndReceipt}, customerReferences[{value, typeCode: CU}], customerDetails.shipperDetails / receiverDetails (postalAddress with addressLine1..3 of max 45, contactInformation {phone, companyName, fullName, email}, registrationNumbers[VAT, EOR] for the shipper, typeCode business/private), content {packages[{weight, dimensions, customerReferences, description}], isCustomsDeclarable, declaredValue, declaredValueCurrency, exportDeclarations[{lineItems[{number, description, price, quantity{value, unitOfMeasurement: PCS}, commodityCodes[{typeCode: outbound, value}], exportReasonType, manufacturerCountry, weight{netValue, grossValue}}], invoice{number, date, totalNetWeight, totalGrossWeight}, exportReasonType, shipmentType: commercial, incoterm}], description, incoterm, unitOfMeasurement: metric}, estimatedDeliveryDate, shipmentNotification[{typeCode: email, receiverId, languageCode: eng}] (only with the setting); query bypassPLTError=true when paperless trade may fall back to paper
GET /shipments/{shipmentTrackingNumber}/tracking (exp-api-shipments-tracking) tracking trackingView=all-checkpoints, levelOfDetail=shipment; reads shipments[0].events[] (date, time, typeCode, description, serviceArea[0].description) and estimatedDeliveryDate
GET /tracking (exp-api-shipments-tracking-multi) lost-answer recovery shipmentReference, shipmentReferenceType=CU, shipperAccountNumber, dateRangeFrom, dateRangeTo, trackingView=shipment-details-only, levelOfDetail=shipment; 404 “No Shipments Found for ReferenceID” = nothing at DHL
GET /shipments/{shipmentTrackingNumber}/get-image (exp-api-shipments-documentimage) waybill of an adopted shipment shipperAccountNumber, typeCode=waybill, pickupYearAndMonth, encodingFormat=pdf, allInOnePDF=true
POST /pickups (exp-api-pickups) courier pickup plannedPickupDateAndTime, closeTime, location, locationType: business, accounts, specialInstructions[{value, typeCode}], customerDetails.shipperDetails, shipmentDetails[{productCode, isCustomsDeclarable, unitOfMeasurement, packages}]; reads dispatchConfirmationNumbers[0], readyByTime
DELETE /pickups/{dispatchConfirmationNumber} (exp-api-pickups-cancel) cancel a pickup required query requestorName, reason; 404 “Pickup not found” is treated as cancelled
POST /landed-cost (exp-api-landed-cost) duties and taxes estimate customerDetails, accounts, productCode, unitOfMeasurement, currencyCode, isCustomsDeclarable, getCostBreakdown: true, charges[freight], shipmentPurpose: commercial, transportationMode: air, merchantSelectedCarrierName: DHL, packages, items[{number, name, manufacturerCountry, quantity, quantityType: prt, unitPrice, unitPriceCurrencyCode, commodityCode}]; reads products[0].detailedPriceBreakdown[].breakdown[] (typeCode DUTY / TAX / FEE, TOTAL ... lines) and warnings[]

Not used: GET /rates (one-piece variant), GET /products, upload of invoice data or images, add-piece, proof of delivery, service points, identifiers and early shipment screening.

  • content.description and lineItems[].description: “only printable ASCII characters are supported”: the add-on transliterates (umlauts, accents, Turkish and Nordic letters) and drops everything else; addresses and names keep UTF-8.
  • Address lines are cut into up to three lines of 45 characters at word boundaries, never inside a UTF-8 character; other texts are cut with mb_substr.
  • Weight is sent in kg (from the store’s “Weight symbol” grams), sizes in cm (product boxes in the unit chosen in the settings). Products without box sizes use the default box (MyDHL needs the dimensions of every piece).
  • plannedShippingDateAndTime is “the local time of the shipment origin coupled with corresponding time zone”: the add-on uses the ship-from country’s time zone (the shop’s zone when it lies in that country), the format of the examples (2026-11-10T10:10:00 GMT+01:00, max 29), and moves to the next working day after the cut-off hour and at weekends.
  • Customs: a shipment is declarable when it crosses the EU customs border (or any border outside the EU), including the special territories (Canary Islands, Ceuta, Melilla, Helgoland, Buesingen, Livigno, French overseas departments, Mount Athos). Document products are never declarable. HS code and country of origin per product come from the Kavio Labs “Cross-Border Duties” add-on when it is installed (read only), else from the settings.
  • Prices: totalPrice[] carries one entry per currencyType (BILLC billing, PULCL public rates, BASEC base). An entry in the store currency wins; otherwise the billing price is converted with the store’s own currency rates. A currency the store does not know hides the rate (logged).
  • Product codes are the global codes of the reference workbook (sheet productCode); “best available” picks the cheapest product of the answer at checkout and asks the merchant to choose one when the label is created.
  • Tracking status: OK = delivered; BA CA CD CM DD DS HP MC MD MS ND NH OH RD RT SS UD = needs attention; only SD MF EM IA = label created; anything else = in transit. Delivered is final.
  • Tracking link: https://www.dhl.com/global-en/home/tracking/tracking-express.html?submit=1&tracking-id=<waybill> (DHL’s public page; the trackingUrl of the API answer needs credentials and is not shown to customers).

MyDHL has no idempotency key and no “cancel shipment” operation. Each label attempt:

  1. takes a per-order database lock (GET_LOCK);
  2. writes a row in state W (pending) BEFORE the call and builds the reference ORDER-<order id>-<row id>, sent as shipment and piece customerReferences (type CU);
  3. on success stores the waybill and documents (state C);
  4. on a 4xx rejection deletes the pending row and logs DHL’s message;
  5. on a timeout, a dropped connection or a 5xx marks the row U (unknown), never resends, and searches DHL for the reference (GET /tracking). Found: the waybill is adopted (waybill image fetched with get-image; the label itself is reprinted in MyDHL). Not found: the merchant searches again later, enters the waybill number from MyDHL (verified with the tracking call) or confirms that nothing was created; only then a new label (with a new reference) is possible.

A queued label job for an order with an unknown attempt is stopped at once (not resent). Pickup bookings use the same pending row and lock per account; a lost answer becomes “unknown” and the merchant enters the confirmation number from MyDHL or closes it.

Table Content
?:kv_dhl_express_accounts one row per account (0 = owner, storefront id in CS-Cart, vendor id in Multi-Vendor); API passwords sealed with AES-256-GCM
?:kv_dhl_express_shipments attempts, waybill, reference, product, documents (base64), pieces, last 20 tracking events, pickup number
?:kv_dhl_express_pickups pickup bookings
?:kv_dhl_express_jobs, ?:kv_dhl_express_log queue and log (masked; rate and landed cost answers are never stored)

The MyDHL legal terms forbid storing rating data and building a database from landed cost data: rates live only in memory for one page view (one call per package without a product code serves every DHL method of the package; 8 s timeout at checkout) and landed cost estimates are rendered once.

  • PHP hooks: change_order_status_post (label at an order status), init_templater_post (upgrade synchronizer, only while pending).
  • Shipping service class Tygh\Shippings\Services\KvDhlExpress (core realtime services); getInfo() gives core the tracking link.
  • Template hooks: orders:tabs_extra (DHL Express tab), add-on settings info blocks; shipping method Configure tab views/shippings/components/services/kv_dhl_express.tpl.
  • Test override: $config['kv_dhl_express_base_url'] in config.local.php points the client at a local mock (development only).

In owner mode the marketplace owner’s DHL account serves every vendor order, but the parcel leaves from the vendor: rates, labels and pickups of a vendor order use the vendor’s company address and tax number, not the owner’s ship-from fields.

CS-Cart (Ultimate): account 0 plus an optional account per storefront. Multi-Vendor: marketplace owner mode (one account; vendors view labels and tracking only) or one account per vendor (vendor panel Connection page; vendors reach only their own orders, shipments and pickups; the job queue is admin only).