# 17Track Package Tracking — Real Events, 3,500+ Carriers (`diopside/seventeen-track`) Actor

Drop-in compatible with crawlerbros/seventeen-track-scraper — cheaper, and it returns real data. Full 17track event history: carrier, status, sub-status, origin/destination, ETA, transit days, staleness. Plus the whole 3,500-carrier directory. Unknown numbers return found:false, never demo events.

- **URL**: https://apify.com/diopside/seventeen-track.md
- **Developed by:** [DIOPSIDE AI](https://apify.com/diopside) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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

## 17Track Package Tracking — Real Events, 3,500+ Carriers

> **Status: not publishable. 17track's tracking API is captcha-walled on every exit IP we can buy.**
> See [Why this is not on the store](#why-this-is-not-on-the-store). The carrier-directory half of
> this actor works perfectly; the package-tracking half is blocked upstream, and the actor fails
> loudly rather than pretending otherwise. Built under DIO-22.

Drop-in compatible with `crawlerbros/seventeen-track-scraper`: same `mode` / `trackingNumbers` /
`carrier` / `query` / `maxItems` inputs, same `trackingNumber` / `carrier` / `carrierCode` /
`status` / `lastEvent` / `lastLocation` / `events[]` / `trackingUrl` / `scrapedAt` output columns,
with extra columns added alongside rather than renamed.

### Why this is not on the store

17track answers automated requests to `POST https://t.17track.net/track/restapi` with **HTTP 200 and
a well-formed JSON envelope** whose `meta.code` is a refusal:

| `meta.code` | Meaning (from 17track's own bundle) |
|---|---|
| `200` | success |
| `-14` | solve the 9-image "pick the matching picture" captcha (`/captcha/generate`) |
| `-11` / `-13` / `-8` | Cloudflare Turnstile / Tencent captcha / legacy captcha |
| `-4` | hard refusal, returned to Apify's own platform IP range |

Every egress we can reach was refused (measured 2026-09-22, DIO-22):

| Egress | Exit IPs | Result |
|---|---|---|
| Apify platform IP (no proxy) | 1 | `-4` |
| Apify Proxy, datacenter | 3 | `-14` captcha |
| Apify Proxy, RESIDENTIAL — US, GB, DE, CN, HK | 15 | `-14` captcha |
| Ordinary cloud datacenter IP, plain `curl` / `httpx` / `curl_cffi` | 1 | `-14` captcha |
| Ordinary cloud datacenter IP, **real headless Chrome** | 1 | `-14` captcha |

The last row is the decisive one. A real Chromium sends 17track's own device fingerprint — we
captured the outgoing request and confirmed a populated 340-character `sign` in the body plus the
`Last-Event-ID` cookie header — and is refused identically. So the gate is not the TLS fingerprint
and not the fingerprint token: **17track scores the exit IP**, and the only way through is an image
captcha per IP. That needs a third-party solving service, which is neither reliable nor something
we want to sell.

`https://www.17track.net/en/track?nums=...` 307-redirects to the same SPA and server-renders no
tracking data, so there is no sibling route. The sanctioned alternative is 17track's official API at
`api.17track.net`, which requires the caller's own registered API key.

### What the incumbent actually does

`crawlerbros/seventeen-track-scraper` is the cluster leader (19 users/30 d, 610 runs, one 5★ review)
and it does not scrape 17track at all. Running it with three tracking numbers returns:

- the canonical UPS *documentation* number `1Z999AA10123456784` → a full delivery history
  ("Delivered - Front Door/Porch, Dallas, TX"). Its own run log says `Using demo data for
  1Z999AA10123456784`.
- every other number → a placeholder row whose `status` is the literal string
  `"Lookup via 17track website"`.

It charges $0.005 per row for both. Its default `mode` is `carrierSearch`, not tracking, which is
why its published failure rate looks survivable. This is worth knowing before anyone concludes the
niche is open: it is not open, it is *unserved*, and the incumbent is billing for stubs.

### What does work

`mode: carrierSearch` reads 17track's **complete carrier directory** from
`https://res.17track.net/asset/carrier/info/carrier.all.js` — a public CDN asset, no token, no
captcha, no proxy needed. That is **3,522 carriers** with the numeric 17track carrier key, ISO
country, website, support e-mail and phone. The incumbent ships a hardcoded dropdown of 20.

```json
{ "mode": "carrierSearch", "query": "DHL" }
```

```json
{
  "carrierKey": 100001,
  "carrierName": "DHL Express",
  "slug": "dhl-express",
  "countryIso": "DE",
  "website": "https://mydhl.express.dhl/",
  "phone": "+49 228 76 26 30 00",
  "email": null,
  "group": "12",
  "scope": []
}
```

An empty `query` returns the whole directory.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `mode` | string | `bulkTrack` | `bulkTrack`, `trackPackage`, `carrierSearch` — the incumbent's values |
| `trackingNumbers` | string\[] | `[]` | One record per number, including numbers 17track has no data for |
| `carrier` | string | `""` | Carrier name, slug (`canada-post`) or numeric 17track key; empty = auto-detect. Any of the 3,522 directory carriers, not a 20-entry dropdown |
| `query` | string | `FedEx` | Carrier search term (`carrierSearch`); empty returns everything |
| `maxItems` | integer | `0` | 0 = no limit |
| `batchSize` | integer | `20` | Numbers per upstream call |
| `residentialFallback` | boolean | `true` | Escalate to RESIDENTIAL once when the configured tier is captcha-walled |
| `debugRawResponse` | boolean | `false` | Store each raw 17track envelope as `raw-response-N` in the key-value store |
| `proxyConfiguration` | object | `{"useApifyProxy": true}` | |

### Output (tracking mode)

Incumbent-compatible columns first, then the additions:

`trackingNumber`, `carrier`, `carrierCode`, `status`, `lastEvent`, `lastLocation`, `events[]`,
`trackingUrl`, `scrapedAt` — then `found`, `resultCode`, `subStatus`, `subStatusDescription`,
`lastEventAt`, `lastEventStage`, `carrierKey`, `secondCarrierKey`, `originCountry`,
`destinationCountry`, `originAddress`, `destinationAddress`, `estimatedDeliveryFrom`,
`estimatedDeliveryTo`, `daysAfterOrder`, `daysOfTransit`, `daysAfterLastUpdate`, `milestones`,
`serviceType`, `weightRaw`, `weightKg`, `pieces`, `referenceNumber`, `eventCount`, `providers[]`,
`providerCount`.

`daysAfterLastUpdate` is the column e-commerce ops actually want — it is how you find the parcels
that are stuck rather than merely in transit.

**Personal data is deliberately dropped.** 17track echoes whatever the carrier published, which for
some carriers includes a recipient street line and postcode. `originAddress` / `destinationAddress`
are cut down to country/state/city on the way out; street, postcode and coordinates are never
written to the dataset.

### Reliability notes

- Every refusal arrives as HTTP 200 with valid JSON. The actor treats **only** `meta.code == 200` as
  data; anything else raises, and a run that produced no records **fails** instead of writing a
  blank row per number and reporting SUCCEEDED. The first build of this actor did exactly that — it
  reported "3 records" against `meta.code -4` — which is how the check earned its place.
- Captcha codes move the proxy ladder (rotate twice in-tier, then escalate to RESIDENTIAL once);
  other refusals do not, because a fresh exit IP cannot fix a schema change and laddering would
  just spend the buyer's residential bandwidth on the same error.
- Nothing is hardcoded: the carrier directory, and therefore every carrier key, is discovered at run
  time.
- `code: 100` means 17track is still querying the carrier; those numbers are polled (4 × 4 s) and, if
  still unresolved, written as `found: false, resultCode: 100` rather than dropped. N numbers in
  always gives N records out.

### Tests

`pytest tests` — 31 tests, all passing. They cover the carrier-directory parse (including the UMD
wrapper trap, where anchoring on the first `[` in the file grabs `global['ResGCarrier']` instead of
the payload), carrier resolution across every input dialect, the refusal taxonomy, the
address-redaction rule, and the record-shape invariants.

The tracking-response fixture is **hand-built from the field names read out of 17track's front-end
bundle**, not a recorded live response — no exit IP we could reach ever returned a populated
envelope. The tests pin the mapping; they do not prove it against live data. Anyone who unblocks the
egress should re-run with `debugRawResponse: true` and diff the stored envelope against
`tests/fixtures/shipment.json` before trusting the parser.

There is no `canary.json`: a health check against a surface that refuses every request would report
BAD forever.

# Actor input Schema

## `mode` (type: `string`):

What to do with 17Track.

## `trackingNumbers` (type: `array`):

Tracking numbers to look up (modes trackPackage and bulkTrack). One record is written per number, including numbers 17track has no data for.

## `carrier` (type: `string`):

Force a carrier instead of letting 17track auto-detect. Accepts a carrier name ("DHL Express"), a slug ("canada-post"), or a numeric 17track carrier key. Any of the ~2,500 carriers in 17track's directory works — run mode=carrierSearch to find one. Leave empty for auto-detection.

## `query` (type: `string`):

Search 17track's carrier directory by name, slug or country code. Leave empty to return the whole directory.

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

Maximum number of records to write (and therefore to be charged for). 0 means no limit.

## `batchSize` (type: `integer`):

How many numbers to send to 17track in one call. The website itself sends them all at once; smaller batches retry more cheaply.

## `debugRawResponse` (type: `boolean`):

Write each raw 17track envelope to the key-value store as raw-response-N. Use this when a run returns unexpected fields so a fix can be a diff rather than a re-probe.

## `residentialFallback` (type: `boolean`):

17track scores the exit IP and answers datacenter addresses with a captcha. When every IP in the configured tier is walled, retry once on RESIDENTIAL. Turn this off to cap proxy spend at the datacenter rate.

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

17track blocks most datacenter IPs with a captcha, so a proxy is required and RESIDENTIAL is the tier that reliably works. The default below starts on Apify Proxy and escalates automatically.

## Actor input object example

```json
{
  "mode": "bulkTrack",
  "trackingNumbers": [
    "9400111899223197428490",
    "1Z999AA10123456784"
  ],
  "carrier": "",
  "query": "FedEx",
  "maxItems": 50,
  "batchSize": 20,
  "debugRawResponse": false,
  "residentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `packages` (type: `string`):

All records. Append ?format=csv for CSV.

## `datasetUrl` (type: `string`):

The default dataset.

# 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 = {
    "mode": "bulkTrack",
    "trackingNumbers": [
        "9400111899223197428490",
        "1Z999AA10123456784"
    ],
    "carrier": "",
    "query": "FedEx",
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("diopside/seventeen-track").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 = {
    "mode": "bulkTrack",
    "trackingNumbers": [
        "9400111899223197428490",
        "1Z999AA10123456784",
    ],
    "carrier": "",
    "query": "FedEx",
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("diopside/seventeen-track").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 '{
  "mode": "bulkTrack",
  "trackingNumbers": [
    "9400111899223197428490",
    "1Z999AA10123456784"
  ],
  "carrier": "",
  "query": "FedEx",
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call diopside/seventeen-track --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,diopside/seventeen-track"
        }
    }
}
```

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/AZZ570fgeN5OvDfbQ/builds/QUqSSPQ7ViohMxJDH/openapi.json
