How to Generate a DHL Paket Return Label via API
Step-by-step: create a DHL Parcel DE return label via the new Returns API at api-eu.dhl.com, with OAuth2 setup, receiverId, and sample curl.
DHL pulled the plug on the legacy Returns API at cig.dhl.de on May 31, 2026. If you're still pointing requests at that host, they're failing in production right now, not in some future deprecation window. DHL's own migration notice is blunt about it: "We would like to remind you that the old version of the Returns API (under cig.dhl.de) will no longer be supported as of May 31, 2026." This walkthrough targets the replacement, the DHL Parcel DE Returns API at api-eu.dhl.com/parcel/de/shipping/returns/v1, which DHL bumped to version 1.1.0 on October 2, 2026, adding new print format and QR code options. Every request below was run against the sandbox host api-sandbox.dhl.com using plain curl, no SDK wrapper, so you can see exactly what crosses the wire.
The target artifact is a German domestic return label (DHL Retoure), rendered as both a PDF shipping label and a QR code. If you're coming from the outbound side of this carrier, our DHL SOAP-to-REST shipping migration post covers that separately. Returns is its own product, own host path, and as you'll see, its own auth quirks.
What you need before starting
Four things, and missing any one of them will cost you an afternoon of confusing 401s and 403s.
- A DHL API Developer Portal account with an application registered for "Parcel DE Returns." To use the DHL Parcel DE Returns API, you first need an application created including the API in production mode, which gives you both a sandbox and production
dhl-api-key. - A DHL Business Customer Portal (Geschäftskundenportal) login with Returns Settings configured. Return recipients and their ID ("Return recipient's name") are configured in the DHL Business Customer Portal, Returns Settings section — this is where your
receiverIdlives. - A decision on auth mode. DHL supports two alternative ways to authorize: a combination of API key and Basic Authentication provided with every call, or OAuth2 Password Flow where you obtain an access token once and provide it as a bearer token. DHL's own guidance is unambiguous about which one to pick going forward: "we are gradually replacing the previous Basic Authentication with the more modern OAuth 2.0 supporting Authentication API."
- Confirmation that your receiver ID has a billing number attached. If it doesn't, you're looking at DHL Retoure Selbstzahler, and the sandbox docs are explicit that "Die DHL Retoure Selbstzahler kann nicht über die DHL Paket DE Retoure API beauftragt werden" — it can't be booked via this API at all, full stop.
Booking a return label, step by step
This is the full sequence from zero to a rendered label, tested against sandbox on the dates noted below. Each step builds on the previous one's output.
- Register the app and pull your keys. In the DHL Developer Portal, create an application scoped to "Parcel DE Returns." You get a sandbox
dhl-api-keyimmediately; the production key activates once DHL certifies your sandbox usage, per the API reference: "Production usage will be approved after successful sandbox usage has been certified by DHL." - Build the order payload. At minimum you need
receiverId, acustomerReference(this prints on the label — use it for your internal RMA number), and the shipper's name and address block. Add an email field if you want DHL to handle digital delivery of the label to the end customer. - Parse the response. A successful booking returns
shipmentNumber,routingCode, base64-encodedlabelData, and (if requested)qrLabelData. These four fields are what the netresearch PHP SDK exposes directly on its confirmation object, which maps cleanly to the raw JSON if you're building your own client. - Decode and render. Base64-decode
labelDatainto a PDF for inclusion in a packing slip or customer email, andqrLabelDatafor cases where the customer scans a code at a DHL ServicePoint instead of printing anything. - Confirm trackability. Take the returned
shipmentNumberand query it against the Unified Tracking API atapi-eu.dhl.com/track/shipments?trackingNumber=...with yourDHL-API-Keyheader. A "label created" event confirms DHL's system has ingested the shipment, not just that your API call succeeded.
POST the order. Hit the orders endpoint with both headers set:
curl -X POST "https://api-sandbox.dhl.com/parcel/de/shipping/returns/v1/orders?labelType=BOTH" \
-H "dhl-api-key: ${API_KEY}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"receiverId": "deu_ggp",
"customerReference": "RMA-2026-00231",
"shipper": {
"name": "Jane Doe",
"addressStreet": "Sträßchensweg 2",
"postalCode": "53113",
"city": "Bonn",
"country": "DEU"
}
}'The labelType query parameter controls output: BOTH gets you the PDF and QR variant in one call, or narrow it to SHIPMENT_LABEL or QR_LABEL if you only render one format downstream.
Exchange Business Customer Portal credentials for an OAuth2 token. This is the step most legacy integrations skip, and it's the one DHL is pushing hardest. The Authentication API documentation confirms "the API provides an implementation of an OAuth2.0 resource-owner password credentials flow", hitting:
curl -X POST https://api-sandbox.dhl.com/parcel/de/account/auth/ropc/v1/token \
-d 'grant_type=password' \
-d 'client_id={YOUR-API-KEY}' \
-d 'client_secret={YOUR-API-SECRET}' \
-d 'username={GKP-USERNAME}' \
-d 'password={GKP-PASSWORD}'The response is "an opaque access token (example: JU8VQkQaBHnKsphAuGQPKF1VAoBv) to be used as Bearer token in the Header Authorization". Cache it — DHL's own guidance says "we explicitly recommend NOT making an access token request against the Authentication API for each functional request." Tokens expire in roughly 1799 seconds based on the expires_in field DHL returns alongside the token.
Look up your receiver IDs. Call GET /parcel/de/shipping/returns/v1/locations with your bearer token:
curl -H "Authorization: Bearer ${TOKEN}" -H "accept: application/json" \
https://api-sandbox.dhl.com/parcel/de/shipping/returns/v1/locationsThe response lists every receiver ID tied to your account. Cross-check each one against the Returns Settings page in the Business Customer Portal — the one you intend to use needs a billing number attached, or step 5 will reject it.
How you know it worked
Three checkpoints, in order of how quickly you'll hit them. First, the order POST returns a 201 with all four fields populated — an empty labelData string with a 200 status is not success, it's a silent partial failure worth logging separately. Second, the decoded PDF should render a valid DHL barcode and show the exact customerReference string you submitted; if it's blank or truncated, check your payload encoding before assuming DHL's side is broken. Third, the shipment number should surface a track event via the Unified Tracking API — in our sandbox runs this showed up within the same session, though sandbox tracking data is synthetic and shouldn't be used to benchmark production latency.
The receiver ID trap and the Basic Auth cliff edge
Two failure modes account for most of the support tickets we'd expect teams to hit here.
Case 1: validation errors from a receiver ID with no billing number. If your POST to /orders comes back rejected and the receiver ID looks correct, check whether it's configured as Selbstzahler in the Business Customer Portal. This isn't a code bug — the API reference is explicit that this category "kann nicht über die DHL Paket DE Retoure API beauftragt werden." The fix lives entirely in the Returns Settings UI, not your integration.
Case 2: Basic Auth that works today but won't tomorrow. DHL still accepts the Apikey-plus-Basic-Auth combination on this endpoint, and plenty of SDKs — including the smart-dato Laravel package — ship it as a fallback mode, labeling OAuth2 as "recommended — Basic Auth is deprecated by DHL". DHL's broader Authentication API guidance adds a concrete reason to move now rather than later: "Using the received bearer token for functional API calls results in approximately 25% faster response times for your requests." Grep your logs for any request still sending an Authorization: Basic header against the returns host, and migrate it to the ROPC bearer flow from step 3 before DHL forces the issue the way it did with cig.dhl.de.
Where this sits in a multi-carrier stack
Worth naming the DX pattern here, because it's not unique to Returns. DHL's German parcel portfolio splits Shipping, Returns, Pickup, and Tracking into separate API products, each with its own host path and sometimes its own auth expectations layered on top of a shared ROPC token service. You end up building receiver-ID lookups, billing-number validation, and token-caching logic once for Returns, then rebuilding variants of the same logic when you wire up Pickup or Shipping. Multi-carrier platforms like Sendcloud, nShift, ShipEngine, and Cargoson exist largely to absorb this per-product auth sprawl so integration teams aren't re-implementing ROPC token refresh and receiver ID checks for every new carrier endpoint DHL ships. Whether that abstraction is worth the platform fee depends entirely on how many carrier products you're touching — if DHL Returns is the only piece you need, raw curl against the REST API is still the cheapest path.
Next steps
If you haven't already migrated the outbound shipping side of this carrier, that's a separate product with its own V2 REST API and the same OAuth2 push — worth doing in the same sprint since the credentials overlap. Open question we're tracking: whether DHL eventually folds Returns into the same versioning cadence as Shipping V2, given both now share the ROPC authentication layer. We'll re-test against the next version bump once it lands and update the field mappings here if anything shifts.