DHL & Hermes Parcel Tracking (Germany) avatar

DHL & Hermes Parcel Tracking (Germany)

Pricing

from $10.00 / 1,000 tracked parcels

Go to Apify Store
DHL & Hermes Parcel Tracking (Germany)

DHL & Hermes Parcel Tracking (Germany)

Track many DHL Paket / Deutsche Post and Hermes parcels in one run and get a unified status timeline (delivered, out for delivery, exception…). Carrier auto-detection, residential proxy built in. DPD/GLS/UPS experimental. Pay per tracked parcel.

Pricing

from $10.00 / 1,000 tracked parcels

Rating

0.0

(0)

Developer

Nerdrx

Nerdrx

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

2

Monthly active users

2 days ago

Last modified

Categories

Share

What is DHL & Hermes Parcel Tracking?

This actor takes a list of German parcel tracking numbers, figures out which carrier each one belongs to, queries that carrier's public tracking endpoint and returns one unified status timeline per parcel — the same fields for DHL, Deutsche Post and Hermes.

It is built for e-commerce operations teams, customer-service automations ("where is my order?" bots) and AI agents that need a machine-readable delivery status instead of a carrier website.

Supported today: DHL / Deutsche Post and Hermes. DPD, GLS and UPS are shipped as experimental and currently failing — DPD moved tracking behind a postal-code privacy gate, GLS moved behind Cloudflare, and UPS bot-protects its endpoint. Their tracking numbers are reported with status: "unsupported_carrier" and are never charged unless you switch on the experimentalCarriers input. See the carrier table below.

This actor uses unofficial, public tracking endpoints — the same JSON endpoints the carriers' own tracking web pages call from your browser. There is no API key and no contract with the carriers. The endpoints are undocumented and can change or start blocking without notice. See "FAQ & support" and NOTES.md for the details and the current verification status of each carrier.

What can it do?

  • Track many parcels in one run — one dataset row per tracking number.
  • Auto-detect the carrier from the number format, or force one carrier for the whole run.
  • Fall back to the next plausible carrier when a number format is ambiguous (14 digits can be Hermes or DPD; 12 digits can be DHL or GLS).
  • Normalize German/English status wording into a stable status enum (pre_transit, in_transit, out_for_delivery, delivered, exception, unknown, plus unsupported_carrier for disabled carriers).
  • Convert all carrier timestamps (German local time, usually without a timezone) into real UTC ISO 8601 strings, DST-correct.
  • Return the full event timeline, the estimated delivery window, the delivery time and a recipient hint (neighbour / parcel shop / signature) when the carrier exposes it.
  • Keep going when one parcel or one carrier fails — failures are written to the dataset with an error message and are not charged.

What data does it extract?

FieldTypeDescription
trackingNumberstringThe normalized tracking number (spaces/dashes removed, upper-cased).
carrierstringdhl, hermes, dpd, gls, ups or unknown.
statusstringUnified enum: pre_transit, in_transit, out_for_delivery, delivered, exception, unknown, unsupported_carrier.
statusTextstring | nullThe carrier's own status wording, in the requested language.
lastEventAtstring | nullISO 8601 UTC timestamp of the newest event.
estimatedDeliverystring | nullISO 8601 UTC estimated delivery / delivery window start, when the carrier publishes one.
deliveredAtstring | nullISO 8601 UTC delivery timestamp (only when delivered).
recipientHintstring | nullWho/where it was handed over: neighbour name, parcel shop, signature, destination city.
eventsarrayFull timeline, oldest → newest. Each item: { at, status, description, location }.
trackingUrlstring | nullHuman-readable carrier tracking page for this number.
sourceUrlstring | nullThe exact endpoint the data came from.
scrapedAtstringISO 8601 UTC timestamp of the run.
errorstring | nullnull on success; otherwise why this parcel could not be tracked.

Carriers, number formats and endpoints

