<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 24hTrack</title>
    <description>The latest articles on DEV Community by 24hTrack (@support24htrack).</description>
    <link>https://dev.to/support24htrack</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4131848%2F4c74856b-d882-4c69-a7ea-08ed099195ad.png</url>
      <title>DEV Community: 24hTrack</title>
      <link>https://dev.to/support24htrack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/support24htrack"/>
    <language>en</language>
    <item>
      <title>Your tracking integration needs a date sanity check</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Mon, 12 Oct 2026 04:34:43 +0000</pubDate>
      <link>https://dev.to/support24htrack/your-tracking-integration-needs-a-date-sanity-check-1n3b</link>
      <guid>https://dev.to/support24htrack/your-tracking-integration-needs-a-date-sanity-check-1n3b</guid>
      <description>&lt;p&gt;Every tracking integration I have seen trusts one thing it should not: that the newest event attached to a tracking number describes the shipment you are looking at.&lt;/p&gt;

&lt;p&gt;Most of the time it does. Then a support ticket arrives saying "it says delivered in Texas in March and I ordered this in October", and you discover that a tracking number is a string in a finite format, not a permanent name for a box.&lt;/p&gt;

&lt;p&gt;This post is the guard I wish every integration shipped with: &lt;strong&gt;compare the event date to the date you learned about the shipment, and refuse to surface anything that predates it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure, concretely
&lt;/h2&gt;

&lt;p&gt;A customer pastes a US domestic number. Your UI shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delivered, In/At Mailbox — SPRING, TX — 2026-03-03 14:42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They ordered on 2026-10-09. The carrier's own website, asked about the same number on the same morning, says tracking not available.&lt;/p&gt;

&lt;p&gt;Nothing is broken. Two different records are being shown by two systems with different retention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The carrier's own site has moved on. USPS states on its help pages that "access to tracking information for non-signature items is limited to 120 days. Tracking information for signature items is available for 2 years."&lt;/li&gt;
&lt;li&gt;Whatever third party showed the March delivery still has its own stored copy from back then.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the number is live again on a new label, and the only system still holding the old story is the one that cached it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your own database will do this too
&lt;/h2&gt;

&lt;p&gt;If you ingest events and upsert them onto a &lt;code&gt;tracking_number&lt;/code&gt; primary key, you have built the same cache. The second shipment's events land next to the first shipment's events, sorted by whatever you sort by, and the newest row wins the status badge. There is no field in the payload that says "different parcel".&lt;/p&gt;

&lt;p&gt;What you do have is the moment the number entered your system. That is the anchor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Decide whether an event can belong to the shipment we are tracking.
 * anchor = when this tracking number entered our system (order/intake time).
 * Anything meaningfully older than the anchor belongs to a previous use
 * of the same number, or to a carrier record that has since been recycled.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STALE_DAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseEventDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Carrier strings are not ISO. Cut trailing timezone notes like "[GMT+8]",&lt;/span&gt;
  &lt;span class="c1"&gt;// and normalise "2026-03-03 14:42:00" which Safari refuses as-is.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[[^\]]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;partitionEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anchorISO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;staleDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;STALE_DAYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anchorISO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;staleDays&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;864&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;priorUse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseEventDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// unparseable: do not hide it&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;priorUse&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priorUse&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;statusFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anchorISO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priorUse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;partitionEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anchorISO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Everything we hold predates this shipment. Say so instead of lying.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No scans yet for this shipment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;priorUse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This number has older history from a previous use. It is not your parcel.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it on the case above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-03-03 14:42:00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Delivered, In/At Mailbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-03-03 10:28:00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Arrived at Post Office&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nf"&gt;statusFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-09T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { status: 'No scans yet for this shipment',&lt;/span&gt;
&lt;span class="c1"&gt;//   stale: true,&lt;/span&gt;
&lt;span class="c1"&gt;//   note: 'This number has older history from a previous use. It is not your parcel.' }&lt;/span&gt;

&lt;span class="nf"&gt;statusFor&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-11 08:02:00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;In transit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-09T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { status: 'In transit', stale: false }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Four details that bite
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Do not hide events you failed to parse.&lt;/strong&gt; Carrier date strings are not a standard. We have seen &lt;code&gt;2026-01-31 16:42:56 [GMT+8]&lt;/code&gt;, bare &lt;code&gt;03/03/2026&lt;/code&gt;, and localised month names. If &lt;code&gt;parseEventDate&lt;/code&gt; returns null, keep the event. A parser bug must not silently delete a customer's history — fail open, not closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pick the anchor you actually have.&lt;/strong&gt; Order creation time is ideal. If you only have the moment the number was first submitted to you, use that, and widen &lt;code&gt;STALE_DAYS&lt;/code&gt;, because a buyer may paste a number days after ordering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Thirty days is a deliberate overshoot.&lt;/strong&gt; Real shipments go quiet for weeks. A cross-border parcel can sit in customs longer than anyone expects. You are not trying to catch slow parcels — you are trying to catch a delivery that happened in a different season.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Keep the old events, just do not promote them.&lt;/strong&gt; Delete nothing. Partition, and render the prior-use block collapsed and labelled. The customer who sees "this number had earlier history that is not yours" stops filing a theft report.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often is this worth coding for?
&lt;/h2&gt;

&lt;p&gt;Rarer than you would guess from how loud it is. Across the tracking numbers added to our platform in the last two months, roughly &lt;strong&gt;one in five hundred&lt;/strong&gt; had its newest scan dated more than two months before the number ever reached us.&lt;/p&gt;

&lt;p&gt;That is a tiny slice. It is also, every single time, a customer who believes their parcel was delivered to a stranger in another state — which is why the ticket takes twenty minutes instead of two, and why the guard is worth the forty lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;If the newest event you hold is older than the shipment itself, you are displaying somebody else's parcel. Compare the dates before you render the badge.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt;, a free package tracker for 3,200+ carriers — paste any tracking number, the carrier is detected automatically, no sign-up. The measurement above comes from our own data. The USPS retention figures are from USPS's own help pages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
    </item>
    <item>
      <title>Why "Delivered" Is the Least Reliable Field in a Tracking API Response</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Sun, 11 Oct 2026 04:40:01 +0000</pubDate>
      <link>https://dev.to/support24htrack/why-delivered-is-the-least-reliable-field-in-a-tracking-api-response-2joa</link>
      <guid>https://dev.to/support24htrack/why-delivered-is-the-least-reliable-field-in-a-tracking-api-response-2joa</guid>
      <description>&lt;p&gt;If you consume a parcel-tracking API, the field you are most tempted to branch on is &lt;code&gt;status&lt;/code&gt;. It is also the field most likely to be wrong about the thing your user actually cares about: whether the parcel is at their door.&lt;/p&gt;

&lt;p&gt;This is not a data-quality problem. It is a modelling problem, and once you see it the fix is small.&lt;/p&gt;

&lt;h2&gt;
  
  
  One parcel, several authors
&lt;/h2&gt;

&lt;p&gt;A cross-border parcel is usually carried by three different companies: the line the seller booked, the operator that moves it across the border and clears customs, and the local carrier that makes the doorstep delivery. Each one writes scans into its own system, in its own words.&lt;/p&gt;

&lt;p&gt;When company B hands the parcel to company C, company B records an event. From B's point of view, it delivered the parcel. To its partner.&lt;/p&gt;

&lt;p&gt;So the scan list contains a line that reads, quite literally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delivered to final mile carrier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and a naive &lt;code&gt;status == "Delivered"&lt;/code&gt; check marks the order complete while the box is sitting in a depot forty miles away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The status field is a derived label, the scan list is the record
&lt;/h2&gt;

&lt;p&gt;Most tracking APIs (ours included) expose two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a normalised &lt;code&gt;status&lt;/code&gt; - a small enum like &lt;code&gt;in_transit | out_for_delivery | delivered | exception&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the raw &lt;code&gt;events&lt;/code&gt; array - what each carrier actually published, with a timestamp and a location&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The enum has to be produced by mapping carrier text onto buckets, and carrier text is written by whoever wrote it. "Delivered to final mile carrier", "Delivered to agent", "Delivered to service point" and "Delivered, front door" all contain the same verb and mean three different things.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;events&lt;/code&gt; array does not have this problem, because it carries the one field that disambiguates: &lt;strong&gt;location&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disambiguation rule
&lt;/h2&gt;

&lt;p&gt;For the latest event, look at the location, not the status word:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location looks like&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;UI you should render&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a facility, depot, hub, airport, or another company's name&lt;/td&gt;
&lt;td&gt;handover between carriers&lt;/td&gt;
&lt;td&gt;still in transit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a shop, locker, access point, parcel shop&lt;/td&gt;
&lt;td&gt;awaiting collection&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;action required by the customer&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the recipient's own town, often with a place note (front door, porch, mailbox, neighbour)&lt;/td&gt;
&lt;td&gt;real delivery&lt;/td&gt;
&lt;td&gt;delivered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle row is the one worth special-casing. It is not in transit and it is not finished - the parcel is physically done travelling but a clock is running, because pickup points typically return a parcel to the sender after about a week. Collapsing it into &lt;code&gt;delivered&lt;/code&gt; is how a support queue fills up with "it says delivered and my porch is empty", and collapsing it into &lt;code&gt;in_transit&lt;/code&gt; is how a customer finds out too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pragmatic implementation
&lt;/h2&gt;

