<?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>ASD-US-F is not a tracking number: stop looking up service codes</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Thu, 22 Oct 2026 04:33:21 +0000</pubDate>
      <link>https://dev.to/support24htrack/asd-us-f-is-not-a-tracking-number-stop-looking-up-service-codes-nc8</link>
      <guid>https://dev.to/support24htrack/asd-us-f-is-not-a-tracking-number-stop-looking-up-service-codes-nc8</guid>
      <description>&lt;p&gt;Some of the strings people paste into a tracking box cannot possibly be tracking numbers, and you can tell before you call anything.&lt;/p&gt;

&lt;p&gt;We run a free multi-carrier tracker (&lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt; — paste any tracking number, the carrier is detected automatically, no sign-up), so our tracking box takes whatever a shopper happens to be holding. Looking at search demand for one carrier's page this month, a cluster of queries turned out to be people trying to track strings like &lt;code&gt;ASD-US-F&lt;/code&gt; and &lt;code&gt;ASD-US-M&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those are not identifiers. They are service codes — they name the shipping product that was purchased. The same string sits on every parcel sent that way. There is nothing to look up, and no carrier API will ever resolve one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this reaches your code at all
&lt;/h2&gt;

&lt;p&gt;Three fields travel together through a checkout, a WMS, a label API and a notification email:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;identifies&lt;/th&gt;
&lt;th&gt;cardinality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;service / product code&lt;/td&gt;
&lt;td&gt;the tariff bought&lt;/td&gt;
&lt;td&gt;a handful per carrier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;order reference&lt;/td&gt;
&lt;td&gt;the order in the shop's DB&lt;/td&gt;
&lt;td&gt;one per order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tracking number&lt;/td&gt;
&lt;td&gt;one physical parcel&lt;/td&gt;
&lt;td&gt;one per parcel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They get mixed up because all three are short alphanumeric strings that arrive in the same payload, and because a notification template that interpolates the wrong one still renders fine. The bug surfaces later, as "tracking not working", with no stack trace attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only safe client-side verdict
&lt;/h2&gt;

&lt;p&gt;It is tempting to write a validator that decides what &lt;em&gt;is&lt;/em&gt; a tracking number. Don't. Carrier formats collide, change, and contradict their own documentation. What you can decide locally is the narrow negative: &lt;strong&gt;this string cannot be an identifier.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DIGITS&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&lt;/span&gt;&lt;span class="sr"&gt;/g&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;function&lt;/span&gt; &lt;span class="nf"&gt;classifyPastedCode&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="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;trim&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;s&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;empty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// \s already covers NBSP and figure space, which is what PDFs and HTML tables leak in.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compact&lt;/span&gt; &lt;span class="o"&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;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;/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;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIGITS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;compact&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="c1"&gt;// A service/product code names a TARIFF, not a parcel.&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;digits&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service_code&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 digits at all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compact&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;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;compact&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service_code&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;hyphen-segmented and mostly letters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compact&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;compact&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;8&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;too_short&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compact&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;candidate_tracking_number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compact&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="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compact&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;Output on real inputs, including a number with a non-breaking space pasted into the middle of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"ASD-US-F"             -&amp;gt; service_code               no digits at all
"ePacket"              -&amp;gt; service_code               no digits at all
"Economy Delivery"     -&amp;gt; service_code               no digits at all
"AS123456789US"        -&amp;gt; candidate_tracking_number  len=13 digits=9
"AS&amp;lt;NBSP&amp;gt;123456789&amp;lt;NBSP&amp;gt;US" -&amp;gt; candidate_tracking_number  len=13 digits=9
"1Z999AA10123456784"   -&amp;gt; candidate_tracking_number  len=18 digits=15
"ZHPHD260713551753ML"  -&amp;gt; candidate_tracking_number  len=19 digits=12
"9400150206217912"     -&amp;gt; candidate_tracking_number  len=16 digits=16
"ORD-77421"            -&amp;gt; candidate_tracking_number  len=9 digits=5
"AB-12"                -&amp;gt; too_short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines deserve attention.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ORD-77421&lt;/code&gt; is a shop order reference and it passes through as a candidate — deliberately. Some carriers &lt;em&gt;do&lt;/em&gt; accept a sender reference as a lookup key, sometimes paired with a postcode. A client-side rule that rejected it would break a working path to save a round trip. The classifier's job is to stop the impossible, not to pre-empt the carrier.&lt;/p&gt;

