# DHL Parcel Prices Scraper (`axiomworks/dhl-parcel-prices-scraper`) Actor

Scrape DHL Germany parcel and packet list prices from dhl.de page URLs: product, price in EUR, max weight and dimensions, tracking and liability flags and order link. The default page gives 7 domestic products; the international price page adds destination and zone.

- **URL**: https://apify.com/axiomworks/dhl-parcel-prices-scraper.md
- **Developed by:** [Axiom Works](https://apify.com/axiomworks) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.95 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## DHL Parcel Prices Scraper

**Returns:** one row per DHL product offer with productId, title, priceEur, isFromPrice, maxWeightKg, maxDimensionsCm, category, sourceUrl.
**Input:** `{"startUrls": [{"url": "https://www.dhl.de/de/privatkunden.html"}], "maxItems": 20}`
**Best for:** DHL price monitoring, carrier price comparison, shipping cost spreadsheets.

```json
{
  "id": "PAECKS",
  "productId": "PAECKS",
  "title": "2 kg - Päckchen S",
  "priceEur": 4.19,
  "isFromPrice": false,
  "maxWeightKg": 2,
  "category": "packet",
  "sourceUrl": "https://www.dhl.de/de/privatkunden.html"
}
```

### What does DHL Parcel Prices Scraper do?

This DHL scraper provides structured records for workflows that need a DHL API for public catalogue prices. DHL Parcel Prices Scraper reads the parcel and packet prices that DHL publishes on `dhl.de` and returns them as structured records. Each record is one product offer, for example "2 kg - Paket" at 6.19 EUR, together with its weight limit, its maximum dimensions and whether liability and tracking are included.

You can use it to:

- Track DHL list prices over time by running it on a schedule and comparing datasets.
- Compare DHL domestic list prices with the prices of other carriers.
- Feed a price table, spreadsheet or dashboard without copying numbers by hand.
- Check which products carry liability and tracking and which do not.

The Actor reads public pages only. It does not log in, it does not solve CAPTCHAs, and it scrapes only `www.dhl.de`. Only if the parser cannot read a value, text from that card may also be sent to a third-party AI processor (TypeSafe AI) to pick it (see the FAQ). By default it reads the private-customer home page, which lists seven domestic products. It can also read the international price page, where each row is a product and price zone, and it follows links from an overview page to the price pages it links to when the start page itself shows no prices.

Offers that appear more than once on a page, for example in a desktop and a mobile layout, are returned once. Every record has a stable `id`, so ids never repeat within a run.

This is an independent tool. It is not affiliated with or endorsed by DHL.

### What data can you get?

Each dataset item is one product offer. Fields that a page does not show are `null` instead of being guessed.

| Field | Type | Description |
|---|---|---|
| `id` | string | Stable id: the product id, plus the destination country for international offers, e.g. `PAK02` or `PAK02-AUT`. |
| `productId` | string | DHL product id taken from the offer's order link, e.g. `PAK02`. |
| `title` | string | Product name as shown on the page, e.g. `2 kg - Paket`. |
| `priceEur` | number | List price in EUR, e.g. `6.19`. |
| `isFromPrice` | boolean | `true` when the page shows the price as "ab ..." (from). |
| `maxWeightKg` | number or null | Maximum weight in kilograms, e.g. `31.5`. |
| `maxDimensionsCm` | string or null | Maximum dimensions in centimetres, e.g. `120 x 60 x 60 cm`. |
| `liabilityIncluded` | boolean or null | Whether liability is included. `null` if the page does not say. |
| `trackingIncluded` | boolean or null | Whether tracking is included. `null` if the page does not say. |
| `destinationCountry` | string or null | Destination country code from the order link. Only set on the international price page. |
| `destinationZone` | string or null | Price zone heading the offer is listed under. Only set on the international price page. |
| `orderUrl` | string or null | The page's online-franking link for the offer. |
| `pricePerKgEur` | number or null | `priceEur` divided by `maxWeightKg`, rounded to cents; `null` when no weight is in the product name. |
| `category` | string or null | `packet`, `parcel` or `letter`, derived from the product name (Päckchen = packet, Paket = parcel). `null` if the name is unclear. |
| `sourceUrl` | string | The page the record came from. |
| `scrapedAt` | string | ISO timestamp of when the record was created. |
| `rawText` | string | Only when `includeRawText` is on: the card or table-row text the values were read from. |

### How to use

1. Open the Actor in Apify Console and go to the Input tab.
2. Leave the start URL as the private-customer home page, or paste one or more `dhl.de` page URLs that list parcel or packet prices.
3. Set the maximum number of records you want.
4. Click Start. The run normally finishes within a minute, because it fetches only a few pages.
5. Open the Output tab to view the table, or download it as JSON, CSV or Excel.

To monitor prices, create a schedule for the Actor (daily or weekly is enough, since list prices change rarely) and compare each run's dataset with the previous one, for example by `id` and `priceEur`.

If you give a page that shows no prices itself, such as a "send parcels" overview, the Actor reads the price pages that page links to. In that case `sourceUrl` on each record names the page the price was actually read from.

### Input

| Field | Type | Description |
|---|---|---|
| `startUrls` | array | `dhl.de` pages that list parcel or packet prices. Default `https://www.dhl.de/de/privatkunden.html` returns the 7 domestic products. For prices per destination country use `https://www.dhl.de/de/privatkunden/pakete-versenden/weltweit-versenden/preise-international.html` (ids like `PAK02-AUT`). Overview pages without prices are followed to the price pages they link. Only `dhl.de` and `www.dhl.de` URLs are accepted. |
| `maxItems` | integer | Shared limit across all URLs, after filters and duplicate removal. 1-500 recommended; default 50, prefilled with 20. Stops at this count when enough matching offers exist. |
| `categories` | array | Only return these categories: `packet`, `parcel`, `letter`. Empty returns all. |
| `destinationCountries` | array | Country codes from international order links, e.g. `["AUT","FRA"]`, ignoring case. Use an international price URL: domestic records have no country and cannot match. Default `[]` keeps all. |
| `maxPriceEur` | number | Nonnegative ceiling, e.g. `10`; omit for no ceiling. Combined with all other filters. A ceiling below every published offer returns zero records successfully. |
| `includeRawText` | boolean | Set `true` to add the visible card or row text as `rawText`. Default `false` omits it. |
| `proxyConfiguration` | object | Connection settings, e.g. `{"useApifyProxy":false}` (default). Blocked requests retry with fresh proxy sessions when available. |

Example input:

```json
{
  "startUrls": [{ "url": "https://www.dhl.de/de/privatkunden.html" }],
  "maxItems": 20,
  "includeRawText": false
}
```

The optional `proxyConfiguration` field defaults to a direct connection. If dhl.de answers 403, 429 or a 5xx error, the Actor retries with backoff on Apify datacenter proxy and then on residential proxy.

All filters apply together. If a run succeeds with no matching offers, remove `categories`, `destinationCountries` or `maxPriceEur` to broaden the request. The final status explains this. An unavailable page is skipped; other supplied URLs still run. The same-language homepage fallback remains available when at least one supplied page loads but produces no matching records. If every supplied target is unavailable, the run succeeds with zero items and a status message explaining which URLs to check. An unavailable URL never causes unrelated homepage offers to be returned on its own.

Invalid input stops the run straight away with a clear error message. This covers an empty `startUrls` list, a URL that is not on `dhl.de`, and a `maxItems` that is not a positive integer.

### Output

The output below comes from a local run on 14 October 2026 with the prefill input. The private-customer home page listed 13 offers including duplicates, and the Actor returned 7 unique records. The first record:

```json
{
  "id": "PAECKS",
  "productId": "PAECKS",
  "title": "2 kg - Päckchen S",
  "priceEur": 4.19,
  "isFromPrice": false,
  "maxWeightKg": 2,
  "pricePerKgEur": 2.1,
  "maxDimensionsCm": "35 x 25 x 10 cm",
  "liabilityIncluded": false,
  "trackingIncluded": false,
  "destinationCountry": null,
  "destinationZone": null,
  "orderUrl": "https://www.dhl.de/int-versenden/gw/rest/init?domain=de&force=true&shoppingcart.items.0.product.id=PAECKS&shoppingcart.items.0.type=ShipmentItem&tid=hp-teaser-minimodul-paecks",
  "category": "packet",
  "sourceUrl": "https://www.dhl.de/de/privatkunden.html",
  "scrapedAt": "2026-10-14T05:01:50.051Z"
}
```

All seven records from that run, in summary:

| id | title | priceEur | maxWeightKg | maxDimensionsCm | liability | tracking |
|---|---|---|---|---|---|---|
| `PAECKS` | 2 kg - Päckchen S | 4.19 | 2 | 35 x 25 x 10 cm | false | false |
| `PAECK` | 2 kg - Päckchen M | 5.19 | 2 | 60 x 30 x 15 cm | false | false |
| `PAK02` | 2 kg - Paket | 6.19 | 2 | 60 x 30 x 15 cm | true | true |
| `PAK05` | 5 kg - Paket | 7.69 | 5 | 120 x 60 x 60 cm | true | true |
| `PAK10` | 10 kg - Paket | 10.49 | 10 | 120 x 60 x 60 cm | true | true |
| `PAK20` | 20 kg - Paket | 18.99 | 20 | 120 x 60 x 60 cm | true | true |
| `PAK31` | 31,5 kg - Paket | 23.99 | 31.5 | 120 x 60 x 60 cm | true | true |

`category` is derived from the product name, so it is filled for every record above. Prices are the list prices shown on the website at the time of the run and may differ from what you pay at checkout or under a business contract.

### How much does it cost?

The Actor uses pay-per-event pricing: you are charged per result, meaning one charge for each record pushed to the dataset. There is no extra charge for `category`, because it is derived from the product name. See the Pricing tab of the Actor for the current rates. Compute and proxy costs are included in the event price. A default run reads a single page and returns seven records, so it is small.

### Use with AI agents (MCP)

You can call this Actor from an AI assistant through the Apify MCP server. Add it to your MCP client configuration with the Actor selected by the `tools` parameter:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=axiomworks/dhl-parcel-prices-scraper"
    }
  }
}
```

- "Get the current DHL Germany parcel prices and tell me which product is cheapest for a 5 kg parcel."
- "List all DHL products from the private-customer page that include tracking and liability."
- "Fetch the DHL international price page and show the price for Austria."

When asking an agent for Austria offers, supply the international price page and `destinationCountries: ["AUT"]`. Keep `maxItems` large enough to include the matching products. A small limit selects the first matching offers in page order; it does not select the cheapest or best shipment service. Use `maxWeightKg`, `maxDimensionsCm` and the feature flags in the returned records to make that comparison.

### Integrations and API

You can start the Actor from code, and connect it to schedules, webhooks and Apify integrations such as Google Sheets, Make, Zapier or n8n. Set your own API token in the `APIFY_TOKEN` environment variable.

**JavaScript**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('axiomworks/dhl-parcel-prices-scraper').call({
  startUrls: [{ url: 'https://www.dhl.de/de/privatkunden.html' }],
  maxItems: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
// Inspect items or pass them to your application.
```