&lt;p&gt;You do not need NLP for this. A short rule over the latest event gets most of the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HANDOVER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;final&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;- &lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;mile|local post|delivery partner|partner carrier|tendered|transferr&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;ed to&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PICKUP&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;service &lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;point|access &lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;point|parcel &lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;shop|pickup point|locker|post office&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PICKUP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;awaiting_collection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HANDOVER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in_transit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things that matter more than the regex itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep the raw text.&lt;/strong&gt; If you only persist your own enum, you have thrown away the evidence and you cannot re-derive anything later when a carrier changes its wording. Store the carrier's string verbatim and normalise on read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not match on "locker" or "mailbox" alone.&lt;/strong&gt; A parcel locker owned by the &lt;em&gt;recipient&lt;/em&gt; is a real delivery - a rule that treats every locker as a pickup point will mislabel a very large number of genuinely delivered domestic parcels. Anchor on the &lt;em&gt;shop or network&lt;/em&gt; words instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second point is not hypothetical: in a sample we checked, the single largest group of "delivered + locker" scans were ordinary residential parcel lockers, not pickup points.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second tracking number
&lt;/h2&gt;

&lt;p&gt;One more consequence of the handover. After it, the destination carrier may issue its own tracking number for the same parcel. Both are valid, both describe the same box, and neither one contains the whole journey. If your UI assumes one order has one number, that is where the trail appears to die.&lt;/p&gt;