&lt;p&gt;The non-breaking-space line matters more than it looks. Numbers copied out of a PDF or an HTML table arrive full of them, and a plain &lt;code&gt;/\s/&lt;/code&gt; already strips them — that one is free. What it does &lt;strong&gt;not&lt;/strong&gt; strip is a zero-width space, because U+200B is not whitespace at all; it is a format character. If you want those gone too, &lt;code&gt;/\p{Cf}/u&lt;/code&gt; catches them, and that is the invisible character most likely to make a perfectly correct-looking number fail a strict regex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things worth changing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Store the service code in its own column.&lt;/strong&gt; If it lands in &lt;code&gt;tracking_number&lt;/code&gt;, every downstream consumer inherits a value that can never resolve, and you cannot tell those rows apart from genuine not-founds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Separate "no number yet" from "number not found".&lt;/strong&gt; They look identical in a UI that only has a spinner, and they need opposite responses: wait, versus go back to the sender. Most of the "your tracking is broken" reports we see are the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Never resolve a guess into a concrete carrier silently.&lt;/strong&gt; Store an unresolved state instead. A guessed carrier is indistinguishable from a confirmed one once it is written, and the first carrier response that actually finds the number can settle it properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Echo the input back when you reject it.&lt;/strong&gt; The person still has the label in front of them. &lt;code&gt;Could not read "ASD-US-F" as a tracking number — that looks like a service name. The parcel number is usually longer and mostly digits.&lt;/code&gt; is a message someone can act on; &lt;code&gt;Invalid tracking number&lt;/code&gt; is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it keeps happening
&lt;/h2&gt;

&lt;p&gt;The parcel usually has two companies on it. One gets it out of the origin country, another delivers it — and each has its own numbering, its own service names, and its own vocabulary in the scan text. The handover is where a single &lt;code&gt;carrier&lt;/code&gt; string and a single &lt;code&gt;tracking_number&lt;/code&gt; string stop being enough to describe what happened. Modelling custody as a sequence rather than a field is a bigger change than this classifier, but the classifier is the cheap half, and it removes a whole class of support ticket on day one.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is a service code ever trackable?&lt;/strong&gt; No. It describes a product, not a parcel, so it has nothing to resolve to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know the real number then?&lt;/strong&gt; For the carrier in the example above it is 13 characters, mostly digits, typically starting &lt;code&gt;AS&lt;/code&gt; and ending &lt;code&gt;US&lt;/code&gt;. In general: mostly digits, usually 10-22 characters. If all you have is a service name, the sender still has the parcel number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I validate check digits?&lt;/strong&gt; Where a carrier publishes one, yes — it is the only client-side check that is genuinely conclusive. Just don't extend it by analogy to carriers that don't document one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about an unrecognised format?&lt;/strong&gt; Try the lookup anyway, and keep the answer. "Carrier says not found" is real information; "my regex didn't match" is not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by the team at &lt;a href="https://www.24htrack.com" rel="noopener noreferrer"&gt;24hTrack&lt;/a&gt;, a free multi-carrier tracking site. The search-demand observation comes from our own platform; the classifier is the one in our notes, run as shown before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Storing Tracking Numbers as Numbers: a float64 Bug Hiding in Your CSV Pipeline</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:14:48 +0000</pubDate>
      <link>https://dev.to/support24htrack/stop-storing-tracking-numbers-as-numbers-a-float64-bug-hiding-in-your-csv-pipeline-2j9p</link>
      <guid>https://dev.to/support24htrack/stop-storing-tracking-numbers-as-numbers-a-float64-bug-hiding-in-your-csv-pipeline-2j9p</guid>
      <description>&lt;p&gt;If any part of your stack reads a shipping CSV, this one is probably already in your data and nobody has noticed. A tracking number is a &lt;strong&gt;label&lt;/strong&gt;, but almost every tool in the chain is happy to treat a long run of digits as a &lt;strong&gt;quantity&lt;/strong&gt; - and quantities get rounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure, in two shapes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9214490424550000386431   -&amp;gt;  9.2144904245500004E+21
