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

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

Under maintenance

Pricing

Pay per usage

Go to Apify Store
17Track Package Tracking — Real Events, 3,500+ Carriers

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

Under maintenance

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.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

DIOPSIDE AI

DIOPSIDE AI

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a month ago

Last modified

Categories

Security

Limited permissions

Share

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. 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.codeMeaning (from 17track's own bundle)
200success
-14solve the 9-image "pick the matching picture" captcha (/captcha/generate)
-11 / -13 / -8Cloudflare Turnstile / Tencent captcha / legacy captcha
-4hard refusal, returned to Apify's own platform IP range

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

EgressExit IPsResult
Apify platform IP (no proxy)1-4
Apify Proxy, datacenter3-14 captcha
Apify Proxy, RESIDENTIAL — US, GB, DE, CN, HK15-14 captcha
Ordinary cloud datacenter IP, plain curl / httpx / curl_cffi1-14 captcha
Ordinary cloud datacenter IP, real headless Chrome1-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.

{ "mode": "carrierSearch", "query": "DHL" }
{
"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

FieldTypeDefaultNotes
modestringbulkTrackbulkTrack, trackPackage, carrierSearch — the incumbent's values
trackingNumbersstring[][]One record per number, including numbers 17track has no data for
carrierstring""Carrier name, slug (canada-post) or numeric 17track key; empty = auto-detect. Any of the 3,522 directory carriers, not a 20-entry dropdown
querystringFedExCarrier search term (carrierSearch); empty returns everything
maxItemsinteger00 = no limit
batchSizeinteger20Numbers per upstream call
residentialFallbackbooleantrueEscalate to RESIDENTIAL once when the configured tier is captcha-walled
debugRawResponsebooleanfalseStore each raw 17track envelope as raw-response-N in the key-value store
proxyConfigurationobject{"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.