&lt;p&gt;If you are building this: 24hTrack (&lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;https://www.24htrack.com&lt;/a&gt;) is a free package tracker for 3,200+ carriers - paste any tracking number and the carrier is detected automatically, no sign-up. There is a REST API and an MCP server (&lt;code&gt;npm i 24htrack-mcp&lt;/code&gt;) if you want an AI agent to do the lookup, and the open guides are at &lt;a href="https://github.com/doanhnd9989/package-tracking-guide" rel="noopener noreferrer"&gt;https://github.com/doanhnd9989/package-tracking-guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;status&lt;/code&gt; tells you what the last company to touch the parcel thought it had finished. The &lt;code&gt;events&lt;/code&gt; array, read with its location field, tells you where the parcel is. Branch on the second one.&lt;/p&gt;

</description>
      <category>api</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why two tracking pages disagree about the same parcel (and what it means for your integration)</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Thu, 08 Oct 2026 05:07:06 +0000</pubDate>
      <link>https://dev.to/support24htrack/why-two-tracking-pages-disagree-about-the-same-parcel-and-what-it-means-for-your-integration-156j</link>
      <guid>https://dev.to/support24htrack/why-two-tracking-pages-disagree-about-the-same-parcel-and-what-it-means-for-your-integration-156j</guid>
      <description>&lt;p&gt;Two tracking pages, one parcel, two different answers. A customer emails you a screenshot of a competitor's page showing a scan yours doesn't have, and asks why your order status is "behind".&lt;/p&gt;

&lt;p&gt;I work on 24hTrack, a free multi-carrier tracking site, and this is the support ticket we get in every language. It is almost never a data bug. It is four assumptions that quietly break once a parcel crosses a border — and all four show up in code, not just in support.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A tracking record is a snapshot, not a feed
&lt;/h2&gt;

&lt;p&gt;The carrier is the only writer. Everything downstream — your order page, your 3PL dashboard, any tracking site — is a copy of what the carrier had published at the moment that copy was made. Two copies made minutes apart can legitimately disagree.&lt;/p&gt;

&lt;p&gt;So "their page is ahead of ours" usually means their copy is newer, not that one of you is wrong. Design your UI for that: show &lt;em&gt;when&lt;/em&gt; you last looked, not just the status. A timestamp on the page kills more WISMO tickets than an extra status string, because it answers the question the customer is actually asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. One shipment is often two numbers
&lt;/h2&gt;

&lt;p&gt;This is the big one. On a cross-border order, one company carries the parcel out of the country it was posted in, and the postal carrier where the buyer lives delivers it — frequently under a second, local number the first carrier never sees.&lt;/p&gt;

&lt;p&gt;The handover is visible in the data. On Belgian postal items we see lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Export customs clearance started
Departure from office of exchange
Left from departure country/region
Arrival in the country of destination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;Arrival in the country of destination&lt;/code&gt;, the original number often goes quiet forever. Nothing is broken; the parcel simply stopped being that carrier's to move. If your UI treats "no new events for N days" as an exception, you will raise false alarms on every international order that is doing fine.&lt;/p&gt;

&lt;p&gt;Model a shipment as &lt;strong&gt;one or more tracking legs&lt;/strong&gt;, not one number. If you can only store one, at least don't present its silence as a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The last two letters are the issuing country — not the destination
&lt;/h2&gt;

&lt;p&gt;S10 is the UPU format: two letters, nine digits, two letters. The trailing pair identifies the postal operator that &lt;strong&gt;issued&lt;/strong&gt; the number.&lt;/p&gt;

&lt;p&gt;I have seen this used as a destination field more than once — routing a shipment to a "Belgium" bucket because the number ends in &lt;code&gt;BE&lt;/code&gt;, when the parcel is going to Brazil. It also drives a support script that tells customers they have the wrong number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// S10 (UPU) postal item number: 2 letters + 9 digits + 2 letters.&lt;/span&gt;
&lt;span class="c1"&gt;// The LAST two letters are the ISSUING country, not the destination.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;S10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{2})(\d{9})([&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{2})&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseS10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;S10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;issuedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Name the field &lt;code&gt;issuedBy&lt;/code&gt;. Not &lt;code&gt;country&lt;/code&gt;. Not &lt;code&gt;destination&lt;/code&gt;. The name is the whole fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. One carrier can have more than one number format
&lt;/h2&gt;

&lt;p&gt;Length-based validation is the most common way a valid number gets rejected at the door. Canada Post publishes two formats on its own support pages: &lt;strong&gt;16 digits&lt;/strong&gt; for items delivered inside Canada, and &lt;strong&gt;13 characters&lt;/strong&gt; (S10) for items going to the US and internationally, plus prepaid envelopes and labels.&lt;/p&gt;

&lt;p&gt;If your input accepts "a Canada Post number" as 16 digits, every prepaid-label and international customer hits an error on a number that is perfectly valid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;looksLikeCanadaPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{16}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;domestic-16-digit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseS10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuedBy&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;international-s10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the two together over a mixed bag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1111 1111 1111 1111    | s10: no                | canadaPost: domestic-16-digit
AA111111111CA          | s10: AA+9+CA           | canadaPost: international-s10
UL000000000BE          | s10: UL+9+BE           | canadaPost: no
CP000000000IE          | s10: CP+9+IE           | canadaPost: no
LX000000000NL          | s10: LX+9+NL           | canadaPost: no
1Z999AA10123456784     | s10: no                | canadaPost: no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the last line: a UPS-shaped number is not S10 and not Canada Post, and that is the correct answer — &lt;code&gt;null&lt;/code&gt;, not an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule underneath all four
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shape tells you very little, and never block a lookup on it.&lt;/strong&gt; Detection should return &lt;em&gt;carrier&lt;/em&gt;, &lt;em&gt;unknown&lt;/em&gt;, or &lt;em&gt;ambiguous&lt;/em&gt; — and &lt;code&gt;unknown&lt;/code&gt; should still run the lookup rather than throwing a validation error at the customer. A person holding a real number does not care that it failed your regex.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to show instead
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The timestamp of the last check, next to the status.&lt;/li&gt;
&lt;li&gt;The newest &lt;strong&gt;line&lt;/strong&gt;, not just a normalised status bucket. "Electronic information submitted by shipper" means the label exists and the carrier does not have the parcel yet — a completely different support action from "In transit".&lt;/li&gt;
&lt;li&gt;An explicit note at the handover point: the final leg may be another company.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to sanity-check a number against a lot of carriers without wiring anything up, you can paste it on &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; — it detects the carrier from the number itself, free and without an account. There is also a &lt;a href="https://www.24htrack.com/api" rel="noopener noreferrer"&gt;REST API and an MCP server&lt;/a&gt; if you want this inside your own app or an AI assistant.&lt;/p&gt;

&lt;p&gt;Numbers quoted here are medians and shares measured on our own platform, and the Canada Post formats come from Canada Post's published support pages.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance. I work on 24hTrack.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>api</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Carrier names are not identifiers: 4 rules for shipping integrations</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Wed, 07 Oct 2026 04:31:05 +0000</pubDate>
      <link>https://dev.to/support24htrack/carrier-names-are-not-identifiers-4-rules-for-shipping-integrations-14n7</link>
      <guid>https://dev.to/support24htrack/carrier-names-are-not-identifiers-4-rules-for-shipping-integrations-14n7</guid>
      <description>&lt;p&gt;Every shipping integration I have seen stores a &lt;code&gt;carrier&lt;/code&gt; field, and most of them treat it as if it were part of the shipment's identity. It is not. It is the least stable thing on the record.&lt;/p&gt;

&lt;p&gt;Here is the evidence I keep running into, and the four design rules I would now apply from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One carrier can have two correct names
&lt;/h2&gt;

&lt;p&gt;YTO Express is 圆通速递 — Yuantong Express, based in Shanghai, listed in Shanghai and Hong Kong. "YTO" is the abbreviation it uses internationally; "Yuantong" is the English spelling of its Chinese name. Both are correct. In Australia the same operation also appears written as &lt;code&gt;AU-YTO&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;carrier === "YTO Express"&lt;/code&gt; and &lt;code&gt;carrier === "Yuantong"&lt;/code&gt; describe the same company, and a user searching for one will never match a record stored under the other.&lt;/p&gt;

&lt;p&gt;This is not a Chinese-carrier quirk. It is what happens any time a company has a legal name in one language and a trading name in another, and it is extremely common in cross-border logistics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule: carrier names need an alias table, and the alias table needs to be the thing you query.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Minimal shape. The point is the normalisation, not the list.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALIASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ontrac&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ontrac&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on trac&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on track&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;cainiao&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cainiao&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cai niao&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;caniao&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;straightship&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;straightship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;straight ship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yto-express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yto express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yuantong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yuan tong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;圆通速递&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;au-yto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFKC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\p&lt;/span&gt;&lt;span class="sr"&gt;{L}&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{N}&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/gu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// keep CJK, drop punctuation&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALIASES&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolveCarrier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;INDEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that matter more than the list itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;\p{L}&lt;/code&gt; with the &lt;code&gt;u&lt;/code&gt; flag, not &lt;code&gt;\w&lt;/code&gt;. &lt;code&gt;\w&lt;/code&gt; is ASCII-only, so &lt;code&gt;圆通速递&lt;/code&gt; normalises to an empty string and every CJK name collapses into the same key. (We have shipped that bug. Once.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NFKC&lt;/code&gt; before stripping, because full-width Latin characters turn up in labels printed by Asian systems and &lt;code&gt;ＹＴＯ&lt;/code&gt; is not &lt;code&gt;YTO&lt;/code&gt; under a naive comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The carrier on the label is not always the carrier that delivers
&lt;/h2&gt;

&lt;p&gt;On a cross-border route, the company the seller handed the parcel to usually hands it on again. Cainiao — Alibaba's logistics arm, the network behind most AliExpress, Taobao and Tmall orders — is explicitly a coordinating network rather than a fleet: it contracts warehouses, consolidators, airlines and local delivery companies. The parcel's last leg is done by a local carrier whose name never appeared on the original label.&lt;/p&gt;

&lt;p&gt;If your data model has &lt;code&gt;shipment.carrier&lt;/code&gt; as a write-once field set at ingest, you will be wrong about a large fraction of international shipments by the time they arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule: &lt;code&gt;carrier&lt;/code&gt; is mutable state, not an identity. Model it as "best current attribution", with a timestamp, and let it be revised.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Practically that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- not this&lt;/span&gt;
&lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;  &lt;span class="c1"&gt;-- set once at insert&lt;/span&gt;

&lt;span class="c1"&gt;-- closer to reality&lt;/span&gt;
&lt;span class="n"&gt;carrier&lt;/span&gt;          &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;-- current best attribution&lt;/span&gt;
&lt;span class="n"&gt;carrier_source&lt;/span&gt;   &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'declared'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'detected'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'reported'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;carrier_updated&lt;/span&gt;  &lt;span class="nb"&gt;DATETIME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;carrier_source&lt;/code&gt; column earns its keep the first time a user-declared value disagrees with what the shipment itself reports. A value the user typed into a dropdown should never outrank a value that came back with the shipment's own events.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Number shape is a hint for some carriers and noise for others
&lt;/h2&gt;

&lt;p&gt;It is tempting to build a single regex table and call carrier detection solved. The coverage is genuinely uneven, and pretending otherwise produces confident wrong answers.&lt;/p&gt;

&lt;p&gt;Of the four carriers above, exactly one has a shape you can lean on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Carrier&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Usable as a signal?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Straightship&lt;/td&gt;
&lt;td&gt;five letters + 13 digits, 18 chars, overwhelmingly &lt;code&gt;STRCA…&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes — one shape dominates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnTrac&lt;/td&gt;
&lt;td&gt;mixed; some &lt;code&gt;D&lt;/code&gt;-prefixed, many all-digit&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YTO Express&lt;/td&gt;
&lt;td&gt;mixed; several prefixes in regular use&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cainiao&lt;/td&gt;
&lt;td&gt;mixed; may carry a partner's number entirely&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mechanism behind the mixed ones is always the same: large carriers absorb other networks and inherit their numbering, and cross-border legs carry partner numbers. A clean format only exists where a carrier issues every number itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule: detection returns three outcomes, not two.&lt;/strong&gt; &lt;code&gt;recognised&lt;/code&gt; / &lt;code&gt;ambiguous&lt;/code&gt; / &lt;code&gt;unrecognised&lt;/code&gt;. Collapsing &lt;code&gt;ambiguous&lt;/code&gt; into &lt;code&gt;unrecognised&lt;/code&gt; throws away the most useful state you have — "this looks like a real number, we just cannot attribute it" is a completely different support conversation from "this is not a tracking number".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Three outcomes, not a boolean&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recognised&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="nx"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;straightship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shape&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ambiguous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ontrac&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;usps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unrecognised&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no known carrier issues this shape&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Never block a lookup because the name did not match
&lt;/h2&gt;

&lt;p&gt;The failure mode I see most often in support queues: a user pastes a perfectly good number, the UI asks them to pick a carrier from a dropdown, they pick the wrong one or a close-sounding one, and the lookup fails. The number was fine the whole time.&lt;/p&gt;

&lt;p&gt;If your form has a carrier selector, make it optional and make it a &lt;em&gt;filter&lt;/em&gt;, not a &lt;em&gt;precondition&lt;/em&gt;. The number is the identifier. The name is a label somebody typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this adds up
&lt;/h2&gt;

&lt;p&gt;The through-line is that carrier identity lives in the number, and every other representation of it — the dropdown value, the label text, the name in the order confirmation email — is a lossy copy that can go stale between ingest and delivery.&lt;/p&gt;

&lt;p&gt;Build for that and a surprising amount of support load disappears, because the two commonest tickets ("I can't find my carrier in your list" and "your site says this number is invalid") both stop being possible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt;, a free multi-carrier package tracker — paste any tracking number, the carrier is detected automatically, no sign-up. There is also a &lt;a href="https://www.24htrack.com/api" rel="noopener noreferrer"&gt;REST API and an MCP server&lt;/a&gt; if you want to do this from code or from an AI assistant. The carrier facts above come from each company's own site; the shape observations come from numbers that have passed through our platform.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Validate a UPS 1Z number before you call any tracking API - the check digit is free</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Tue, 06 Oct 2026 05:00:54 +0000</pubDate>
      <link>https://dev.to/support24htrack/validate-a-ups-1z-number-before-you-call-any-tracking-api-the-check-digit-is-free-2fh9</link>
      <guid>https://dev.to/support24htrack/validate-a-ups-1z-number-before-you-call-any-tracking-api-the-check-digit-is-free-2fh9</guid>
      <description>&lt;p&gt;If your app accepts tracking numbers from users, you are almost certainly treating two very different failures as one. "This number does not exist" and "this number cannot exist" look identical at the API layer  -  both come back as not found  -  but only one of them is worth retrying.&lt;/p&gt;

&lt;p&gt;UPS parcel numbers give you a way to tell them apart for free, before any network call. The last character of a 1Z number is a check digit computed from the fifteen before it.&lt;/p&gt;

&lt;p&gt;Disclosure up front: I work on 24hTrack, a free package tracker. The numbers below come from our own data and you can reproduce the algorithm yourself in about ten lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;A UPS parcel number is &lt;code&gt;1Z&lt;/code&gt; plus 16 alphanumeric characters  -  18 in total, no spaces, no dashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1Z  999AA1  01  2345678  4
|   |       |   |        |
|   |       |   |        +-- check digit
|   |       |   +----------- package identifier
|   |       +--------------- service level
|   +-------------------------- shipper account (6 chars)
+------------------------------ fixed prefix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shipper account being inside the number is the reason every label from a given seller opens with the same eight characters. Useful if you are grouping shipments by merchant and do not have the data to do it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check digit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidUps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^1Z&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9A-Z&lt;/span&gt;&lt;span class="se"&gt;]{16}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// 16 chars: 15 + check digit&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="c1"&gt;// letters map to digits: A-&amp;gt;2, B-&amp;gt;3 ... wrapping at 10&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// alternate weighting&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sanity check against the number UPS uses in its own documentation examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;isValidUps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1Z999AA10123456784&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;isValidUps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1Z999AA10123456785&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// false - one digit off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Does it actually hold on real traffic?
&lt;/h2&gt;

