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

  1. 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.
  2. 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};
})();
  1. 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

ParamWhat it doesExample value
tracking-numbersOne identifier or comma-separated identifiers inserted into tracking-id1234567890,00340434161094000001
localeCountry-page locale used in the tracking URLus-en
fallback-localeFallback country-neutral page when the selected locale does not resolve the shipmentglobal-en

Possible Friction Points

TriggerAction
Cookie consent dialog blocks the pageClick button#onetrust-reject-all-handler, then continue after it disappears or the result renders
Country deep link leaves the prefilled form unsubmittedSubmit button[type='submit']; if needed, use the direct query-parameter URL again
Initial session does not render the result or precise errorRestart the BQL session with a US residential proxy and repeat the direct deep link
Tracking page has not rendered result text or event rowsWait for .c-tracking-result--container.is-visible, .c-multiple-tracking-result--container, #panel_events tr, #panel_details, or .c-tracking-result-message--content
Locale page does not resolve the shipmentRetry https://www.dhl.com/global-en/home/tracking.html?tracking-id={tracking-numbers}&submit=1
Combined request returns a multiple-match listingUse 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 identifierReturn a separate not-found record for that identifier; do not copy details from another record
Result displays a tracking code different from the requested numberSet identity_mismatch: true, return no shipment details or events, and report both identities
A bogus number renders event panels for another recordValidate the displayed tracking code before accepting panel data
A prefilled tracker value appends text when replacedUse a fresh navigation with the identifiers in the URL instead of typing into the existing field
A long identifier produces a rendered validation messageRead .c-tracking-result-message--content, classify as validation, and return no shipment details
In-page Track navigation leaves the submitted result unchanged or times outNavigate directly to https://www.dhl.com/{locale}/home/tracking.html, then use the query-parameter URL again

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=dhl.com&task=track-dhl-shipment