17Track Package Tracking — Real Events, 3,500+ Carriers
Under maintenancePricing
Pay per usage
17Track Package Tracking — Real Events, 3,500+ Carriers
Under maintenanceDrop-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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a month ago
Last modified
Categories
Security
Limited permissionsShare
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.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
statusis 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
| 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 == 200as 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" againstmeta.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: 100means 17track is still querying the carrier; those numbers are polled (4 × 4 s) and, if still unresolved, written asfound: false, resultCode: 100rather 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.