&lt;p&gt;That is the part worth measuring rather than trusting. I ran this over every UPS number added to our platform in the last 60 days: &lt;strong&gt;1,539 numbers, 1,539 passes, zero failures.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a strong enough signal to act on. If a 1Z-shaped string fails this check, it is not a parcel you have not heard about yet  -  it is a corrupted string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it sounds
&lt;/h2&gt;

&lt;p&gt;Three things change once you can separate the two failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. You stop retrying garbage.&lt;/strong&gt; A tracking integration typically re-polls unknown numbers on a schedule for days. A number that fails the check digit will never resolve, so every one of those polls is wasted  -  against a rate limit you are probably sharing across your whole catalogue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You can give the user a real error message.&lt;/strong&gt; "We could not find that number yet, we will keep checking" is correct for a valid number with no scans. For an invalid one it is actively misleading: the customer waits instead of going back and re-copying the number. Compare:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We cannot read that number. UPS numbers are 1Z plus 16 characters, and this one does not match. Check for &lt;code&gt;O&lt;/code&gt; typed as &lt;code&gt;0&lt;/code&gt;, or &lt;code&gt;I&lt;/code&gt; typed as &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The four confusable pairs  -  &lt;code&gt;0&lt;/code&gt;/&lt;code&gt;O&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;/&lt;code&gt;I&lt;/code&gt;, &lt;code&gt;5&lt;/code&gt;/&lt;code&gt;S&lt;/code&gt;, &lt;code&gt;8&lt;/code&gt;/&lt;code&gt;B&lt;/code&gt;  -  are where the damage almost always is, because these numbers get read off printed labels and screenshots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You validate at the edge of your system, not in the middle.&lt;/strong&gt; This is a pure function with no I/O. It belongs in the form, not behind an HTTP call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The important caveat
&lt;/h2&gt;

&lt;p&gt;Do not use this as a carrier detector. &lt;strong&gt;Not every UPS parcel has a 1Z number.&lt;/strong&gt; Economy services hand the final mile to the national postal operator and the customer is often given the postal number instead; freight shipments use a shorter, all-numeric reference; and plenty of sellers hand over an internal order code and call it a tracking number.&lt;/p&gt;

&lt;p&gt;So the logic is one-way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shape matches &lt;code&gt;1Z...&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; check digit passes -&amp;gt; safe to treat as a UPS parcel number&lt;/li&gt;
&lt;li&gt;shape matches &lt;code&gt;1Z...&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; check digit fails -&amp;gt; reject at the form, do not queue it&lt;/li&gt;
&lt;li&gt;shape does not match -&amp;gt; say nothing about UPS; it may still be a perfectly good number belonging to someone else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last branch is the one most implementations get wrong. "Not a 1Z number" means "I have no opinion", not "invalid".&lt;/p&gt;

&lt;h2&gt;
  
  
  If you would rather not maintain shape rules at all
&lt;/h2&gt;

&lt;p&gt;Carrier-shape detection is a surprisingly long tail  -  a few hundred patterns, several of them shared between companies, plus the re-labelling that happens when a cross-border parcel is handed to a local carrier. We ended up building that detection anyway, so it is exposed: paste any number into &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; and the carrier is identified for you, free and without an account, or call it from code through the &lt;a href="https://www.24htrack.com/api" rel="noopener noreferrer"&gt;REST API&lt;/a&gt;. There is also an MCP server if you want an AI agent to do it.&lt;/p&gt;

&lt;p&gt;But if all you need is to stop queueing broken UPS numbers, the ten lines above will do it today.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Validate tracking numbers at ingest, not at the support desk</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:28:58 +0000</pubDate>
      <link>https://dev.to/support24htrack/validate-tracking-numbers-at-ingest-not-at-the-support-desk-2fl4</link>
      <guid>https://dev.to/support24htrack/validate-tracking-numbers-at-ingest-not-at-the-support-desk-2fl4</guid>
      <description>&lt;p&gt;&lt;em&gt;Full disclosure: I work on 24hTrack, a free multi-carrier package tracker. The patterns below come from running that ingest path; discount the last section accordingly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most shipment pipelines validate a tracking number exactly once: when a customer complains that it does not work. By then the number has already been written to the order, emailed to the buyer, and copied into three systems.&lt;/p&gt;

&lt;p&gt;Treat it as untrusted input at ingest, the way you already treat an email address.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tracking number has three validation outcomes, not two
&lt;/h2&gt;

&lt;p&gt;The usual model is &lt;code&gt;valid | invalid&lt;/code&gt;. Real carrier identification is a classifier with three outcomes, and conflating two of them is where the bugs live:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recognised&lt;/strong&gt; - the shape matches exactly one carrier's published format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous&lt;/strong&gt; - the shape matches more than one. Bare numeric formats do this constantly: a 12-digit string is FedEx Express &lt;em&gt;or&lt;/em&gt; a dozen regional couriers. "2 letters + 9 digits + 2 letters" is any UPU member postal operator; only the trailing country code narrows it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unrecognised&lt;/strong&gt; - matches nothing published anywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Outcome 3 is the one worth acting on at ingest, because it is almost never a carrier problem. It is a warehouse job code, a consolidator reference, or an order ID that someone pasted into the wrong column. Catching it on day zero costs one validation call. Catching it on day twelve costs a support ticket and a refund window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check digits are free validation and almost nobody uses them
&lt;/h2&gt;

&lt;p&gt;Several formats carry a check digit you can verify offline, with no API call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;USPS IMpb (20-22 digits): mod-10 with alternating 3/1 weights.&lt;/li&gt;
&lt;li&gt;FedEx Express (12 digits): mod-11 with weights 1,3,7 repeating.&lt;/li&gt;
&lt;li&gt;UPU S10 (&lt;code&gt;XX123456789YY&lt;/code&gt;): mod-11 on the 8 serial digits, with 5 and 0 as special cases.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// USPS IMpb mod-10, weights alternating 3 and 1 from the right&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uspsCheckDigitOk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed check digit on a well-formed-looking number is nearly always a transcription error - a digit dropped, two swapped. That is a &lt;em&gt;different&lt;/em&gt; support reply from "the carrier has not scanned it yet", and your system is the only thing that can tell them apart.&lt;/p&gt;

&lt;p&gt;One warning from experience: calibrate a check-digit rule against your own historical data before you ship it as a hard reject. We tightened a USPS rule once that looked obviously correct and would have quietly orphaned a slice of real numbers that a partner was formatting slightly differently. Run it in log-only mode first and count what it would have rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Carrier is a mutable field. Model it that way
&lt;/h2&gt;

&lt;p&gt;A surprising number of schemas store &lt;code&gt;carrier&lt;/code&gt; as if it were decided once, at creation. It is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parcels get re-labelled mid-journey. Purolator even has a public status for it - &lt;em&gt;New Tracking Number Assigned&lt;/em&gt; - at which point the original number stops moving and a different one carries the rest of the trip.&lt;/li&gt;
&lt;li&gt;Consolidators issue their own job number first and a real carrier number later.&lt;/li&gt;
&lt;li&gt;Rebrands happen. Toll Global Express became Team Global Express in 2022 and the old portal is gone, but the numbers on the labels did not change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store &lt;code&gt;(carrier_code, number)&lt;/code&gt; and resolve the tracking URL at render time. Never persist a tracking URL - it is derived data with a shorter lifetime than the row it lives on.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Reference" is not a key, even when the carrier accepts it
&lt;/h2&gt;

&lt;p&gt;Some carriers deliberately accept the shipper's own code. Purolator's tracking box asks for "a PIN or Reference", where a Reference is up to 15 characters chosen by whoever shipped it.&lt;/p&gt;

&lt;p&gt;That is useful for humans and dangerous for code. A reference is not unique across shippers: two merchants can both use &lt;code&gt;INV-1042&lt;/code&gt;. If you let a reference into the same column as a carrier number, you have introduced a lookup that can return somebody else's shipment. Keep a separate &lt;code&gt;shipper_reference&lt;/code&gt; column and never make it the join key.&lt;/p&gt;

&lt;p&gt;The same applies to naming. Several carriers do not use the phrase "tracking number" at all: Purolator says PIN, US LTL freight says PRO, air cargo says AWB (a 3-digit airline prefix plus 8 digits), Australian freight says consignment note. If you render a generic "Tracking number" label, a chunk of your users will be looking for something they cannot find on the carrier's site either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different timeouts, two different alerts
&lt;/h2&gt;