Carrier (carrier value)StatusTypical number formatsEndpoint usedReliability caveats
DHL Paket / Deutsche Post (dhl)supported (needs residential proxy)20 digits starting 003… (GS1/NVE), 12 or 16 digits, JJD… + 15–20 digits, UPU S10 RR123456785DEGET https://www.dhl.de/int-verfolgen/data/search?piececode=<nr>&language=de&inputSearch=true&noRedirect=true&cid=appNeeds cookies: a "cold" client gets an HTML consent/bot page instead of JSON, so the actor always loads the public tracking page first (same cookie jar, browser headers) and retries once if it is still blocked. From Apify datacenter IPs it stays blocked even after the warm-up, so the Apify RESIDENTIAL proxy (country DE) is required and is the input default — verified working with it on 2026-09-02. Unknown numbers come back as a 200 stub with sendungNichtGefunden.keineDatenVerfuegbar, which is reported as "no data", not as a failure.
Hermes Germany (hermes)supported14 digits (also H…, 12/16/20-digit label variants)GET https://api.my-deliveries.de/tnt/parcelservice/parceldetails/<nr> (fallback: …/tnt/v2/shipments/search/<nr>)Answers 404 for numbers that exist but have not been scanned yet. Both response shapes (parcelHistory and parcelProgress) are supported.
DPD Germany (dpd)⚠️ experimental — currently failing14 digits (often leading 0), 28-digit long label numbersGET https://tracking.dpd.de/rest/plc/de_DE/<nr> (en_DE for English)Broken as of 2026-09-02. The endpoint now hangs up the socket (also via proxy) — DPD redirects tracking to a postal-code privacy gate (my.dpd.de/myparcels/dataprotection-tracking.aspx), which needs the recipient PLZ, so the old REST endpoint is effectively dead. Kept behind experimentalCarriers in case it returns.
GLS Germany (gls)⚠️ experimental — currently failing11 or 12 digits, ZB…GET https://gls-group.eu/app/service/open/rest/DE/de/rstt001?match=<nr>Broken as of 2026-09-02. gls-group.eu now redirects to www.gls-pakete.de/reach-sendungsverfolgung?match=<nr> behind Cloudflare, so the old open REST endpoint is unverified and likely dead. Kept behind experimentalCarriers.
UPS Germany (ups)⚠️ experimental — currently failing1Z + 16 alphanumeric charactersPOST https://www.ups.com/track/api/Track/GetStatus?loc=de_DE with an X-XSRF-TOKEN taken from the cookie set by the tracking pageNever verified working. UPS bot-protects this endpoint aggressively and usually returns an HTML challenge instead of JSON. Kept behind experimentalCarriers; use the official UPS API for production UPS volume.

Carrier auto-detection is a format heuristic, not a lookup. When two carriers match the same format, the actor queries the most likely one first and falls back to the next. Set the carrier input explicitly when you already know the carrier — it is faster and avoids the extra request.

How to use

  1. Paste your tracking numbers into Tracking numbers (one per line). Spaces and dashes are fine — they are stripped automatically.
  2. Leave Carrier on auto for mixed lists, or pick a carrier when the whole list is from one carrier.
  3. Pick the Language for the carrier's original status texts (de or en). The status enum is language independent, so downstream logic never has to parse German.
  4. Leave Proxy configuration on the default (Apify RESIDENTIAL, country DE) — DHL needs it.
  5. Optionally set Maximum items as a cost cap, then run.
  6. Read the results from the dataset (Overview view) or via the API: https://api.apify.com/v2/datasets/<DATASET_ID>/items?clean=true&format=json.

To keep an order dashboard in sync, schedule the actor (for example hourly) with the open tracking numbers and upsert on trackingNumber; stop sending a number once its status is delivered.

Proxy — keep the default. proxyConfiguration defaults to the Apify RESIDENTIAL proxy, country DE, because DHL requires it: from datacenter IPs dhl.de serves a bot/consent HTML page instead of JSON even after the cookie warm-up (verified on the platform). With the residential proxy DHL returns JSON normally. Hermes works either way. If you turn the proxy off, expect DHL rows with blocked (use a residential proxy) in error.

How much does it cost?

This actor is pay per event:

EventWhen it firesPlanned price
parcel-trackedA tracking number returned a usable result (status and/or events). Charged once per tracking number, after the row is stored.$0.01
parcel-failedThe carrier returned nothing usable (unknown number, blocked request, endpoint change), or the number belongs to a carrier that is not supported yet (status: "unsupported_carrier"). The row is still written with an error so you can see what happened.free — not charged

So 1,000 successfully tracked parcels cost about $10, plus Apify platform compute (the actor is HTTP-only and runs at 256 MB, so compute is a rounding error). Duplicate tracking numbers inside one run are collapsed and charged once. The run stops gracefully as soon as your pay-per-event charge limit is reached.

Input

