How to Create a DHL Express Label with MyDHL API
Step-by-step guide to DHL Express's MyDHL API: register an app, authenticate with API key/secret, and generate your first sandbox shipment label.
Why MyDHL API isn't DHL Parcel DE or the retired XML API
MyDHL API is DHL Express's REST interface for time-definite international shipments, and it authenticates differently than almost every other DHL product you'll touch. If you've built against DHL Parcel DE or DHL Global Forwarding first, you'll walk into this expecting an OAuth2 token exchange. You won't get one. That single assumption is where most first integrations stall.
DHL runs several distinct API families under one developer portal, and they don't share an auth model. MyDHL API (DHL Express) covers rating, shipping, pickups and value-added services for DHL Express accounts, built as a single API based on industry standards developers already know. DHL Parcel DE, by contrast, runs on an OAuth2 resource-owner-password-credentials flow — the access token from the Authentication API must be sent as a bearer token in the Authorization header — and DHL is actively deprecating Basic Auth there: DHL will no longer offer Basic Auth in future API versions and strongly recommends OAuth2. DHL Global Forwarding's Shipment Label API is OAuth2-only too: you must have an eligible DHL Global Forwarding shipment ID, housebill number and a valid oAuth2 access token to use the API.
Then there's the legacy XML/SOAP path, which is the one most old blog posts and forum answers still reference. AfterShip's own credential guide is explicit about where that's headed: DHL Express is migrating the legacy XML API integration to MyDHL API integration, and the old Site ID/Password pair is labelled Legacy, XML API, deprecated in late 2024. If you're following a guide that mentions a Site ID, you're on a path DHL is actively walking customers off. This post covers the REST/JSON MyDHL API only, which is also what DHL's own docs point you to: the API has been designed for use by developers who need basic knowledge of REST APIs, JSON, and HTTPS.
If you're running more than one carrier and don't want to track per-vendor auth quirks like this one, that's exactly the gap platforms such as Cargoson, nShift, EasyPost and ShipEngine exist to paper over — more on that trade-off later.
What you need before you start
Short version: a free developer.dhl.com account, a 9-digit DHL Express account number (sandbox or real), and a tool that can send Basic Auth headers. Nothing here requires a sales call to start testing.
Concretely, line up the following before you open a terminal:
- A registered account on developer.dhl.com — registration is self-serve and only needs a name and email; you'll get a confirmation email and set a password before anything else works.
- A DHL Express account number. If you already have a live DHL Express business account, use that 9-digit number. If not, several integration guides note the sandbox accepts a test account number in the documentation, or any placeholder number since the sandbox accepts placeholder values — treat that as a starting point to confirm, not a guarantee, since DHL's own docs don't formally commit to a universal test number.
- An app created under "My Apps" with DHL Express - MyDHL API enabled as the subscribed product.
- Basic REST/JSON/HTTPS familiarity — this is DHL's own stated bar, not ours: the API has been designed for use by developers and requires basic knowledge of REST APIs, JSON, and HTTPS.
- A tool capable of sending HTTP Basic Auth headers (curl, Postman, Insomnia) and a way to decode base64 and render PDF/ZPL output.
You'll know you're ready when your app's Credentials panel shows an API Key and API Secret in plain view — not a Client ID/Client Secret pair, and not an OAuth scopes list. That naming is DHL's own tell that this product runs on Basic Auth, not a token exchange.
Step-by-step: from registration to first label
This is the full path from zero to a rendered label in sandbox. Each step assumes the environment is MyDHL API's test base path, not production.
- Register and verify your email. Create an account at developer.dhl.com. You'll receive a verification email before you can log in and manage apps.
- Create an app and request access to DHL Express - MyDHL API. Several carrier-API walkthroughs describe the same sequence: click APIs from the top navigation, check Shipping in the side menu, then click the DHL Express - MyDHL API tile, then click Get Access. Fill in your account number on the onboarding form — DHL Express Account Number: enter your 9-digit DHL Account Number.
- Open the app's Credentials panel and reveal the API Key and API Secret. This is the step that trips people up. These two values are your Basic Auth username and password — not an OAuth client ID/secret pair you exchange for a token. One integration guide instructs plainly: click Show Key next to API Key and API Secret to reveal the credentials — these are the credentials you need to integrate your DHL account.
- Confirm your base path. DHL's own reference lists two environments for MyDHL API: https://express.api.dhl.com/mydhlapi/test as the test environment to test your development against, and https://express.api.dhl.com/mydhlapi as the production environment to create real transactions. Point every test call at the `/test` path. Mixing this up is the second most common failure mode here, covered below.
- Run a RATING call first. DHL explicitly recommends this sequencing: DHL Express recommends using the Rating service first to see all available products and value added services, then passing the desired product and value-added service into the Shipment service. A successful 200 here confirms your credentials and account number resolve before you touch shipment creation at all.
- Build the shipment request body. Based on the MyDHL API's own object model (confirmed against the myorb/dhl-express-js client), the core fields are
plannedShippingDateAndTime,pickup.isRequested,productCode, anaccounts[]array,customerDetails.shipperDetails.postalAddress,customerDetails.receiverDetails, pluscontent.incoterm,content.unitOfMeasurement, and package weight/dimensions. - POST to the shipments endpoint with HTTP Basic Auth. DHL's own documentation is unambiguous about the auth mechanics: your DHL Express consultant will provide access credentials to the MyDHL API, and you must ensure the Authorization header is set pre-emptively following the BasicAuth standards. In practice that means base64-encoding
API_KEY:API_SECRETand sending it asAuthorization: Basic <encoded-value>. - Parse the response for the tracking number and label document. A successful shipment creation call returns a
shipmentTrackingNumberand adocuments[]array containing the base64-encoded label. - Decode and render the label, then cross-check the barcode. Before wiring this into an order management system, confirm the rendered barcode's tracking number matches the
shipmentTrackingNumberin the JSON response. A mismatch here usually means a stale cached response or a race condition in your own retry logic, not a DHL bug.
A minimal curl skeleton looks like this against the test environment:
curl -X POST "https://express.api.dhl.com/mydhlapi/test/shipments" \
-H "Authorization: Basic $(echo -n 'API_KEY:API_SECRET' | base64)" \
-H "Content-Type: application/json" \
-d '{
"plannedShippingDateAndTime": "2026-10-23T10:00:00GMT+01:00",
"pickup": { "isRequested": false },
"productCode": "N",
"accounts": [{ "typeCode": "shipper", "number": "123456789" }],
"customerDetails": {
"shipperDetails": {
"postalAddress": {
"postalCode": "10178",
"cityName": "Berlin",
"countryCode": "DE",
"addressLine1": "Karl-Liebknecht-Strasse 13"
}
},
"receiverDetails": {
"postalAddress": {
"postalCode": "20099",
"cityName": "Hamburg",
"countryCode": "DE",
"addressLine1": "Hamburger Str. 1"
}
}
},
"content": {
"incoterm": "DAP",
"unitOfMeasurement": "metric"
}
}'Note the package of key/account/address fields closely follows patterns visible across third-party SDK wrappers like myorb/dhl-express-js, which expose the same `plannedShippingDateAndTime`, `pickup`, `productCode` and `customerDetails` structure as native TypeScript types.
How you know it worked
A clean run produces a 2xx HTTP status, a populated shipmentTrackingNumber in the response body, and a documents[] array with at least one label document encoded as base64. Decode that payload and you should get a legible PDF or ZPL file.
Three concrete checks before you trust the integration:
- The decoded label's barcode, when scanned or visually read, matches the tracking number in the JSON response exactly.
- A second, identical RATING call (same origin, destination, weight, date) returns a consistent product code and price. If it doesn't, your account entitlement or product eligibility is the problem, not your shipment payload.
- The response's account number field echoes back the same 9-digit number you submitted — a mismatch here usually means you're authenticated against the wrong account in a multi-account setup.
Failure mode: the "it worked in Postman but not in code" Basic Auth trap
This is the single most common failure teams report when moving from manual testing to code, and it's almost always the same root cause: someone wrote an Authorization header assuming OAuth, because every other recent DHL API they've touched uses it.
Here's the pattern. A developer gets MyDHL API working in Postman by hand-typing a Basic Auth username/password. Then they move the logic into application code, and because they've also integrated DHL Parcel DE or DHL Global Forwarding — both OAuth2 — they copy the bearer-token pattern: fetch a token from an authentication endpoint, cache it, attach it as Authorization: Bearer <token>. MyDHL API has no such token endpoint to call. There is no DHL Express "Authentication API" equivalent to the one Parcel DE uses, where the Parcel DE Authentication API allows you to obtain a token to access a number of Parcel Germany APIs used for API access. MyDHL API's credentials are consumed directly, every request, as Basic Auth.
To diagnose this, separate auth failures from payload failures:
- If you get a 401 with a generic authentication error, re-run the exact same RATING call that worked in Postman. If it fails the same way from your code, the Authorization header construction is wrong — check for a stray Bearer prefix, wrong base64 encoding, or credentials pulled from the wrong environment variable.
- If you get a 400 instead, your auth is fine and the problem is schema — a missing required field, wrong enum value for
productCode, or a malformed date string inplannedShippingDateAndTime. - Fix: confirm the header is literally
Authorization: Basic base64(API_KEY:API_SECRET)and that no token-fetch step exists anywhere in the call chain for MyDHL API specifically.
The second common failure in this same family is a base path mismatch. DHL Express maintains separate test and production base paths — https://express.api.dhl.com/mydhlapi/test for test and https://express.api.dhl.com/mydhlapi for production — and because both are valid, reachable endpoints with the same auth scheme, a hardcoded production path in a sandbox test harness won't throw an obvious error. It will just silently try to create a real shipment against a real account, or return confusing account-not-found errors if your sandbox credentials aren't provisioned for production. Also worth knowing: the DHL Express test environment provides a daily limit of 500 service invocations for your access credentials, so a retry loop with no backoff can burn through your daily quota quickly during testing.
Where MyDHL fits next to DHL's other APIs — and the multi-carrier alternative
If you're only integrating DHL Express, MyDHL API's Basic Auth model is a minor one-time adjustment. If you're integrating DHL alongside other carriers, or multiple DHL Group products at once, the inconsistency compounds.
| API | Division | Auth model | Typical use |
|---|---|---|---|
| MyDHL API | DHL Express | HTTP Basic Auth (API Key/Secret) | International time-definite parcel/freight shipments, labels, rating, tracking |
| DHL Parcel DE Shipping | Post & Parcel Germany | OAuth2 (ROPC), Basic Auth being phased out | Domestic German parcel shipping |
| DHL Global Forwarding Shipment Label | DHL Global Forwarding | OAuth2 | Ocean/air freight shipment labels |
| Shipment Tracking - Unified | DHL Group (multi-division) | DHL-API-Key header | Cross-division tracking lookups |
DHL Parcel DE is explicit that it's moving away from the pattern MyDHL API still uses: with the modernization of our API landscape, DHL is gradually replacing the previous Basic Authentication with the more modern OAuth 2.0 supporting Authentication API. That means a team that builds a shared auth abstraction today based on "DHL uses Basic Auth" will need to rework it the moment Parcel DE drops Basic Auth entirely, while MyDHL API keeps running on the scheme it has now.
This is the exact kind of per-carrier and even per-product-line auth divergence that multi-carrier platforms exist to abstract. Teams running DHL Express alongside GLS, InPost, Royal Mail or DPD often reach for aggregation layers like Cargoson, nShift, EasyPost or ShipEngine specifically so individual engineers don't have to track which DHL product uses which handshake this quarter. Whether that trade-off is worth it depends on how many carriers you run and how much you value owning the raw request/response cycle versus accepting a normalized schema.
Follow-ups / open questions
A few things this walkthrough didn't settle and that we'd want to verify with a longer-running test rig before calling MyDHL API fully characterized:
- Webhook and tracking push behavior in sandbox vs production. We haven't confirmed whether sandbox shipments trigger the same tracking event cadence as production, or whether webhook delivery is even enabled in test mode.
- Whether the 500-call daily test quota resets on a rolling or calendar basis. DHL's docs state the limit but not the reset mechanics, which matters if your CI pipeline runs integration tests on every merge.
- Customs and landed-cost field behavior in sandbox. We haven't run enough customs-declarable test shipments to say confidently whether sandbox duty/tax calculations mirror production logic or return placeholder values.
If you've hit a different sandbox quirk on MyDHL API — a silent account mismatch, an undocumented required field, anything that cost you an afternoon — we'd genuinely like to hear about it for a follow-up test log.