&lt;p&gt;Teams usually have one staleness alert: &lt;em&gt;no movement for N days&lt;/em&gt;. You want two, because the remedies are unrelated.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Who fixes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Zero scans ever&lt;/strong&gt;, N days after ingest&lt;/td&gt;
&lt;td&gt;the number may not exist in any carrier system&lt;/td&gt;
&lt;td&gt;you and the merchant - data problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Had scans, none for N days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the parcel is between scan points&lt;/td&gt;
&lt;td&gt;nobody, usually - it is in the air, in customs, or mid-handover&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second is normal and should be scored against that lane's own distribution, not a global constant. Cross-border parcels average roughly 18-22 scans end to end against 4-5 for a domestic one, and a lane with a median of 13 days can have an 80th percentile near 29. A flat "two weeks means lost" rule will fire on more than one parcel in five that is behaving perfectly.&lt;/p&gt;

&lt;p&gt;The first is the one that actually needs a human, and it is the one almost nobody alerts on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Validate shape at ingest, verify check digits where the carrier publishes one, keep carrier mutable, keep shipper references out of your key, and alert separately on "never scanned" versus "not scanned lately".&lt;/p&gt;

&lt;p&gt;If you want a reference implementation to compare against, &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; does the detection step for 3,200+ carriers from the number alone, with a REST API and an MCP server if you want an AI assistant to do the lookup. Free to try without an account.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your tracking integration probably can't tell a refusal from a failed delivery - and on COD orders that matters</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Sun, 04 Oct 2026 05:55:43 +0000</pubDate>
      <link>https://dev.to/support24htrack/your-tracking-integration-probably-cant-tell-a-refusal-from-a-failed-delivery-and-on-cod-orders-51p</link>
      <guid>https://dev.to/support24htrack/your-tracking-integration-probably-cant-tell-a-refusal-from-a-failed-delivery-and-on-cod-orders-51p</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I work on 24hTrack, a multi-carrier package tracker. The patterns below apply whatever you build on.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most post-purchase code treats a shipment as a line that moves one way: label created, in transit, out for delivery, delivered. That model is fine until the order is &lt;strong&gt;cash on delivery&lt;/strong&gt;, and then it quietly produces wrong answers for a large part of the world.&lt;/p&gt;

&lt;p&gt;Cash on delivery is ordinary across the Gulf, much of Africa and a lot of South-East Asia. GFS Express, one of the networks carrying Chinese e-commerce into that region, lists local operations in Saudi Arabia, Bahrain, Jordan, Kuwait, Oman, Qatar, Azerbaijan, Georgia and South Africa, and sells cash collection to merchants as a product with weekly remittance. If your buyers are there, a meaningful share of your orders are only paid for at the doorstep.&lt;/p&gt;

&lt;p&gt;Four things that break.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Delivered" stops meaning "paid"
&lt;/h2&gt;

&lt;p&gt;On a prepaid order those two collapse into one event. On COD they are different facts arriving from different systems: the scan from the carrier, the money from a remittance file days later. If your fulfilment logic recognises revenue on a delivery scan, COD will overstate it, and every refusal becomes a correction you have to find by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The reverse path is a normal path, not an error
&lt;/h2&gt;

&lt;p&gt;A COD buyer can simply decline at the door. Tracking records that as a failed attempt and then a return - the same two statuses you get when nobody was home. Carriers rarely tell you which it was. If your code treats &lt;code&gt;returned&lt;/code&gt; as an exception to alert on, COD markets will drown you in alerts for transactions that worked exactly as designed. Model it as an outcome with a reason you may never learn, not as a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Amounts are not in tracking data, and you should not synthesise them
&lt;/h2&gt;

&lt;p&gt;Carriers publish scans: a status, a time, often a place. They do not publish what the recipient owes. Every so often someone asks us to show the COD amount on a tracking page. We can't, and neither can anyone else reading the same feeds - the number lives in the order, not in the shipment. If your UI implies otherwise, you have invented a figure a customer will act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Duty is a second, unrelated money event
&lt;/h2&gt;

&lt;p&gt;Import duty and VAT are billed by the carrier or the post, usually ahead of delivery, usually by SMS or email with a link. It can land on a fully prepaid order. It is not COD and it is not collected the same way. Conflating them produces support answers that are confidently wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the wrong answers actually surface
&lt;/h2&gt;

&lt;p&gt;Not in your dashboard - in the "is this a scam?" ticket. Buyers in COD markets are also the buyers most targeted by doorstep payment fraud, and they cannot tell your legitimate collection apart from it. The cheap mitigation is boring and entirely on your side: state the COD amount in the order confirmation, in the buyer's currency, in a line they can find on a phone at the door. Everything a scammer cannot replicate - a matching order, a receipt, payment to the courier rather than a link - then becomes checkable in ten seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small data detail while you are in there
&lt;/h2&gt;

&lt;p&gt;When you normalise scans, do not assume the location is a field. Several carriers write it into the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Riyadh, Parcel picked up from customs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you render a location column from &lt;code&gt;event.location&lt;/code&gt; alone, those rows look placeless, and your "we have no location for this parcel" branch fires on a parcel whose city is sitting in the first eight characters of the description. Read the words before you declare the field empty.&lt;/p&gt;

&lt;p&gt;None of this needs a new vendor. It needs the shipment model to have two axes - where the goods are, and whether the money moved - instead of one.&lt;/p&gt;

&lt;p&gt;If you would rather not build the tracking half: &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; detects the carrier from the number across 3,200+ carriers, is free on the web with no sign-up, syncs to Google Sheets at no cost, and has a REST API plus an MCP server (&lt;code&gt;npm i 24htrack-mcp&lt;/code&gt;) for AI agents. The open guide and transit-time data are on &lt;a href="https://github.com/doanhnd9989/package-tracking-guide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can a tracking API tell me the cash-on-delivery amount?&lt;/strong&gt;&lt;br&gt;
No. Tracking feeds carry statuses, timestamps and sometimes locations. The amount lives in the order record, not in the shipment, so any tracking tool showing you a figure is reading it from somewhere other than the carrier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I distinguish a refused COD parcel from a missed delivery?&lt;/strong&gt;&lt;br&gt;
Usually you cannot from the scans alone - both appear as a failed attempt followed by a return. Carry your own signal instead: a COD flag on the order plus the remittance file, and treat an unpaid return on a COD order as a refusal until proven otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is cash on delivery the same as paying customs duty?&lt;/strong&gt;&lt;br&gt;
No. COD is the price of the goods, collected for the seller. Duty is tax, billed by the carrier or post, and it can appear on an order that was already paid in full.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which carriers use cash on delivery?&lt;/strong&gt;&lt;br&gt;
It is a service sold to merchants rather than a property of a carrier, and it is common wherever the market expects it. GFS Express is one example that advertises collection with weekly payout to the seller.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Don't store a tracking URL. Store the number and resolve the link at render time.</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Sat, 03 Oct 2026 05:18:58 +0000</pubDate>
      <link>https://dev.to/support24htrack/dont-store-a-tracking-url-store-the-number-and-resolve-the-link-at-render-time-1f3p</link>
      <guid>https://dev.to/support24htrack/dont-store-a-tracking-url-store-the-number-and-resolve-the-link-at-render-time-1f3p</guid>
      <description>&lt;p&gt;Most order systems I have looked at store a tracking URL on the shipment: a full link, built at order creation, pasted into the confirmation email and the order page.&lt;/p&gt;

&lt;p&gt;That link is a liability, and the failure mode is quiet. Here is the one that bit us this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure
&lt;/h2&gt;

&lt;p&gt;Toll Group sold its Global Express division to Allegro Funds, and the business has traded as &lt;strong&gt;Team Global Express&lt;/strong&gt; since 2022. The domestic parcel arm is still IPEC. The customer portal moved from MyToll to &lt;strong&gt;myteamge.com&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Checked on 3 October 2026: &lt;code&gt;mytoll.com&lt;/code&gt; and &lt;code&gt;www.mytoll.com&lt;/code&gt; return no page at all. Not a 404 page, not a redirect - the connection does not complete.&lt;/p&gt;

&lt;p&gt;So every order record that baked in a MyToll URL now ships a dead link to the customer. The parcel is fine. The number was never reissued and still resolves on the new portal. The only thing that broke is a string written down two years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why storing the URL is the bug
&lt;/h2&gt;

&lt;p&gt;A tracking URL is derived data. It is a function of (carrier, number) evaluated at one point in time. Storing it freezes that evaluation forever, and the inputs keep moving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rebrands and acquisitions.&lt;/strong&gt; Toll Global Express to Team Global Express (2022). CIRRO E-Commerce was founded as PARCLL, renamed April 2023, acquired by GOFO February 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last-mile handover.&lt;/strong&gt; A cross-border parcel is handed to a domestic carrier near the end, so the right link to show changes mid-journey.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portal path changes.&lt;/strong&gt; Carriers restructure query params without warning, and a stored URL with the old param silently renders an empty result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the facts, derive the view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// not this&lt;/span&gt;
&lt;span class="nx"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tracking_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.mytoll.com/?searchQuery=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;