9400150206217912         -&amp;gt;  9400150206217910
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is loud. The second is the one that costs you a week: same length, no exponent, no warning - the final digit is now a zero. Every downstream lookup returns "not found" for parcels that are completely fine, and the first instinct is to blame the carrier's API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;IEEE-754 double precision carries about 15-17 significant decimal digits. Look at what common formats need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;All digits?&lt;/th&gt;
&lt;th&gt;Survives a float?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;USPS IMpb&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FedEx Ground&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;borderline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DPD parcel number&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes, but still reformatted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPS &lt;code&gt;1Z&lt;/code&gt; + 16&lt;/td&gt;
&lt;td&gt;18 chars&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S10 postal (&lt;code&gt;AA999999999CC&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;13 chars&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern that matters: &lt;strong&gt;the formats that survive are the ones containing letters&lt;/strong&gt;, because a letter forces the value to stay a string. The all-digit formats are the exposed ones - and that includes the single most common barcode in the world, the 22-digit USPS IMpb.&lt;/p&gt;

&lt;p&gt;This is also why the bug feels intermittent. A column of &lt;code&gt;1Z...&lt;/code&gt; numbers round-trips perfectly, someone concludes the pipeline is fine, and then the first USPS batch of the month is silently mangled.&lt;/p&gt;

&lt;h2&gt;
  
  
  In code
&lt;/h2&gt;

&lt;p&gt;Pandas will do this to you by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# wrong - pandas infers int64/float64 and your 22-digit numbers are gone
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipments.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# right - say it explicitly
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipments.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracking_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# or, when you do not want to name every column
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipments.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same rule in the other languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JS/TS:&lt;/strong&gt; never &lt;code&gt;Number(tn)&lt;/code&gt; or &lt;code&gt;parseInt(tn)&lt;/code&gt;. A 22-digit value is already past &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; (9007199254740991, 16 digits), so the damage happens before you can validate it. If you need a numeric type, use &lt;code&gt;BigInt&lt;/code&gt; - but you almost never do, because you never do arithmetic on a tracking number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL:&lt;/strong&gt; &lt;code&gt;VARCHAR&lt;/code&gt;, not &lt;code&gt;BIGINT&lt;/code&gt;. Leading zeros are real and significant in several postal formats, and a numeric column eats them without comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON:&lt;/strong&gt; quote it. An unquoted 22-digit literal is a double in most parsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excel/Sheets exports:&lt;/strong&gt; format the column as Text &lt;strong&gt;before&lt;/strong&gt; the first paste. Formatting a column that already holds broken values does not recover the digits - that information is gone, not hidden.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Validating a column you inherited
&lt;/h2&gt;

&lt;p&gt;Cheapest checks first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracking_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# loud corruption
&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;value_counts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                &lt;span class="c1"&gt;# a USPS column should be all 22s
&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&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="nf"&gt;value_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# an implausible pile of "0" = rounding
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is the useful one. If a column has the right lengths but an unreasonable share of values ending in &lt;code&gt;0&lt;/code&gt;, you are looking at rounding, not coincidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validation rule that actually helps
&lt;/h2&gt;

&lt;p&gt;Do not validate tracking numbers with a numeric range or a length-only check. Check the &lt;strong&gt;shape&lt;/strong&gt;: a letter-anchored prefix plus a fixed length is both cheap and specific. &lt;code&gt;^1Z[0-9A-Z]{16}$&lt;/code&gt; identifies UPS exactly. &lt;code&gt;^[A-Z]{2}\d{9}[A-Z]{2}$&lt;/code&gt; is the S10 international postal form, and the last two letters are the issuing country - not the destination, which is a different and equally common mix-up.&lt;/p&gt;

&lt;p&gt;And if the number survives shape validation but returns nothing, check whether the label was merely created before you conclude anything is lost - "we have the order details but not the parcel" is a legitimate state, not an error.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by the team behind &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 and the carrier is detected automatically, no sign-up. Shape and format notes come from the carrier formats we maintain at &lt;a href="https://github.com/doanhnd9989/package-tracking-guide" rel="noopener noreferrer"&gt;package-tracking-guide&lt;/a&gt; (CC BY 4.0).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop using scan count as a health signal: it measures handovers, not progress</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:42:36 +0000</pubDate>
      <link>https://dev.to/support24htrack/stop-using-scan-count-as-a-health-signal-it-measures-handovers-not-progress-213f</link>
      <guid>https://dev.to/support24htrack/stop-using-scan-count-as-a-health-signal-it-measures-handovers-not-progress-213f</guid>
      <description>&lt;p&gt;If you ingest carrier tracking data, you will eventually be asked for a "is this shipment healthy?" signal, and the cheapest-looking feature in the payload is &lt;code&gt;events.length&lt;/code&gt;. It is the wrong feature, and the way it is wrong is systematic rather than noisy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the field actually counts
&lt;/h2&gt;

&lt;p&gt;A tracking event is written when a parcel changes custody or location &lt;em&gt;and somebody records it&lt;/em&gt;. Nothing writes an event because time passed. So &lt;code&gt;events.length&lt;/code&gt; is a count of &lt;strong&gt;recorded handovers&lt;/strong&gt;, and the number of handovers is a property of the &lt;strong&gt;route&lt;/strong&gt;, not of how well the shipment is going.&lt;/p&gt;

&lt;p&gt;Two consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A short list is usually a short route.&lt;/li&gt;
&lt;li&gt;A long list is usually a long relay that is reporting honestly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers that make this concrete
&lt;/h2&gt;

&lt;p&gt;Average events per parcel and median first-scan-to-delivered, measured over a 90-day window ending 2026-10-19 on parcels tracked through our platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shipping model&lt;/th&gt;
&lt;th&gt;Avg events&lt;/th&gt;
&lt;th&gt;Median days&lt;/th&gt;
&lt;th&gt;p80 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Destination-country warehouse, domestic leg only&lt;/td&gt;
&lt;td&gt;~6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional carrier, single country&lt;/td&gt;
&lt;td&gt;~4&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-border, forwarder hands to national post&lt;/td&gt;
&lt;td&gt;~12&lt;/td&gt;
&lt;td&gt;~22&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-border, operator runs its own destination network&lt;/td&gt;
&lt;td&gt;~18&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-border via destination regional hub&lt;/td&gt;
&lt;td&gt;~17&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-border, densest reporting&lt;/td&gt;
&lt;td&gt;~25&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the first and last rows together. &lt;strong&gt;Six events, one day. Twenty-five events, eight days.&lt;/strong&gt; If you had ranked those two by event count you would have ranked them backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The modelling mistake
&lt;/h2&gt;

&lt;p&gt;The mistake is treating one integer as a quality score across heterogeneous routes. &lt;code&gt;events.length&lt;/code&gt; is only comparable &lt;em&gt;within&lt;/em&gt; a route archetype. Across archetypes it is confounded by the number of legs, so any threshold you pick (&lt;code&gt;&amp;lt; 5 events = stalled&lt;/code&gt;) fires hardest on your fastest lane.&lt;/p&gt;

&lt;p&gt;A workable decomposition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;route_archetype&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;origin_country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination_country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;expected_events&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;route_archetype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staleness&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staleness_budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;p80&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inter_event_gap&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;route_archetype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;leg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;alert&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;staleness&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;staleness_budget&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The alerting signal is &lt;strong&gt;staleness against a per-archetype budget&lt;/strong&gt;, not count. Count is at best a weak prior on &lt;code&gt;expected_events&lt;/code&gt; and should never be the trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why percentiles, not means, for the budget
&lt;/h2&gt;

&lt;p&gt;Gap distributions here are long-tailed because two legs produce no events at all: linehaul and customs. On delivered parcels the median longest silent stretch is about &lt;strong&gt;4 days&lt;/strong&gt;, p90 about &lt;strong&gt;10&lt;/strong&gt;; 58% of &lt;em&gt;successfully delivered&lt;/em&gt; parcels go three or more days with no event. A mean-based threshold is dragged up by the tail and fires late; a p80/p90 budget per leg behaves.&lt;/p&gt;

&lt;p&gt;Store the percentiles per &lt;code&gt;(carrier, leg)&lt;/code&gt; and recompute them on a schedule. They drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The identity problem underneath
&lt;/h2&gt;

&lt;p&gt;One more thing that corrupts count-based metrics: a cross-border parcel frequently acquires a &lt;strong&gt;second identifier&lt;/strong&gt; at the destination handover. The forwarder's number and the final carrier's number are different strings for the same physical box. If your pipeline keys on the string, you get two short records instead of one long one, and both look "stalled by count" while the parcel is moving normally.&lt;/p&gt;

&lt;p&gt;Keying on a shipment entity with a set of identifiers, rather than on the identifier itself, removes a whole class of phantom alerts - and it is also what lets you show a customer one continuous timeline instead of two broken ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to expose in the API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;events[]&lt;/code&gt; with a normalized &lt;code&gt;status&lt;/code&gt; plus the carrier's raw text (do not throw the raw string away; it carries the collection-point and handover language your enum cannot represent)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_event_at&lt;/code&gt; - the field people actually need&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;staleness_days&lt;/code&gt; computed server-side, so clients stop reimplementing it badly&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;not&lt;/strong&gt; expose a &lt;code&gt;health_score&lt;/code&gt; derived from count. You will be asked for it. It will be wrong for your best lane.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checking your own data
&lt;/h2&gt;

&lt;p&gt;Group delivered shipments by route archetype, then compare median event count against median transit time within each group. If the correlation flips sign between groups - and it usually does - count is not a health signal in your data either.&lt;/p&gt;




&lt;p&gt;Written by the team at &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. Open dataset of transit times and silent-gap percentiles, CC BY 4.0: &lt;a href="https://github.com/doanhnd9989/package-tracking-guide" rel="noopener noreferrer"&gt;package-tracking-guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI: Fully Autonomous&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>logistics</category>
    </item>
    <item>
      <title>Stop treating an empty tracking response as one state (measured: 97% vs 1 in 5)</title>
      <dc:creator>24hTrack</dc:creator>
      <pubDate>Thu, 15 Oct 2026 04:56:26 +0000</pubDate>
      <link>https://dev.to/support24htrack/stop-treating-an-empty-tracking-response-as-one-state-measured-97-vs-1-in-5-3bmi</link>
      <guid>https://dev.to/support24htrack/stop-treating-an-empty-tracking-response-as-one-state-measured-97-vs-1-in-5-3bmi</guid>
      <description>&lt;p&gt;If you are building anything that consumes shipment tracking, there is one modelling mistake that quietly generates most of your support load: treating "no data" as a single state with a single policy.&lt;/p&gt;

&lt;p&gt;I spent today measuring that assumption against real traffic, and the numbers are wider apart than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;p&gt;Every tracking number submitted to our tracker over 30 days, grouped by carrier, asking one question: what share of these numbers ever reached a real status?&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;Reached a status&lt;/th&gt;
&lt;th&gt;Scans when they did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UPS&lt;/td&gt;
&lt;td&gt;~97%&lt;/td&gt;
&lt;td&gt;~12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FedEx&lt;/td&gt;
&lt;td&gt;~1 in 3&lt;/td&gt;
&lt;td&gt;~11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Australia Post&lt;/td&gt;
&lt;td&gt;~1 in 5&lt;/td&gt;
&lt;td&gt;~12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same ingest path, same day, same code. A 5x spread.&lt;/p&gt;

&lt;p&gt;Now look at the third column, because that is the part that changes your design. &lt;strong&gt;When a number reports, all three carry about the same amount of history.&lt;/strong&gt; So this is not "carrier A publishes richly, carrier B publishes sparsely". Every one of them publishes richly once anything shows up. The variance is entirely in &lt;em&gt;whether the first scan reaches you at all&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why that breaks the usual design
&lt;/h2&gt;

&lt;p&gt;The common implementation looks like 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="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&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;events&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;0&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;daysSinceCreated&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;markSuspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;scheduleRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FIXED_INTERVAL&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;One &lt;code&gt;N&lt;/code&gt;. One interval. Applied to every carrier.&lt;/p&gt;

&lt;p&gt;With the spread above, a single &lt;code&gt;N&lt;/code&gt; is wrong in both directions at once. Tuned for UPS, it flags most Australia Post shipments as suspect - they are not, they simply have not surfaced. Tuned for Australia Post, it is so permissive that a genuinely stuck UPS parcel sits unnoticed for a week.&lt;/p&gt;

&lt;p&gt;The same bug shows up in the retry schedule. An empty response from a carrier with a 97% answer rate is &lt;strong&gt;informative&lt;/strong&gt;: it probably means pre-transit, and the first scan is likely imminent, so a tighter poll pays. An empty response from a 20% carrier is mostly &lt;strong&gt;uninformative&lt;/strong&gt; about the parcel, and polling it harder buys nothing except rate limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three states, not one
&lt;/h2&gt;

&lt;p&gt;The fix is to stop collapsing distinct conditions into &lt;code&gt;events.length === 0&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-transit.&lt;/strong&gt; The label exists; nothing has been scanned. The number is valid and the parcel is real, it just has not entered the network. This is the majority of empty responses on every carrier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not visible here.&lt;/strong&gt; The number belongs to a network whose history is not reachable from where you are standing. Nothing is wrong with the parcel; you are querying the wrong source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a carrier number.&lt;/strong&gt; An order reference, an internal ID, a reused string. It will never resolve anywhere, no matter how long you retry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These need different handling, and critically, &lt;strong&gt;different copy&lt;/strong&gt;. State 1 deserves "the carrier has not scanned it yet". State 2 deserves "check the carrier's own site". State 3 deserves "this does not look like a tracking number - here is what one looks like for this carrier". A single "No tracking information available" string for all three is how you generate tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical rules that fell out of this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep per-carrier baselines, not one global threshold.&lt;/strong&gt; Store the observed answer rate and median time-to-first-scan per carrier, and derive your retry interval and your "suspect" threshold from those. The same two lines of config, applied per carrier, removes most false alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the shape as a cheap pre-filter.&lt;/strong&gt; Validating at ingest catches state 3 before you ever make a request. UPS is &lt;code&gt;1Z&lt;/code&gt; plus sixteen alphanumerics, eighteen total, with a trailing check digit - a mistyped one fails arithmetic, not a lookup. FedEx numbers are overwhelmingly twelve digits. A regex and a checksum at the input boundary is cheaper than an infinite retry loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure delivery windows instead of quoting service levels.&lt;/strong&gt; For parcels still moving when first tracked, we measure medians of roughly 2 days (FedEx), 4 days (UPS) and 6 days (Australia Post), with 80th percentiles of about 6, 7 and 13 days. Carrier marketing pages will not give you those numbers, and your "is this late?" logic needs percentiles, not brochures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat p80, not the median, as your alert threshold.&lt;/strong&gt; Half of all parcels are slower than the median by definition. Alerting there means alerting on half your shipments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is not technical
&lt;/h2&gt;

&lt;p&gt;Having done the measurement, we changed the product to match it. On carrier pages where we rarely resolve numbers, the page now says so at the top, in plain words, instead of promising real-time updates it cannot deliver. It costs clicks. It is still correct, because a user sent away with a true answer is worth more than one kept on a page that was never going to help.&lt;/p&gt;

&lt;p&gt;Your API should do the same thing. Return a state your caller can branch on, not an empty array that forces them to guess.&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. There is a &lt;a href="https://www.24htrack.com/api" rel="noopener noreferrer"&gt;REST API and MCP server&lt;/a&gt; if you want to consume this programmatically, and the longer write-up of the measurement is &lt;a href="https://github.com/doanhnd9989/package-tracking-guide/blob/main/guides/why-tracking-page-is-blank.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>logistics</category>
    </item>
    <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>
  </channel>
</rss>
