Purpose
Track one or more shipment numbers through 17TRACK and return the carrier, current status, origin, destination, latest update date, latest scan, displayed arrival estimate, and available movement events.
When to Use
Use when the caller provides one or more tracking numbers and needs current 17TRACK results. Numbers may belong to different carriers and can be submitted together. Apply an optional carrier filter when the carrier is known or when 17TRACK needs help identifying the shipment.
Workflow
- Build the direct client-side results URL by joining the supplied numbers with commas:
https://t.17track.net/en#nums={tracking-number-1},{tracking-number-2},...If a carrier is known, append its 17TRACK carrier code in the hash:https://t.17track.net/en#nums={tracking-number-1},{tracking-number-2},...&fc={carrier-id} - Navigate directly to that URL with
domcontentloadedorload. - Start the client-side lookup by clicking
div#yq-tracking-search > button, then wait about 5–8 seconds for tracking cards to render. This initiation step is required even when tracking numbers andfcare already present in the hash. - If a tutorial or consent overlay appears, click
div[role='alertdialog'] > buttonuntil the overlay is gone; the first-run tutorial may require two dismiss clicks. - Evaluate the following function on the loaded page. It preserves input order, extracts the first displayed event as the latest scan, and returns the site's displayed arrival estimate when present:
(() => {
const numbers = location.hash.replace(/^#nums=/, '').split('&')[0].split(',').map(decodeURIComponent).filter(Boolean);
const clean = s => (s || '').replace(/\u00a0/g, ' ').replace(/[ \t]+/g, ' ').replace(/\n{2,}/g, '\n').trim();
const text = el => clean(el?.innerText || el?.textContent || '');
const all = [...document.querySelectorAll('body *')];
const findCard = number => {
const candidates = all.filter(el => {
const t = text(el);
return t.includes(number) && t.length > number.length && t.length <= 10000;
});
candidates.sort((a, b) => text(a).length - text(b).length);
return candidates[0] || null;
};
const labelled = (raw, labels) => {
const re = new RegExp('(?:' + labels.join('|') + ')\\s*[::]?\\s*([^\\n]+)', 'i');
return (raw.match(re)?.[1] || '').trim();
};
const normalizeDate = value => {
const m = value.match(/(20\d{2})[-/.](\d{1,2})[-/.](\d{1,2})/);
return m ? `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}` : value;
};
const eventRows = card => [...(card || document.body).querySelectorAll('li, tr, [class*="event"], [class*="timeline"], [class*="detail"]')]
.map(text)
.filter((s, i, a) => s && s.length > 8 && s.length < 1000 && a.indexOf(s) === i)
.filter(s => /20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}|picked up|departed|arrived|delivered|customs|shipment|delivery|out for delivery|配送|运输|到达/i.test(s));
return numbers.map(tracking => {
const card = findCard(tracking);
const rawText = text(card);
const lines = rawText.split(/\n+/).map(clean).filter(Boolean);
const events = eventRows(card).map(description => ({description}));
const status = labelled(rawText, ['status', 'latest status', 'current status', '最新状态']) || lines.find(s => /in transit|delivered|out for delivery|pending|exception|expired|not found|pre-transit|delivery/i.test(s)) || '';
const carrier = labelled(rawText, ['carrier', 'courier', '物流商']);
const origin = labelled(rawText, ['origin', 'from', '发件地']);
const destination = labelled(rawText, ['destination', 'to', '收件地']);
const arrivalEstimate = labelled(rawText, ['estimated delivery', 'delivery estimate', 'estimated arrival', 'expected delivery', 'arrival estimate', 'est\. delivery', '预计送达', '预计到达']);
const dates = [...rawText.matchAll(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/g)].map(m => normalizeDate(m[0]));
return {
tracking,
trackingNumber: tracking,
carrier,
status,
origin,
destination,
updated: dates[0] || '',
latestScan: events[0]?.description || '',
arrivalEstimate,
events,
rawText
};
});
})()Site-Specific Gotchas
- Tracking numbers are passed in the URL hash as
nums, separated by commas; loading/enalone does not preserve requested results. - The optional
fchash parameter supplies a 17TRACK carrier ID. The observed FedEx carrier selection usedfc=100003, producing URLs such ashttps://t.17track.net/en#nums={tracking-number}&fc=100003. - The hash alone does not always trigger the lookup. Click
div#yq-tracking-search > buttonafter navigation and allow the client-rendered cards time to populate. - A first-run tutorial can cover the results. The observed dismiss control is
div[role='alertdialog'] > button; it may need to be clicked twice. - 17TRACK renders results client-side, so wait for the tracking cards before evaluating the page.
latestScanis the first matching event in the rendered card, which normally corresponds to the newest event because the timeline is displayed newest-first; verify againstrawTextif the site changes event ordering.arrivalEstimatereports only an ETA explicitly displayed by 17TRACK. Do not invent a date when the site provides no estimate; use the latest scan and status to explain that an estimate is unavailable.- DOM class names may change between deployments. The evaluator anchors each result on its supplied tracking number, returns
rawText, and uses label-based fallbacks so callers can recover details when field labels or event classes change. - Preserve the caller's tracking-number order. If no matching card is found, return the requested number with empty parsed fields and any available page text.
Expected Output
Return an array with one object per requested number:
{tracking, trackingNumber, carrier, status, origin, destination, updated, latestScan, arrivalEstimate, events: [{description}], rawText}.
latestScan is the most recent tracking event; arrivalEstimate is the ETA the site displays — say when none is shown.