&lt;span class="c1"&gt;// this&lt;/span&gt;
&lt;span class="nx"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;carrier_code&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TEAM_GLOBAL_EXPRESS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;// mutable&lt;/span&gt;
&lt;span class="nx"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tracking_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;64................001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;// immutable&lt;/span&gt;
&lt;span class="c1"&gt;// at render time:&lt;/span&gt;
&lt;span class="nf"&gt;trackingUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;carrier_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tracking_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a rebrand is a one-line change in a lookup table, not a backfill across every historical order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Carrier is a mutable field, not a constant
&lt;/h2&gt;

&lt;p&gt;The second half of the same bug: most schemas write &lt;code&gt;carrier&lt;/code&gt; once, at order creation, from the shipping method the customer picked. It then never changes, even when the parcel changes hands.&lt;/p&gt;

&lt;p&gt;Make it updatable, and normalise it. One Australian shipment can reach you labelled Toll, Toll IPEC, Toll Road Express or Team Global Express, depending on which system passed the data along. If you report on carrier performance without collapsing those into one key, you are reporting on four carriers that do not exist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TOLL | TOLL IPEC | TOLL ROAD EXPRESS | TEAM GLOBAL EXPRESS  -&amp;gt;  TEAM_GLOBAL_EXPRESS
PARCLL | CIRRO E-COMMERCE                                   -&amp;gt;  CIRRO_ECOMMERCE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the raw string the partner sent you in a separate column. You will want it the first time a new spelling appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things worth doing today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep your templates, help articles and support macros for carrier names, not just URLs.&lt;/strong&gt; The name is what a customer types into a search engine when your link fails, and a dead brand name sends them nowhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a cheap liveness check on the carrier URLs you emit.&lt;/strong&gt; A weekly job that requests each distinct base URL and alerts on connection failure would have caught MyToll the week it went dark. Do not check for HTTP 200 only - a carrier that serves a styled "no results" page at 200 is also broken for your customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not derive a carrier from number length alone.&lt;/strong&gt; Team Global Express numbers are 22 digits beginning 64: a four-digit prefix, a thirteen-digit consignment number, then a three-digit item sequence. Several US numeric labels are also 20-22 digits. Anchor on prefix and length together, and measure a rule against real data before you ship it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The customer-facing half
&lt;/h2&gt;

&lt;p&gt;If the link does fail, the person holding the parcel has one workable move: search the brand together with "renamed" or "acquired", or paste the number into a universal tracker that works the carrier out from the number itself rather than from a stored name. We build one - &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; is a free package tracker for 3,200+ carriers; paste any tracking number, the carrier is detected automatically, no sign-up. A rename never invalidates the number, so it still resolves.&lt;/p&gt;

&lt;p&gt;Written by the team at 24hTrack. Longer walkthrough of the Toll case, with the dates and the number format: &lt;a href="https://youtu.be/kbCsSWW4d6k" rel="noopener noreferrer"&gt;https://youtu.be/kbCsSWW4d6k&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Two couriers, one name: why you should key a tracking integration on number shape, not carrier name</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Fri, 02 Oct 2026 04:41:42 +0000</pubDate>
      <link>https://dev.to/support24htrack/two-couriers-one-name-why-you-should-key-a-tracking-integration-on-number-shape-not-carrier-name-b68</link>
      <guid>https://dev.to/support24htrack/two-couriers-one-name-why-you-should-key-a-tracking-integration-on-number-shape-not-carrier-name-b68</guid>
      <description>&lt;p&gt;If you are building anything that touches shipment tracking  -  an order page, a WISMO deflection flow, a 3PL dashboard  -  there is a failure mode that does not show up in your tests and shows up constantly in production: &lt;strong&gt;two different courier companies with the same name.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not similar. The same.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CTT&lt;/strong&gt; is the Portuguese post (ctt.pt).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTT Express&lt;/strong&gt; is a Spanish courier (cttexpress.com).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different companies, different networks, different tracking systems. A consignment number issued by one is simply not recognised by the other. If your integration stores &lt;code&gt;carrier: "CTT"&lt;/code&gt; as a string and later resolves that string to a tracking URL, you have a coin flip baked into your data model, and the half that loses sends a customer to a page that says their parcel does not exist.&lt;/p&gt;

&lt;p&gt;It is not a rare shape either. Shippers reuse initialisms heavily: three-letter brands, "Express" suffixes, and local operators that took a name already in use somewhere else. On top of that, a &lt;em&gt;generic word&lt;/em&gt; collision is just as damaging for discovery: "247Express" is a real Vietnamese courier operating since 2005, but the string &lt;code&gt;247 tracking&lt;/code&gt; reads to a search engine as &lt;em&gt;round-the-clock tracking&lt;/em&gt;, so users looking for the company and users looking for the concept land in the same bucket.&lt;/p&gt;

&lt;p&gt;Here is what I would do differently, having watched this break.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The number is evidence. The name is a label somebody typed.
&lt;/h2&gt;

&lt;p&gt;The carrier name on an order usually arrives from one of three places: a dropdown the merchant picked, a shipping method chosen at checkout, or free text in a CSV. None of those is authoritative about who is physically carrying the box. The &lt;strong&gt;tracking number&lt;/strong&gt;, by contrast, is issued by the carrier's own system and carries its format.&lt;/p&gt;

&lt;p&gt;So resolve carrier from the number pattern first, and treat any stored name as a hint to be overridden  -  not the other way around.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resolveCarrier(order):
  byShape = matchPattern(order.trackingNumber)   # authoritative
  if byShape: return byShape
  return normalise(order.carrierName)            # fallback, not truth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical payoff: when a merchant types "CTT" and the number is clearly a Spanish CTT Express pattern, you route correctly anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Anchor patterns on prefix &lt;strong&gt;and&lt;/strong&gt; length, or you will cannibalise
&lt;/h2&gt;

&lt;p&gt;The tempting rule is a loose one. It is also the one that quietly steals other carriers' parcels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Don't
/^\d{12}$/            -&amp;gt; "FedEx"
/^[A-Z]{3}\d+[A-Z]{2}$/ -&amp;gt; "SomeCarrier"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare twelve digits is not a carrier. It is a &lt;em&gt;length&lt;/em&gt;. Several operators issue twelve-digit numbers, and the moment your rule claims them, their parcels get routed to the wrong tracking system and fail  -  silently, because a failed lookup looks identical to "no data yet".&lt;/p&gt;

&lt;p&gt;Anchor both ends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Do&lt;/span&gt;
&lt;span class="sr"&gt;/^JCZ\d{10}[A-Z]{2}$/&lt;/span&gt;   &lt;span class="c1"&gt;# prefix + exact digit count + suffix class&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And before you add a rule, run it against your whole existing corpus and count how many numbers it takes &lt;strong&gt;away&lt;/strong&gt; from a rule that already works. If a new rule rescues 30 unidentified numbers but overrides 120 already-correct ones, it is a regression wearing a feature's clothes. Measure rescue vs clobber, not just precision on the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Do not publish a format you have not measured
&lt;/h2&gt;

&lt;p&gt;If you show users "your number looks like XXX", that string had better be true, because people use it to decide whether a number is even the right one.&lt;/p&gt;

&lt;p&gt;The rule I settled on: &lt;strong&gt;only state a format when a single shape accounts for 60% or more of that carrier's numbers in your sample, and say so in the shape, not with a real number.&lt;/strong&gt; Below that, say nothing. One carrier I looked at recently had a dominant shape at 91%  -  safe to print. Another had its most common shape at 57%, with the rest of the basket carrying other carriers' prefixes entirely, because merchants had been filing anything into that carrier field. "Eleven digits" would have been a guess dressed as a fact.&lt;/p&gt;

&lt;p&gt;The honest answer, when the data is mixed, is to say nothing about the format and let the number resolve itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Never use a real tracking number as the example
&lt;/h2&gt;

&lt;p&gt;Obvious when written down, routinely violated in docs and UI copy. A real number identifies a real shipment to a real address, and once it is in your public docs it is in search results.&lt;/p&gt;

&lt;p&gt;Publish the &lt;strong&gt;shape&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Three letters, ten digits, two letters (e.g. ABC0000000000XX)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;not someone's parcel.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Carrier must be a correctable field, not a derived constant
&lt;/h2&gt;