FieldTypeDefaultDescription
trackingNumbersarray of strings— (required)Tracking numbers, one per line.
carrierenumautoauto, dhl, hermes, and (only with experimentalCarriers) dpd, gls, ups.
languageenumdeLanguage of the carrier status texts: de or en.
maxItemsinteger100Maximum tracking numbers processed per run.
experimentalCarriersbooleanfalseAlso attempt DPD, GLS and UPS. They currently fail; leave off.
proxyConfigurationobjectApify RESIDENTIAL, country DEKeep the default — DHL needs it.
{
"trackingNumbers": ["00340434161094015902", "12345678901234"],
"carrier": "auto",
"language": "de",
"maxItems": 100,
"experimentalCarriers": false,
"proxyConfiguration": {
"useApifyProxy": true,
"apifyProxyGroups": ["RESIDENTIAL"],
"apifyProxyCountry": "DE"
}
}

Output

{
"trackingNumber": "00340434161094015902",
"carrier": "dhl",
"status": "delivered",
"statusText": "Die Sendung wurde erfolgreich zugestellt.",
"lastEventAt": "2026-08-31T09:42:00.000Z",
"estimatedDelivery": "2026-08-31T08:00:00.000Z",
"deliveredAt": "2026-08-31T09:42:00.000Z",
"recipientHint": "Nachbar Mustermann",
"events": [
{
"at": "2026-08-28T15:03:00.000Z",
"status": "pre_transit",
"description": "Die Auftragsdaten zu dieser Sendung wurden vom Absender elektronisch an DHL übermittelt.",
"location": null
},
{
"at": "2026-08-29T02:12:00.000Z",
"status": "in_transit",
"description": "Die Sendung wurde im Start-Paketzentrum bearbeitet.",
"location": "Neuwied"
},
{
"at": "2026-08-31T05:20:00.000Z",
"status": "out_for_delivery",
"description": "Die Sendung wurde für die Zustellung verladen.",
"location": "Hamburg"
},
{
"at": "2026-08-31T09:42:00.000Z",
"status": "delivered",
"description": "Die Sendung wurde erfolgreich zugestellt.",
"location": "Hamburg"
}
],
"trackingUrl": "https://www.dhl.de/de/privatkunden/pakete-empfangen/verfolgen.html?piececode=00340434161094015902&language=de",
"sourceUrl": "https://www.dhl.de/int-verfolgen/data/search?piececode=00340434161094015902&language=de&inputSearch=true&noRedirect=true&cid=app",
"scrapedAt": "2026-09-02T08:00:00.000Z",
"error": null
}

A failed parcel has the same shape with status: "unknown", events: [] and a filled error, for example:

{
"trackingNumber": "12345678901234",
"carrier": "dpd",
"status": "unknown",
"events": [],
"error": "dpd: DPD returned no parcel life cycle data for this tracking number | hermes: Hermes: no shipment found (HTTP 404)",
"scrapedAt": "2026-09-02T08:00:00.000Z"
}

FAQ & support

Is this an official carrier API? No. It calls the same undocumented public JSON endpoints the carriers' own tracking pages use in your browser. No API key or carrier contract is needed, but the endpoints can change their URL, headers or response shape at any time. NOTES.md lists every assumption and the curl command to re-verify it.

Why is my DPD / GLS / UPS parcel unsupported_carrier? Those three carriers closed their public tracking endpoints (DPD now demands the recipient postal code on my.dpd.de, GLS moved behind Cloudflare on gls-pakete.de, UPS serves a bot challenge). Rather than pretend, the actor reports those numbers with status: "unsupported_carrier", error: "carrier not supported yet" and charges nothing. Set experimentalCarriers: true to attempt them anyway — expect failures.

Why did a parcel come back with an error? The usual reasons: the number is not (yet) in the carrier's system, the number belongs to a different carrier than detected, the shipment is too old, or the endpoint answered 403/429 (for DHL: run with the residential proxy). Failed parcels are never charged.

The carrier was detected wrong. Number formats overlap between German carriers, so detection is a heuristic. Set the carrier input to the correct carrier and re-run — that also skips the fallback request.

Why are the timestamps different from the carrier website? Carriers publish German local time without a timezone. This actor converts them to UTC ISO 8601 (DST-aware), so 2026-08-31 11:42 in Berlin becomes 2026-08-31T09:42:00.000Z.

Can I get the recipient's name / signature? Only what the carrier itself shows publicly — a neighbour name, a parcel-shop name or a surname on a signature line. This actor never returns personal data the carrier does not publish for the tracking number.

Does it work for international parcels? Partly. DHL handles JJD… and UPU (RR…DE) numbers and Hermes covers its German network. For parcels handled entirely outside Germany, use the destination carrier's own tracker.

Support: open an issue on the actor page. Include the tracking number's format (not necessarily the number), the carrier and the run ID.