Track DHL shipment and extract full event timeline
Site dhl.comTask track-dhl-shipmentVersion v10Updated Sep 16, 2026Category logistics
Track one or more DHL shipment numbers and return separate shipment records with statuses, routes, services, latest locations, scan timelines, validation state, and unresolved identifiers. This skill was captured from a live agent session on dhl.com and is published here as a reusable recipe for agents.
NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.
Track DHL shipment numbers through the public tracker, including comma-separated batch requests, and return one record per requested identifier without fabricating details when DHL reports not-found or multiple-match results.
Use Cases
Track one parcel by number
Track several parcels in one tracker request
Capture a 20-digit DHL eCommerce shipment
Preserve validation, not-found, or multiple-match states
Extract shipment details and a chronological scan history
Automation Flow
Build https://www.dhl.com/{locale}/home/tracking.html?locale=true&tracking-id={tracking-numbers}&submit=1, URL-encoding comma-separated identifiers; use global-en when no country locale is known.
In one BQL call, goto the URL, reject button#onetrust-reject-all-handler if present, wait for .c-tracking-result--container.is-visible, .c-multiple-tracking-result--container, #panel_events tr, #panel_details, or .c-tracking-result-message--content, then run this evaluate() on the loaded page:
(() => {
const clean=v=>(v||'').replace(/\s+/g,' ').trim()||null;
const root=document.querySelector('main, .c-tracking--container')||document.body;
const text=root.innerText||'';
const lines=text.split(/\r?\n/).map(clean).filter(Boolean);
const requested=(new URL(location.href).searchParams.get('tracking-id')||'').split(',').map(x=>decodeURIComponent(x).trim()).filter(Boolean);
const message=clean(document.querySelector('.c-tracking-result-message--content')?.innerText)||lines.find(x=>/not found|could not be found|unable to find|no shipment|tracking attempt was not successful|multiple match|multiple shipment|multiple numbers|select (?:a|your) shipment|invalid|enter a valid|tracking number|required|must contain|too long|maximum|characters/i.test(x))||null;
const displayed=clean((text.match(/Tracking Code:?\s*([^\n]+)/i)||[])[1])?.replace(/\s+/g,'')||null;
const notFound=/not found|could not be found|unable to find|no shipment|tracking attempt was not successful|sorry, your tracking attempt was not successful/i.test(text);
const multiple=/multiple match|multiple shipment|multiple numbers|more than one result|select (?:a|your) shipment/i.test(text)||!!document.querySelector('.c-multiple-tracking-result--container');
const validation=/invalid|validation|enter a valid|tracking number|required|must contain|too long|maximum|characters/i.test(text)&&!multiple&&!notFound;
const details=document.querySelector('#panel_details');
const detailText=clean(details?.innerText)||'';
const label=(source,name)=>{const e=name.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\\\$&');const r=new RegExp('(?:^|\\\\s)'+e+'\\\\s*:?[\\\\s]+(.+?)(?=\\\\s+(?:Total Pieces|Service|Weight|Reference|Local Tracking Number|$))','i');return clean(source.match(r)?.[1]);};
const rows=[...document.querySelectorAll('#panel_events tr')].filter(r=>r.querySelector('.c-event-log--date'));
const events=rows.map((row,i)=>{const cells=[...row.children].map(c=>clean(c.innerText));return {date:clean(row.querySelector('.c-event-log--date')?.innerText)||cells[0]||null,local_time:clean(row.querySelector('.c-event-log--localtime')?.innerText)||cells[1]||null,status:clean(row.querySelector('td.c-event-log--status > .c-event-log--status-content > .c-event-log--status')?.innerText)||clean(row.querySelector('.c-event-log--status')?.innerText)||cells[2]||null,location:clean(row.querySelector('.c-event-log--location')?.innerText)||cells[3]||null,rating:clean(row.querySelector('[class*="rating"], [data-rating]')?.innerText)||null,_i:i};}).filter(e=>e.date||e.local_time||e.status||e.location);
const parsed=e=>Date.parse(`${e.date||''} ${e.local_time||''}`);
events.sort((a,b)=>{const x=parsed(a),y=parsed(b);return Number.isNaN(x)||Number.isNaN(y)?a._i-b._i:x-y;});events.forEach(e=>delete e._i);
const line=name=>{const e=name.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\\\$&');const r=new RegExp('^'+e+'\\\\s*:?[\\\\s]*(.*)$','i');const hit=lines.find(x=>r.test(x));return clean(hit?.match(r)?.[1]);};
const cards=[...document.querySelectorAll('.c-multiple-tracking-result--container .js--tracking-result-overview--box')].map(card=>{const t=clean(card.innerText)||'';const codes=[...new Set((t.match(/\b\d{8,30}\b/g)||[]))];return {tracking_number:codes[0]||null,status:clean(t.match(/(?:Status|Shipment status)\s*:?\s*(.*?)(?=\s+(?:Tracking|Origin|Destination|Service|$))/i)?.[1])||null,text:t};}).filter(x=>x.tracking_number||x.text);
const candidateNumbers=[...new Set(cards.map(c=>c.tracking_number).filter(Boolean))];
const invalid=notFound||multiple||validation||!!(displayed&&requested.length===1&&displayed!==requested[0]);
const normal={requested_tracking_number:requested[0]||null,input_value:document.querySelector('#c-tracking--input')?.value||null,tracking_number:displayed||requested[0]||null,result_state:multiple?'multiple-match':(notFound?'not-found':(validation?'validation':(events.length?'result':'empty-result'))),precise_message:message,candidate_tracking_numbers:multiple?candidateNumbers:[],not_found:notFound,identity_mismatch:!!(displayed&&requested.length===1&&displayed!==requested[0]),multiple_match:multiple,validation_error:validation,status:invalid?null:line('Status'),handler:invalid?null:line('This shipment is handled by'),service:invalid?null:(label(detailText,'Service')||clean(details?.querySelector('dt')?.nextElementSibling?.innerText)),origin:invalid?null:line('Origin'),destination:invalid?null:line('Destination'),last_update:invalid?null:line('Last Update'),last_location:invalid?null:(events.at(-1)?.location||null),local_tracking_number:invalid?null:label(detailText,'Local Tracking Number'),weight:invalid?null:label(detailText,'Weight'),total_pieces:invalid?null:label(detailText,'Total Pieces'),expected_delivery:invalid?null:clean((text.match(/Expected(?: delivery| delivery date)?:\s*([^\n]+)/i)||[])[1]),events_count:invalid?0:events.length,events:invalid?[]:events};
const records=requested.map(id=>{const card=cards.find(c=>c.tracking_number===id);if(card)return {...normal,requested_tracking_number:id,tracking_number:id,result_state:'multiple-match',precise_message:message,candidate_tracking_numbers:candidateNumbers,multiple_match:true,not_found:false,validation_error:false,status:card.status,service:null,origin:null,destination:null,last_update:null,last_location:null,events_count:0,events:[]};if(requested.length>1&&!displayed&&/not found|could not be found|unable to find|no shipment/i.test(text))return {...normal,requested_tracking_number:id,tracking_number:id,result_state:'not-found',not_found:true,events_count:0,events:[],status:null,service:null,origin:null,destination:null,last_update:null,last_location:null};return requested.length===1?normal:{...normal,requested_tracking_number:id,tracking_number:id};});
return {requested_tracking_numbers:requested,records};
})();
Return each records entry separately; for not-found, identity-mismatch, multiple-match, validation, or empty-result, preserve the requested identity, precise message, and candidate codes while returning null shipment fields and no inferred events.
Params
Param
What it does
Example value
tracking-numbers
One identifier or comma-separated identifiers inserted into tracking-id
1234567890,00340434161094000001
locale
Country-page locale used in the tracking URL
us-en
fallback-locale
Fallback country-neutral page when the selected locale does not resolve the shipment
global-en
Possible Friction Points
Trigger
Action
Cookie consent dialog blocks the page
Click button#onetrust-reject-all-handler, then continue after it disappears or the result renders
Country deep link leaves the prefilled form unsubmitted
Submit button[type='submit']; if needed, use the direct query-parameter URL again
Initial session does not render the result or precise error
Restart the BQL session with a US residential proxy and repeat the direct deep link
Tracking page has not rendered result text or event rows
Wait for .c-tracking-result--container.is-visible, .c-multiple-tracking-result--container, #panel_events tr, #panel_details, or .c-tracking-result-message--content
Use the overview-card selector, preserve candidate codes and card statuses, and leave route, service, location, and events null until DHL exposes a selected candidate
Combined request contains one unresolved identifier
Return a separate not-found record for that identifier; do not copy details from another record
Result displays a tracking code different from the requested number
Set identity_mismatch: true, return no shipment details or events, and report both identities
A bogus number renders event panels for another record
Validate the displayed tracking code before accepting panel data
A prefilled tracker value appends text when replaced
Use a fresh navigation with the identifiers in the URL instead of typing into the existing field
A long identifier produces a rendered validation message
Read .c-tracking-result-message--content, classify as validation, and return no shipment details
In-page Track navigation leaves the submitted result unchanged or times out
Navigate directly to https://www.dhl.com/{locale}/home/tracking.html, then use the query-parameter URL again