&lt;p&gt;The last one, and the one that bites cross-border integrations hardest: the company that finally delivers is frequently &lt;strong&gt;not&lt;/strong&gt; the company named at checkout. Cross-border shippers typically carry the parcel out of the origin country and hand the final leg to a local courier or post. That means the correct carrier value for an order can legitimately change after the order record was written, and one shipment can legitimately hold more than one number.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;carrier&lt;/code&gt; is computed once at insert time and never revisited, every one of those orders ends its life displaying a timeline that stopped a week ago, and your support queue fills with people who think the parcel is lost. It is not. The page ended; the parcel moved on under a different name.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The summary, if you only take one thing:&lt;/strong&gt; names are ambiguous, user-supplied and mutable; number patterns are issued by the system that actually has the parcel. Key on the pattern, keep the name as a correctable hint, and only publish a format you have counted.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I work at &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt;, a free package tracker that identifies the carrier from the number across 3,200+ carriers  -  which is exactly why these collisions are my problem rather than a trivia question. Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Don't localize carrier scan timestamps. You are not storing an instant.</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Thu, 01 Oct 2026 06:42:11 +0000</pubDate>
      <link>https://dev.to/support24htrack/dont-localize-carrier-scan-timestamps-you-are-not-storing-an-instant-1a0c</link>
      <guid>https://dev.to/support24htrack/dont-localize-carrier-scan-timestamps-you-are-not-storing-an-instant-1a0c</guid>
      <description>&lt;p&gt;If you build order-tracking UI, you have almost certainly done this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks correct. It is the single most common way tracking pages end up lying to customers, and the bug report you get back will not look like a timezone bug. It will look like this, which is a real support message we got this week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This morning my package said delivered at 21:49. This was at 6:04 am. It was not delivered to my house."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two different complaints are tangled together there, and only one of them is the carrier's fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scan is a local wall-clock reading, not an instant
&lt;/h2&gt;

&lt;p&gt;A tracking event is produced by a device in a facility, or in a driver's hand, writing down the time &lt;strong&gt;where the scan happened&lt;/strong&gt;. Carriers publish that wall-clock reading. Most of them do not publish an offset with it.&lt;/p&gt;

&lt;p&gt;So the string &lt;code&gt;2026-09-30 21:49:00&lt;/code&gt; arriving in your pipeline is not an instant in time. It is "the clock on the wall at whichever building scanned this said 21:49". Without the offset you cannot convert it, and if you treat it as UTC and then render it in the viewer's zone, you have invented a number nobody published.&lt;/p&gt;

&lt;p&gt;The failure mode is nasty because it is plausible. Attach &lt;code&gt;Z&lt;/code&gt; to a naive string, render in the browser's zone, and a 09:57 delivery in Germany becomes 16:57 for a viewer in Vietnam. Nothing throws. The page just shows a time that never existed, and the customer - who was standing in their hallway at 09:57 - concludes your tracking is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we do instead
&lt;/h2&gt;

&lt;p&gt;We print the digits the carrier published, unconverted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The stored value is the carrier's wall clock with no offset.&lt;/span&gt;
&lt;span class="c1"&gt;// Reading it back with UTC getters returns the original digits unchanged.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCFullYear&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;pad2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;pad2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCDate&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s2"&gt; `&lt;/span&gt;
     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;pad2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCHours&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;pad2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCMinutes&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using UTC getters on a value you only ever &lt;em&gt;tagged&lt;/em&gt; as UTC is not a hack, it is the inverse of the tag. You get back exactly what the carrier said. The rule that follows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never convert a timestamp whose offset you were never given. Render it as text and label whose clock it is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you do have a genuine offset - some carriers send one, and your own events like "we last checked this" are real instants - convert those freely. Just keep the two classes apart in your schema. One is an instant. The other is a string that looks like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then handle the scans that have no clock at all
&lt;/h2&gt;

&lt;p&gt;Once you stop converting, you notice the second problem: not every scan has a time.&lt;/p&gt;

&lt;p&gt;Measured on delivery scans flowing through our platform (sample of ~27,000 recent delivery events):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;carries a time of day&lt;/td&gt;
&lt;td&gt;~96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date only, no clock&lt;/td&gt;
&lt;td&gt;~4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four percent sounds small until it is the one parcel a customer is angry about. Those arrive as &lt;code&gt;09/18/2026&lt;/code&gt; with nothing after it. &lt;code&gt;new Date()&lt;/code&gt; will happily give you midnight, your formatter will print &lt;strong&gt;00:00&lt;/strong&gt;, and you have just told someone their parcel was delivered at midnight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Treat "no clock" as a distinct state. Do not let a parser invent 00:00.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HAS_CLOCK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\d{1,2}&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;HAS_CLOCK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseDateOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render that as the date alone. A missing time is information: it tells the customer the carrier never published a minute, so there is no minute to argue about.&lt;/p&gt;

&lt;p&gt;Watch the format spread while you are in there. In the same sample the clock-bearing values came in at least two shapes - &lt;code&gt;YYYY-MM-DD HH:MM:SS&lt;/code&gt; and &lt;code&gt;July 20, 2026 9:00 PM&lt;/code&gt;. If you regex for one, you will silently classify the other as "no time". I did exactly that in the first pass of this measurement and got 72% instead of 96%; the 12-hour strings were hiding in the remainder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not build "is this late?" on the hour either
&lt;/h2&gt;

&lt;p&gt;The last trap: once you have hours, it is tempting to flag odd ones. Resist.&lt;/p&gt;

&lt;p&gt;Delivery scans in that same sample, by hour of day (carrier local):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Busiest window is late morning to mid-afternoon, roughly 8-9% of deliveries per hour&lt;/li&gt;
&lt;li&gt;Between 20:00 and 05:59: &lt;strong&gt;about 9%&lt;/strong&gt;, or roughly 1 in 11&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 21:49 delivery scan is not an anomaly worth surfacing as one. Contract and gig drivers finish the route they were assigned, and in peak season that runs later. If your UI puts a warning badge on evening deliveries, you will generate support tickets for one parcel in eleven, every day, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually helps the customer
&lt;/h2&gt;

&lt;p&gt;Having removed three ways to be wrong, here is the one field worth promoting in the UI: &lt;strong&gt;the location on the delivery scan&lt;/strong&gt;, not the minute.&lt;/p&gt;

&lt;p&gt;"Delivered" with the customer's town on it is a different claim from "Delivered" at a depot, a locker, or an access point. That distinction tells the customer whether to look on their porch or to go ask the seller to open an investigation. The timestamp, converted or not, never tells them that.&lt;/p&gt;

&lt;p&gt;So: store the offset-less string as a string, label whose clock it is, treat "no clock" as its own state, don't badge evening deliveries, and put the scan location where the eye lands first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; (24htrack.com), a free package tracker for 3,200+ carriers - paste any tracking number, the carrier is detected automatically, no sign-up. The percentages above come from delivery scans on our own platform. There is a longer, non-technical version of this as an &lt;a href="https://github.com/doanhnd9989/package-tracking-guide/blob/main/guides/delivery-scan-time-meaning.md" rel="noopener noreferrer"&gt;open guide&lt;/a&gt; (CC BY).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Translating carrier scan text: all-or-nothing, or don't bother</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:19:19 +0000</pubDate>
      <link>https://dev.to/support24htrack/translating-carrier-scan-text-all-or-nothing-or-dont-bother-4p54</link>
      <guid>https://dev.to/support24htrack/translating-carrier-scan-text-all-or-nothing-or-dont-bother-4p54</guid>
      <description>&lt;p&gt;Every cross-border tracking UI eventually hits the same wall: a scan line arrives in a language your users cannot read, and you have to decide what to render.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;邮件离开处理中心【广州】
已妥投【US California Olivehurst 95961】
包裹已签收，如有疑问请联系收件人
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The naive fix is a phrase dictionary plus &lt;code&gt;str.replace&lt;/code&gt;. That is the right idea and the wrong implementation, and the failure modes are specific enough to be worth writing down. Everything below came out of shipping this on a live corpus of Chinese-language scan lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A half-translated sentence is worse than an untranslated one
&lt;/h2&gt;

&lt;p&gt;This is the rule that decides the whole design.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;包裹已签收，如有疑问请联系收件人&lt;/code&gt; contains a phrase you probably have in your dictionary (&lt;code&gt;已签收&lt;/code&gt; = delivered, signed for) and a clause you probably do not (&lt;em&gt;if you have questions, contact the recipient&lt;/em&gt;). Replace the part you know and the user gets:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Delivered (signed for)，如有疑问请联系收件人&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That reads as &lt;strong&gt;broken software&lt;/strong&gt;, not as foreign text. Worse, it hides which half you actually understood — the untouched half might be an instruction the user needs to act on.&lt;/p&gt;

