     [Blog](https://scrapfly.io/blog)   /  [hidden-api](https://scrapfly.io/blog/tag/hidden-api)   /  [How to Scrape DHL Tracking Status and Shipment Events](https://scrapfly.io/blog/posts/how-to-scrape-dhl)   # How to Scrape DHL Tracking Status and Shipment Events

 by [Ziad Shamndy](https://scrapfly.io/blog/author/ziad) Sep 11, 2026 19 min read [\#hidden-api](https://scrapfly.io/blog/tag/hidden-api) [\#python](https://scrapfly.io/blog/tag/python) [\#scrapeguide](https://scrapfly.io/blog/tag/scrapeguide) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl "Share on LinkedIn") [  ](https://x.com/intent/tweet?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl&text=How%20to%20Scrape%20DHL%20Tracking%20Status%20and%20Shipment%20Events "Share on X") [  ](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl "Share on Facebook")    

 

 

Summarize this article with

 [  ](https://chat.openai.com/?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl) [  ](https://claude.ai/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl) [  ](https://x.com/i/grok?text=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl) [  ](https://www.perplexity.ai/search/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl) [  ](https://www.google.com/search?udm=50&aep=11&q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-dhl) 



         

A DHL tracking scraper can return a perfectly valid page and still produce no shipment data. An empty lookup, a blocked response, and a populated event timeline can look identical in a raw HTTP response, until that response gets classified.

In the 14-day production window ending 2026-09-11, Scrapfly logged roughly 46,000 requests to dhl.com. The domain-level data shows demand for DHL data, but it does not identify tracking-page traffic specifically. This guide covers page inspection, JSON discovery, response classification, event normalization, and DHL's official tracking API.



[**DHL Scraper**github.com/scrapfly/scrapfly-scrapers/tree/main/dhl-scraper](https://github.com/scrapfly/scrapfly-scrapers/tree/main/dhl-scraper)

## Key Takeaways

A quick summary before the full walkthrough:

- DHL's Track &amp; Trace page loads status and events from a background call to `www.dhl.com/utapi`. The scraper requests that endpoint directly, then falls back to rendering the page and capturing the same call.
- Plain HTTP to dhl.com gets blocked. The scraper routes every request through residential proxies with JavaScript rendering and Anti Scraping Protection enabled.
- The live `/utapi` sample uses `pre-transit`, `transit`, `delivered`, and `unknown`. DHL's official API also documents `failure`, but its support page warns that status codes can differ by division.
- An HTTP 200 is not proof of a shipment record. The scraper tags every result with a classification of `success`, `empty`, `captcha-or-forbidden`, or `semantic-mismatch` before any field extraction.
- DHL states that tracking events usually appear 24 to 48 hours after a Track and Trace ID is received, so a valid identifier can legitimately show zero events.
- Tracking numbers are sensitive operational inputs. Accept them only from authorized users, redact them in logs, and never harvest or guess identifiers.

**Get web scraping tips in your inbox**Trusted by 100K+ developers and 30K+ enterprises. Unsubscribe anytime.







## What DHL Tracking Data Can You Extract?

A populated DHL tracking lookup yields a shipment level status plus zero or more timestamped events, with origin, destination, and an estimated delivery date when the source carries them. The exact field set comes from the maintained scraper running against DHL's own backend, not from assumption.

Treat the tracking number as input and the parsed result as output. A tracking scraper should never log a full tracking number, since that identifier alone can reveal a shipment's status and route to anyone holding it.

The output side stays a normalized record with a current status, an ordered list of events, origin and destination localities, and an estimated delivery date. Optional fields stay `null` rather than filled with a guess.

DHL's own Unified Shipment Tracking API documents a wider field set than the website backend is guaranteed to expose. That gap matters when deciding what a page based scraper can promise a caller.

Documented API fields include current location, estimated delivery, status and milestone timestamps, complete travel history, origin and destination, shipment pieces, dimensions, and weight. Availability depends on service and product.

Treat that API field list as a reference, not a guarantee. The scraper populates a field only when the live `/utapi` response carries it, leaving the rest `null`.

The table below separates the shipment level record from the event level records nested inside it:

| Field | Scope | Description |
|---|---|---|
| classification | Result | `success`, `empty`, `captcha-or-forbidden`, or `semantic-mismatch` |
| tracking\_number | Shipment | Shipment id from the response, or the redacted input when absent |
| status | Shipment | Human readable status description, falling back to the status code |
| origin | Shipment | Origin locality, when the response exposes it |
| destination | Shipment | Destination locality, when the response exposes it |
| estimated\_delivery | Shipment | Projected arrival from DHL Global Forwarding route data, when present |
| events | Shipment | Ordered list of timestamped event records |
| event.timestamp | Event | ISO 8601 timestamp with timezone offset |
| event.status | Event | Status code, already mapped to DHL's simplified vocabulary |
| event.description | Event | Raw milestone description for that event |
| event.location | Event | Location locality tied to the event, when present |

json```json
{
  "classification": "success",
  "tracking_number": "XXXXX9526",
  "status": "Document Handover (if no POD)",
  "events": [
    {
      "timestamp": "2026-08-27T17:05:00+08:00",
      "status": "unknown",
      "description": "Actual Arrival at Destination CFS/CY",
      "location": "Shanghai"
    },
    {
      "timestamp": "2026-08-25T09:00:00+08:00",
      "status": "delivered",
      "description": "Document Handover (if no POD)",
      "location": "Shanghai"
    }
  ],
  "estimated_delivery": "2026-08-23T00:00:00+08:00",
  "origin": "London",
  "destination": "Shanghai"
}
```



This is the shape `scrape_tracking` returned when rechecked on 2026-09-11, trimmed to the two newest events and with the tracking number redacted. Shipment status changes over time, so refresh this snapshot before a later republish. The sample is a DHL Global Forwarding shipment, which is why the events read as vessel milestones and the estimated delivery comes from a `dgf:routes` entry. The `shipments[].events[]` envelope the parser targets is the same regardless of division.

This tracking record sits inside a broader category of shipment data spanning multiple carriers. The [Logistics and Supply Chain Web Scraping](https://scrapfly.io/use-case/logistics-web-scraping) use case page covers that wider scope.

The page fills this schema only after a tracking lookup resolves.



## How Does the DHL Track &amp; Trace Page Work?

DHL's public Track &amp; Trace page accepts one or more tracking identifiers through a single search box, then renders status and event data only after that lookup resolves. Everything before submission is just a form state.

The observed page heading reads Track &amp; Trace, with a prompt of Enter your tracking number(s). Submitting the form with no input returns the message Please enter your tracking number(s), which a scraper should treat as expected rather than an error worth retrying.

Direct HTTP requests to the [DHL tracking page](https://www.dhl.com/us-en/home/tracking.html) without a managed bypass are blocked before they reach tracking data. The maintained scraper routes every request through residential proxies with JavaScript rendering and Anti Scraping Protection for that reason, rather than a bare HTTP client.

### What DHL Tracking Number Inputs Are Valid?

DHL's tracking page states that a tracking ID is a combination of numbers and possibly letters, with a minimum length of five characters. Tab, comma, space, and semicolon each work as separators when a lookup carries more than one identifier.

DHL's separate [eCommerce Tracking FAQ](https://www.dhl.com/us-en/home/customer-service/ecommerce-tracking-faq.html) says its tracking IDs contain numbers and/or letters and range from 10 to 39 characters.

That FAQ covers a different DHL division. Treat the two statements as separate first party claims rather than one universal format.

A scraper that validates input locally should check against whichever statement matches the tracking numbers it actually receives, and should not force every DHL tracking ID through a single regular expression built from just one of these two sources.

### When Do DHL Tracking Events Appear?

DHL states that tracking events usually appear 24 to 48 hours after a Track and Trace ID is received, generally once the shipment reaches a DHL facility. A valid identifier can sit with zero events during that window.

A scraper needs to treat a DHL tracking lookup as one of several distinct states, not a single pass or fail outcome:

- No input submitted, which returns the documented empty state message
- Malformed input, shorter than the minimum length or split by an unsupported separator
- Valid input with no events yet, inside the 24 to 48 hour window DHL describes
- Valid input that never resolves to a known shipment at all
- A populated result carrying a current status and one or more events
- A blocked or challenged response that never reaches tracking data in the first place

Six outcomes from one search box make classification the real engineering problem here before any parsing logic gets written. That classification work carries over directly into how the scraper inspects the requests behind this page.



## How Do You Inspect DHL JSON/XHR Tracking Requests?

Finding how a DHL tracking result loads means watching the network panel through an authorized test lookup, not guessing at an endpoint from outside. Browser developer tools turn that lookup into a short, repeatable inspection routine.

The workflow runs the same way on any tracking page, DHL included:

1. Open developer tools and the Network panel before submitting the tracking form, then enable Preserve log so the request list survives any redirect.
2. Filter the request list down to Fetch/XHR only, since that removes images, fonts, and analytics noise from the view.
3. Submit an empty form first and note which requests fire, then submit one sanctioned tracking lookup and compare the two request lists.
4. For each new request tied to the lookup, record the URL, method, query or body fields, required headers, response content type, and status code.
5. Open the response body for any request that changed between the two submissions, and confirm it carries the status or event timeline shown on screen.

Running this against DHL's page surfaces a background call to `https://www.dhl.com/utapi?trackingNumber=<id>` whose JSON body carries the `shipments` array rendered on screen. That is the request the maintained scraper targets, either by calling it directly or by capturing it from a rendered page.

A request only qualifies as the tracking source once its response body carries the visible status or event data. A request that fires on submission but only returns page configuration or analytics data should not be mistaken for that source.

Check for a `script[type="application/ld+json"]` block and any inline page state variables too. Some carrier sites expose a structured summary of the same record outside the XHR layer, and a clean JSON response beats parsing embedded script content when both are available.

[How to Scrape Hidden Web DataThe visible HTML doesn't always represent the whole dataset available on the page. In this article, we'll be taking a look at scraping of hidden web data. What is it and how can we scrape it using Python?](https://scrapfly.io/blog/posts/how-to-scrape-hidden-web-data)

Treat the `/utapi` path as volatile. A public page's internal endpoint can change shape or path without any version guarantee, unlike a published API contract. Rerun this inspection whenever field extraction starts coming back empty, since a silent schema change is a common reason a working scraper stops producing events.

[How to Scrape Hidden APIsIn this tutorial we'll be taking a look at scraping hidden APIs which are becoming more and more common in modern dynamic websites - what's the best way to scrape them?](https://scrapfly.io/blog/posts/how-to-scrape-hidden-apis)



## How Do You Set Up a DHL Scraper with Scrapfly?

A DHL scraper needs the Scrapfly Python SDK installed and an API key configured before it can route tracking requests through managed proxy and anti bot handling. Setup takes one install command and a short client block.

shell```shell
pip install scrapfly-sdk
```



This installs the official Scrapfly SDK, the same package used across Scrapfly's other scrape guides for requests, proxy rotation, and response handling. The maintained scraper also uses `loguru` for logging.

python```python
import os

from loguru import logger as log
from scrapfly import ScrapeConfig, ScrapflyClient, ScrapflyError

SCRAPFLY = ScrapflyClient(key=os.environ["SCRAPFLY_KEY"])

BASE_CONFIG = {
    "asp": True,
    "country": "US",
    "render_js": True,
    "proxy_pool": "public_residential_pool",
}

API_BASE_URL = "https://www.dhl.com/utapi"
TRACKING_PAGE_URL = "https://www.dhl.com/us-en/home/tracking.html"
```



Every request spreads `BASE_CONFIG` into its `ScrapeConfig`. Each flag earns its place:

- `asp=True` routes the request through Anti Scraping Protection, which handles DHL's fingerprint and challenge checks.
- `render_js=True` forces browser execution for the page fallback. The canonical DHL scraper currently keeps this setting on the direct `/utapi` request too; remove it there only after a live test proves a non-rendered ASP request returns shipment JSON.
- `country="US"` pins the residential proxy to a US exit node for a consistent locale.
- `proxy_pool="public_residential_pool"` selects the residential pool used by the canonical DHL scraper.

DHL's `/utapi` endpoint returned HTTP 403 with `server: AkamaiGHost` to plain curl, while the rendered ASP request returned shipment JSON. Keep the direct and browser paths because they fail at different boundaries; add retries only after measuring a retryable failure mode.

The [Python SDK documentation](https://scrapfly.io/docs/sdk/python) covers the rest of `ScrapeConfig`. Scrapfly also ships TypeScript, Go, and Rust SDKs, plus a Scrapy integration, for teams working outside Python.

Classify the response before reading shipment fields from it.



## How Do You Parse DHL Shipment Status and Tracking Events?

Parsing a DHL tracking response only makes sense after that response gets classified. The scraper tries DHL's backend directly. It checks whether the result carries events, and falls back to a rendered page only when it does not.

python```python
import json


def _build_tracking_url(tracking_number: str, api_endpoint: bool = False) -> str:
    if api_endpoint:
        return f"{API_BASE_URL}?trackingNumber={tracking_number}"
    return f"{TRACKING_PAGE_URL}?tracking-id={tracking_number}&submit=1"


async def scrape_tracking(tracking_number: str) -> dict:
    # strategy 1: request the website's own tracking backend directly
    try:
        url = _build_tracking_url(tracking_number, api_endpoint=True)
        response = await SCRAPFLY.async_scrape(ScrapeConfig(url, **BASE_CONFIG))
        if response.upstream_status_code == 403:
            result = _empty_tracking_result(
                tracking_number, "captcha-or-forbidden"
            )
        else:
            data = json.loads(response.content)
            result = _parse_tracking_api_response(data, tracking_number)
        if result["classification"] == "success":
            return result
    except (ScrapflyError, json.JSONDecodeError) as exc:
        log.warning(f"direct API request failed for a redacted tracking number: {exc}")

    # strategy 2: render the tracking page and capture the /utapi call it fires
    url = _build_tracking_url(tracking_number, api_endpoint=False)
    try:
        response = await SCRAPFLY.async_scrape(
            ScrapeConfig(url, wait_for_selector="xhr:/utapi", **BASE_CONFIG)
        )
        if response.upstream_status_code == 403:
            return _empty_tracking_result(
                tracking_number, "captcha-or-forbidden"
            )
        data = _get_xhr_data(response, "/utapi")
        return _parse_tracking_api_response(data, tracking_number)
    except ScrapflyError as exc:
        classification = classify_tracking({}, exc)
        return _empty_tracking_result(tracking_number, classification)
    except (RuntimeError, json.JSONDecodeError):
        return _empty_tracking_result(tracking_number, "semantic-mismatch")
```



The direct call skips browser rendering so the scraper tries it first. If it returns no events, the second strategy loads the tracking page with `wait_for_selector="xhr:/utapi"`, which holds the response until the page's own `/utapi` call resolves and then exposes it in the captured request list.

python```python
def _get_xhr_data(response, url_pattern: str) -> dict:
    """Return the JSON body of the first captured XHR call whose URL contains url_pattern."""
    xhr_calls = response.scrape_result.get("browser_data", {}).get("xhr_call", [])
    call = next((c for c in xhr_calls if url_pattern in c.get("url", "")), None)
    if not call:
        raise RuntimeError(f"XHR call matching '{url_pattern}' not found, try a longer render wait")
    return json.loads(call["response"]["body"])
```



Both strategies feed the same JSON envelope into one parser, so the rest of the pipeline does not care which path produced it.

### How Should a DHL Scraper Classify Empty and Blocked Responses?

Every result should carry one of four labels, `success`, `empty`, `captcha-or-forbidden`, or `semantic-mismatch`, and each label needs to tie back to concrete evidence in the response rather than a status code by itself. The scraper types the `classification` field to exactly those four values.

python```python
from typing import Literal, Optional

Classification = Literal[
    "success", "empty", "captcha-or-forbidden", "semantic-mismatch"
]


def classify_tracking(
    data: dict, error: Optional[ScrapflyError] = None
) -> Classification:
    """Classify a DHL response before field extraction."""
    if error is not None:
        api_response = getattr(error, "api_response", None)
        upstream_status = getattr(api_response, "upstream_status_code", None)
        if upstream_status == 403:
            return "captcha-or-forbidden"
        raise error

    shipments = data.get("shipments")
    if shipments is None:
        return "semantic-mismatch"
    if not shipments or not shipments[0].get("events"):
        return "empty"
    return "success"
```



An upstream 403 becomes `captcha-or-forbidden`; unrelated SDK errors still raise. A JSON body with no `shipments` key is a `semantic-mismatch`, while an empty shipment list or eventless shipment is `empty`. Only `success` moves on to normalization.

### How Should DHL Tracking Events Be Normalized?

Normalization runs only after classification returns `success`. The parser flattens each raw event into a fixed shape and keeps optional fields nullable rather than inventing values.

python```python
def _empty_tracking_result(
    tracking_number: str, classification: Classification
) -> dict:
    return {
        "classification": classification,
        "tracking_number": tracking_number,
        "status": None,
        "events": [],
        "estimated_delivery": None,
        "origin": None,
        "destination": None,
    }


def _parse_tracking_api_response(data: dict, tracking_number: str) -> dict:
    classification = classify_tracking(data)
    if classification != "success":
        return _empty_tracking_result(tracking_number, classification)

    shipment = data["shipments"][0]
    status = shipment.get("status") or {}
    details = shipment.get("details") or {}
    routes = details.get("dgf:routes") or []

    events = []
    for event in shipment.get("events") or []:
        address = (event.get("location") or {}).get("address") or {}
        events.append(
            {
                "timestamp": event.get("timestamp"),
                "status": event.get("statusCode") or event.get("status"),
                "description": event.get("description"),
                "location": address.get("addressLocality"),
            }
        )

    origin = (shipment.get("origin") or {}).get("address") or {}
    destination = (shipment.get("destination") or {}).get("address") or {}

    return {
        "classification": classification,
        "tracking_number": shipment.get("id") or tracking_number,
        "status": status.get("description") or status.get("statusCode"),
        "events": events,
        "estimated_delivery": routes[0].get("dgf:estimatedArrivalDate") if routes else None,
        "origin": origin.get("addressLocality"),
        "destination": destination.get("addressLocality"),
    }
```



Each event keeps its `statusCode` as the normalized `status` and falls back to the free text `status` only when the code is missing. In the live 2026-09-11 sample, those codes are `pre-transit`, `transit`, `delivered`, and `unknown`. DHL's official API also documents `failure`, but one shipment does not prove that every division returns every value. The shipment level `status` stays a human readable description.

Location comes from `location.address.addressLocality` alone, so an event with no structured address resolves to `null`. The estimated delivery date is pulled from the first `dgf:routes` entry, a DHL Global Forwarding field that is `null` for other divisions.

The parser does not resort or deduplicate. DHL returns events newest first in the sample, so sort on `timestamp` after extraction if you need strict chronological order.

DHL's own [status codes support article](https://support-developer.dhl.com/support/solutions/articles/47001197243-what-are-the-different-status-codes-available-in-the-dhl-shipment-tracking-unified-api-) defines `pre-transit`, `transit`, `delivered`, `failure`, and `unknown` for its Unified Shipment Tracking API. The `/utapi` backend appears to use the same values, but confirm that against your own tracking numbers before mapping any downstream logic to them.

DHL's official API is a separate, versioned integration path.



Scrapfly

#### Scale your web scraping effortlessly

Scrapfly handles proxies, browsers, and anti-bot bypass — so you can focus on data.

[Try Free →](https://scrapfly.io/register)## Should You Use the DHL Unified Shipment Tracking API Instead?

Use DHL's official Unified Shipment Tracking API when the reader's access, division, and rate limits fit the application. Reach for public extraction when the goal is public web parity or data that account does not otherwise expose.

The official API is not the `/utapi` backend this guide reads. `/utapi` is the website's internal call with no version guarantee. The official API is a subscription product with a versioned schema, version 1.5.8 as of 2026-09-11. It uses RESTful JSON and requires a subscription key in the `DHL-API-Key` header.

The documented base endpoint is `api-eu.dhl.com/track/shipments`, kept as plain text here since an unauthenticated request against it correctly returns 401.

DHL's documented initial access allows 250 calls per day with at most one call every five seconds, meant for development rather than production traffic. Higher limits require an upgrade request submitted through the developer portal's app dashboard.

| Factor | Official API | Public extraction |
|---|---|---|
| Access | Developer account plus a `DHL-API-Key` subscription | No account required, subject to the site's own bot controls |
| Schema stability | Versioned contract, 1.5.8 as of this review | `/utapi` can change shape without any version notice |
| Rate limits | 250 calls per day at initial access, 1 call per 5 seconds | No published limit, governed by the site's own protection |
| Data scope | Fields documented per service and product | Only what the `/utapi` response actually renders |
| Best fit | Production integrations with an approved account | Parity checks, discovery, or divisions outside API coverage |

Neither path bypasses authorization for private data. The API surfaces what DHL grants to an approved account, and the public path surfaces what DHL already renders for any visitor who submits a valid tracking number.

Full details, including onboarding and per product field coverage, live on the [DHL Unified Shipment Tracking API reference](https://developer.dhl.com/tracking?language_content_entity=en) and the portal's [rate limit upgrade page](https://developer.dhl.com/getting-started/get-a-higher-rate-limit?language_content_entity=en). Recheck the version, limits, and header name against the live portal before relying on them.



DHL also offers a separate [Location Finder Unified API](https://developer.dhl.com/api-reference/location-finder-unified) for service-point data. That endpoint does not use tracking numbers and is outside this shipment-tracking tutorial.



## How Do You Handle DHL Tracking Numbers Responsibly?

A tracking number identifies one shipment and can expose its status and route to anyone holding it. Treat every tracking number as a sensitive operational input, not a disposable test string.

A responsible pipeline should follow a few concrete rules:

- Accept tracking numbers only through explicit application input or an authorized internal dataset, never by enumerating, guessing, or purchasing identifiers.
- Redact all but a short suffix of any tracking number that reaches a log line, an error report, or a support ticket.
- Keep tracking numbers out of screenshots, saved fixtures, and shared debugging sessions.
- Set a short retention window on raw tracking responses, and separate any public location data from shipment specific records in storage and access controls.

Data minimization here is an operational boundary, not a compliance afterthought. A pipeline that follows these four rules avoids most of the exposure risk tracking numbers carry on their own.



## FAQ

Does DHL Provide Live Parcel Tracking?DHL provides shipment status and milestone updates rather than continuous location tracking. DHL's own FAQ describes milestone based tracking, and new events can take time to appear rather than streaming in as they happen.







How Long Does DHL Tracking Take to Show an Event?DHL states that tracking events usually appear 24 to 48 hours after a Track and Trace ID is received, generally once the shipment reaches a DHL facility.

A scraper should read that gap as a valid pending state, not a parsing failure.







Can One DHL Request Track Multiple Numbers?DHL's eCommerce FAQ says multiple tracking numbers can be submitted together, and the main tracking page documents tab, comma, space, and semicolon as separators. Confirm batching behavior and safe limits directly before relying on it in a production scraper.









## Conclusion: Build Reliable DHL Tracking Extraction

A reliable DHL tracking scraper starts with an authorized tracking number, reads DHL's `/utapi` backend through a managed bypass, classifies the response before parsing, and normalizes events into one consistent record rather than trusting whatever text happens to be on screen.

That order held up throughout this guide, including the fallback where a direct backend call returns nothing and the scraper renders the page to capture the same request instead.

For ocean freight rather than parcel tracking, the [Maersk container tracking guide](https://scrapfly.io/blog/posts/how-to-scrape-maersk) covers container schedules on a different carrier model entirely. A Hapag-Lloyd guide is planned as a separate, carrier specific article.

Scrapfly's [Web Scraping API](https://scrapfly.io/products/web-scraping-api) handles the residential proxies, JavaScript rendering, and anti bot handling this scraper leans on. DHL's own Unified Shipment Tracking API remains the better fit whenever its access and rate limits already match the use case at hand.



### Web Scraping API

Scrape any website with our powerful API. Anti-bot bypass, JavaScript rendering, and rotating proxies built-in.



[Try Web Scraping API](https://scrapfly.io/docs/scrape-api/getting-started)



Legal Disclaimer and PrecautionsThis tutorial covers popular web scraping techniques for education. Interacting with public servers requires diligence and respect:

- Do not scrape at rates that could damage the website.
- Do not scrape data that's not available publicly.
- Do not store PII of EU citizens protected by GDPR.
- Do not repurpose *entire* public datasets which can be illegal in some countries.

Scrapfly does not offer legal advice but these are good general rules to follow. For more you should consult a lawyer.

 

   [  Add as a preferred source ](https://google.com/preferences/source?q=scrapfly.io) Table of Contents















 

  Table of Contents- [Key Takeaways](#key-takeaways)
- [What DHL Tracking Data Can You Extract?](#what-dhl-tracking-data-can-you-extract)
- [How Does the DHL Track &amp;amp; Trace Page Work?](#how-does-the-dhl-track-amp-trace-page-work)
- [What DHL Tracking Number Inputs Are Valid?](#what-dhl-tracking-number-inputs-are-valid)
- [When Do DHL Tracking Events Appear?](#when-do-dhl-tracking-events-appear)
- [How Do You Inspect DHL JSON/XHR Tracking Requests?](#how-do-you-inspect-dhl-json-xhr-tracking-requests)
- [How Do You Set Up a DHL Scraper with Scrapfly?](#how-do-you-set-up-a-dhl-scraper-with-scrapfly)
- [How Do You Parse DHL Shipment Status and Tracking Events?](#how-do-you-parse-dhl-shipment-status-and-tracking-events)
- [How Should a DHL Scraper Classify Empty and Blocked Responses?](#how-should-a-dhl-scraper-classify-empty-and-blocked-responses)
- [How Should DHL Tracking Events Be Normalized?](#how-should-dhl-tracking-events-be-normalized)
- [Should You Use the DHL Unified Shipment Tracking API Instead?](#should-you-use-the-dhl-unified-shipment-tracking-api-instead)
- [How Do You Handle DHL Tracking Numbers Responsibly?](#how-do-you-handle-dhl-tracking-numbers-responsibly)
- [FAQ](#faq)
- [Conclusion: Build Reliable DHL Tracking Extraction](#conclusion-build-reliable-dhl-tracking-extraction)
 
    Join the Newsletter  Get monthly web scraping insights 

 

  



Scale Your Web Scraping

Anti-bot bypass, browser rendering, and rotating proxies, all in one API. Start with 1,000 free credits.

  No credit card required  1,000 free API credits  Anti-bot bypass included 

 [Start Free](https://scrapfly.io/register) [View Docs](https://scrapfly.io/docs/onboarding) 

 Not ready? Get our newsletter instead. 

 

 ## Related Articles

 [  

 python headless-browser 

### Web Scraping Background Requests with Headless Browsers

In this tutorial we'll be taking a look at a rather new and popular web scraping technique - capturing background reques...

 

 ](https://scrapfly.io/blog/posts/web-scraping-background-requests-with-headless-browsers-and-python) [     

 python screenshots 

### How to Track Web Page Changes with Automated Screenshots

There are many different ways to monitor web page changes and one of the most popular techniques is screenshot tracking....

 

 ](https://scrapfly.io/blog/posts/how-to-track-web-page-changes-using-automated-screenshots) [     

 python scrapeguide 

### How to Scrape Maersk Container Tracking and Shipping Schedule Data

Learn how to scrape Maersk container tracking events and vessel schedules using Python and Scrapfly, extracting containe...

 

 ](https://scrapfly.io/blog/posts/how-to-scrape-maersk) 

  



   



 Scale your web scraping effortlessly, **1,000 free credits** [Start Free](https://scrapfly.io/register)