**Python**

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("axiomworks/dhl-parcel-prices-scraper").call(run_input={
    "startUrls": [{"url": "https://www.dhl.de/de/privatkunden.html"}],
    "maxItems": 20,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(items)
```

**cURL**

```bash
curl -X POST "https://api.apify.com/v2/acts/axiomworks~dhl-parcel-prices-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls":[{"url":"https://www.dhl.de/de/privatkunden.html"}],"maxItems":20}'
```

### FAQ

**Where does `category` come from?**
It is derived in code from the product name: names with "Päckchen" give `packet`, "Paket" gives `parcel`, and letter products give `letter`. No AI is involved and nothing extra is charged.

**Does the Actor use AI?**
Only as a fallback. If the normal parser cannot read a product id, title or price from a card, the Actor may send the candidate text spans from that card to a third-party AI processor (Jev by TypeSafe AI) to pick the right one. This only happens when such a value is missing, is capped per run and is not charged separately.

**Which pages can I scrape?**
Pages on `dhl.de` or `www.dhl.de` that show parcel or packet prices: the private-customer home page and the national and international price pages, in German or English. URLs on other hosts are rejected.

**Why did I get fewer records than `maxItems`?**
`maxItems` is an upper limit. The private-customer home page lists seven distinct products, so a run with only that page returns seven records even if you ask for 20.

**Why is `liabilityIncluded` or `trackingIncluded` `false` or `null`?**
`false` means the page lists the product without that feature (the Päckchen products in the output above). `null` means the page does not say either way.

**Why is `destinationCountry` empty?**
It is only set on the international price page, where offers are listed per destination country and price zone. Domestic offers have `null` there.

**Are these the price I will pay?**
No. They are list prices as displayed on the website. They are not a complete rate table and do not include business contract prices or surcharges.

**A run failed. What should I check?**
Read the error in the run log. Invalid input, such as an empty URL list or a non-`dhl.de` URL, stops the run immediately with a message. A valid request with no matching offers succeeds with an empty dataset and a status message explaining which URLs or filters to change. Unavailable targets are logged and skipped. If the site changes its layout, report it through the Issues tab with the input and run link.

### Is it legal to scrape DHL Germany?

The Actor collects only publicly visible price information that loads without logging in, and it does not collect personal data. It requests few pages at a modest rate. Laws and site terms differ by country and use case, and the site's terms may restrict automated access or commercial reuse, so check them and take your own legal advice before using the data commercially. This is not legal advice.

### Feedback

If you find a bug, a page that returns no prices, or a field you would like added, open an issue from the Actor's Issues tab in Apify Console. Include the run link and the input you used so the problem can be reproduced.

# Actor input Schema

## `startUrls` (type: `array`):

Public DHL Germany page objects, e.g. \[{"url":"https://www.dhl.de/de/privatkunden.html"}]. Omit for the domestic catalogue. Only HTTP(S) URLs on dhl.de or www.dhl.de are accepted. Overview pages follow linked price pages; unavailable targets are skipped, and if all are unavailable the run succeeds with zero offers. For destinationCountries, supply an international price page.

## `maxItems` (type: `integer`):

Positive integer limiting matching offers across all startUrls, after filters and deduplication, e.g. 20. Default 50; 1-500 recommended. The homepage has 7 unique offers, so 20 returns 7 when unchanged. Results follow page order; this does not sort by price.

## `includeRawText` (type: `boolean`):

Set true to add rawText with visible product card or table-row text, e.g. {"includeRawText":true}. Default false omits rawText.

## `categories` (type: `array`):

Category strings to keep, e.g. \["packet"] or \["parcel","packet"]. Accepted values: packet, parcel, letter; comparisons ignore case. Default \[] keeps all. Combined with the other filters; unmatched values return no offers.

## `destinationCountries` (type: `array`):

International destination codes from DHL order links, e.g. \["AUT","FRA"]. Comparisons ignore case. Default \[] keeps all. Requires an international price page in startUrls: domestic offers have no destinationCountry and do not match this filter.

## `maxPriceEur` (type: `number`):

Nonnegative numeric ceiling for priceEur, e.g. 10. Omit for no price ceiling. Combined with categories and destinationCountries; a ceiling below all published prices returns zero offers successfully. This filters published offer prices, not a shipment quote.

## `proxyConfiguration` (type: `object`):

Proxy object, e.g. {"useApifyProxy":false} for direct requests (default). Configured proxies are used first. HTTP 403, 429, server errors or block pages retry with backoff on fresh sessions, escalating to Apify datacenter then residential Germany when available.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.dhl.de/de/privatkunden.html"
    }
  ],
  "maxItems": 20,
  "includeRawText": false,
  "categories": [],
  "destinationCountries": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `prices` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://www.dhl.de/de/privatkunden.html"
        }
    ],
    "maxItems": 20,
    "includeRawText": false,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("axiomworks/dhl-parcel-prices-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [{ "url": "https://www.dhl.de/de/privatkunden.html" }],
    "maxItems": 20,
    "includeRawText": False,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("axiomworks/dhl-parcel-prices-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://www.dhl.de/de/privatkunden.html"
    }
  ],
  "maxItems": 20,
  "includeRawText": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call axiomworks/dhl-parcel-prices-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,axiomworks/dhl-parcel-prices-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Yeh8hMHJAVrZ6FRSO/builds/lsGp64Vy7i4Oc1kGA/openapi.json