&lt;p&gt;So: translate all of it, or hand back the original byte for byte.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gloss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;PHRASES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;` &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;            &lt;span class="c1"&gt;// recognised nothing: unchanged&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PROSE_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;  &lt;span class="c1"&gt;// real prose survived: give up&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tidy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. You need a boundary between "place name" and "sentence"
&lt;/h2&gt;

&lt;p&gt;The all-or-nothing check needs to answer: &lt;em&gt;is what's left a proper noun, or a clause I failed to read?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Chinese does not put spaces between words, so you cannot count tokens. What works is a &lt;strong&gt;character-run threshold&lt;/strong&gt;. Measured against real place names and real clauses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;东莞市&lt;/code&gt; (Dongguan City) — 3 characters&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;深圳&lt;/code&gt; (Shenzhen) — 2&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;如有疑问请联系收件人&lt;/code&gt; — 10&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four is the boundary. A run of 4+ Han characters is a clause; shorter is a name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROSE_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;぀-ヿ一-鿿가-힯&lt;/span&gt;&lt;span class="se"&gt;]{4,}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And strip bracketed payloads before testing, because carriers put the location inside &lt;code&gt;【…】&lt;/code&gt; and you deliberately never translate those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;strip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/【&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;】&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*】/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Ordering is load-bearing, and getting it wrong is silent
&lt;/h2&gt;

&lt;p&gt;Dictionary entries overlap. &lt;code&gt;邮件离开处理中心&lt;/code&gt; contains &lt;code&gt;离开处理中心&lt;/code&gt; contains &lt;code&gt;离开&lt;/code&gt;. Put the short one first and the long one can never match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;邮件到达处理中心【SZ】
  → 邮件 Arrived at sorting centre 【SZ】   // orphan characters, prose gate trips, whole row reverts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we put the bare forms first by accident, it made thousands of rows worse — and nothing threw. The output was just &lt;em&gt;slightly&lt;/em&gt; wrong on a page nobody was diffing.&lt;/p&gt;

&lt;p&gt;Two habits that catch this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Longest-prefix first, enforced by a test that asserts entry order, not just entry presence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff old vs new over your whole live corpus before shipping&lt;/strong&gt;, and count three things: rows changed, rows emptied, rows that lost more than half their length. "Rows emptied" should be zero, always.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Never gloss a bare noun
&lt;/h2&gt;

&lt;p&gt;We had a &lt;code&gt;处理中心&lt;/code&gt; → "sorting centre" entry. It reached &lt;em&gt;inside&lt;/em&gt; a facility name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;【深圳邮区中心邮件处理中心】  →  【深圳邮区中心邮件 sorting centre】
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every entry should be a &lt;strong&gt;verb plus its object&lt;/strong&gt;, or a pattern that consumes the brackets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/离开&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;【&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;】&lt;/span&gt;&lt;span class="se"&gt;]{0,40}&lt;/span&gt;&lt;span class="sr"&gt;】&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;处理中心/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Departed $1 sorting centre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/到达&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;【&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;】&lt;/span&gt;&lt;span class="se"&gt;]{0,40}&lt;/span&gt;&lt;span class="sr"&gt;】&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;处理中心/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Arrived at $1 sorting centre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Pad your replacements, then collapse
&lt;/h2&gt;

&lt;p&gt;No spaces between words means a replacement welds itself to whatever follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;邮件离开处理中心Dongguan  →  Departed sorting centreDongguan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pad both sides on insert, then collapse runs and trim before punctuation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*，&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s{2,}&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;,.;:)&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;】&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the full-width comma &lt;code&gt;，&lt;/code&gt; — leave it in and you get Chinese punctuation inside English sentences.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Do this on the read path only
&lt;/h2&gt;

&lt;p&gt;The glossed string is for display. The stored value stays exactly as the carrier sent it.&lt;/p&gt;

&lt;p&gt;That is not tidiness. Anything derived from status text — a &lt;code&gt;delivered_at&lt;/code&gt; stamp, a downgrade guard, a "stop polling this parcel" rule — must key off the carrier's own words. Translate on write and you have quietly made your business logic depend on the current contents of a phrase table. Change the table, change the meaning of historical rows.&lt;/p&gt;

&lt;p&gt;One function, called from every render surface. No new call sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All-or-nothing: fully translated, or byte-identical.&lt;/li&gt;
&lt;li&gt;4+ CJK characters in a row means a clause, not a name — bail out.&lt;/li&gt;
&lt;li&gt;Longest phrase first, and test the ordering.&lt;/li&gt;
&lt;li&gt;Verb + object, never a bare noun.&lt;/li&gt;
&lt;li&gt;Pad on insert, collapse after.&lt;/li&gt;
&lt;li&gt;Display layer only; never the write path.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you want to see the output rather than the code: &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; is a free package tracker for 3,200+ carriers — paste any tracking number, the carrier is detected automatically, no sign-up — and the recognised Chinese scan lines are rendered in English on the timeline. There is a plain-English walkthrough of the phrases themselves &lt;a href="https://youtu.be/aTsF2Y52FE8" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance; the measurements and the failure cases are from our own production corpus.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>i18n</category>
      <category>webdev</category>
      <category>javascriptlibraries</category>
    </item>
    <item>
      <title>Stop showing "last updated 5 days ago" on order tracking - classify the scan instead</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Tue, 29 Sep 2026 04:45:30 +0000</pubDate>
      <link>https://dev.to/support24htrack/stop-showing-last-updated-5-days-ago-on-order-tracking-classify-the-scan-instead-m70</link>
      <guid>https://dev.to/support24htrack/stop-showing-last-updated-5-days-ago-on-order-tracking-classify-the-scan-instead-m70</guid>
      <description>&lt;p&gt;If you build order-status pages, there is a good chance you render something like &lt;code&gt;Last updated 5 days ago&lt;/code&gt; and let the customer decide what it means. They decide it means the parcel is lost, and they open a ticket.&lt;/p&gt;

&lt;p&gt;Elapsed time is close to useless as a signal on a tracking timeline, because a tracking feed is not a location stream. It is an append-only log of handling events. A row appears only when a human or a machine physically scans the barcode. Between scans the parcel is moving and nothing is written. So "no event for five days" is not an error state - on some legs it is the expected state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal you actually have
&lt;/h2&gt;

&lt;p&gt;The useful signal is the type and place of the most recent event, not its age. Roughly five classes cover almost everything you will see:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Typical wording&lt;/th&gt;
&lt;th&gt;Is silence expected?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ORIGIN_DEPARTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;departed, left facility, handed to airline&lt;/td&gt;
&lt;td&gt;Yes - longest gaps in the journey&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IMPORT_PENDING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;arrived at destination country, import scan, customs clearance&lt;/td&gt;
&lt;td&gt;Yes, unless duties or documents were requested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DOMESTIC_HUB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;arrived at a depot or sorting centre in the destination country&lt;/td&gt;
&lt;td&gt;No - short leg remaining&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HANDOVER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;handed over to local partner, transferred to another carrier&lt;/td&gt;
&lt;td&gt;Yes, permanently - this carrier stops publishing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NO_SCAN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nothing at all&lt;/td&gt;
&lt;td&gt;Depends how long since the label was created&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once you have that, the copy writes itself, and it is different copy per class rather than one "still on its way" for everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// deliberately boring: substring rules over a normalised string&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;classify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destCountry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/handed over|transferred to|local partner/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HANDOVER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/customs|import|arrived at destination country/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IMPORT_PENDING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/departed|left .*facility|handed to airline/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;destCountry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ORIGIN_DEPARTED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;destCountry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMESTIC_HUB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IN_TRANSIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shouldEscalate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destCountry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destCountry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;864&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IMPORT_PENDING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dutiesRequested&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMESTIC_HUB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;   &lt;span class="c1"&gt;// short leg: a week is odd&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HANDOVER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;      &lt;span class="c1"&gt;// track the second carrier&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;                                &lt;span class="c1"&gt;// cross-border linehaul&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth building in beyond the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A handover is a terminal state for that carrier, not a pause.&lt;/strong&gt; Once a parcel is handed to a last-mile partner, the original carrier will never add another row. If your UI polls only the primary carrier, it will show "no update" forever while the parcel is being delivered under a different number. Model handover as its own visible state and, where you can, follow the second leg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distinguish "no scan yet" from "scans stopped".&lt;/strong&gt; A number that has never had a single event is usually a label that was printed but not handed over - or a customer pasting an order number instead of a tracking number. That is a different email from a parcel that moved and then went quiet, and conflating them is where a lot of support looping comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this cuts tickets
&lt;/h2&gt;

&lt;p&gt;The question behind almost every where-is-my-order contact is not "where is it" but "is something wrong, and do I need to do something". A status line that answers that directly - waiting is correct here, or this one needs you to pay duties, or this is worth chasing - resolves the contact before it is made. &lt;code&gt;Last updated 5 days ago&lt;/code&gt; answers none of it.&lt;/p&gt;

&lt;p&gt;If you want to see what a real timeline looks like across both legs, &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; is free and needs no sign-up: paste a number and the carrier is detected automatically, across 3,200+ carriers. There is also a &lt;a href="https://www.24htrack.com/api" rel="noopener noreferrer"&gt;REST API and an MCP server&lt;/a&gt; if you would rather pull events into your own UI, and an open guide repo with status wordings and number formats: &lt;a href="https://github.com/doanhnd9989/package-tracking-guide" rel="noopener noreferrer"&gt;https://github.com/doanhnd9989/package-tracking-guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at 24hTrack, with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>ecommerce</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
