Every shipping integration I have seen stores a carrier 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.
Here is the evidence I keep running into, and the four design rules I would now apply from day one.
1. One carrier can have two correct names
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 AU-YTO.
So carrier === "YTO Express" and carrier === "Yuantong" describe the same company, and a user searching for one will never match a record stored under the other.
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.
Rule: carrier names need an alias table, and the alias table needs to be the thing you query.
// Minimal shape. The point is the normalisation, not the list.
const ALIASES = {
ontrac: ['ontrac', 'on trac', 'on track'],
cainiao: ['cainiao', 'cai niao', 'caniao'],
straightship: ['straightship', 'straight ship'],
'yto-express': ['yto', 'yto express', 'yuantong', 'yuan tong', '圆通速递', 'au-yto'],
};
const norm = s => String(s ?? '')
.toLowerCase()
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, ' ') // keep CJK, drop punctuation
.trim();
const INDEX = new Map(
Object.entries(ALIASES).flatMap(([key, names]) => names.map(n => [norm(n), key]))
);
export const resolveCarrier = input => INDEX.get(norm(input)) ?? null;
Two details that matter more than the list itself:
-
\p{L}with theuflag, not\w.\wis ASCII-only, so圆通速递normalises to an empty string and every CJK name collapses into the same key. (We have shipped that bug. Once.) -
NFKCbefore stripping, because full-width Latin characters turn up in labels printed by Asian systems andYTOis notYTOunder a naive comparison.
2. The carrier on the label is not always the carrier that delivers
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.
If your data model has shipment.carrier as a write-once field set at ingest, you will be wrong about a large fraction of international shipments by the time they arrive.
Rule: carrier is mutable state, not an identity. Model it as "best current attribution", with a timestamp, and let it be revised.
Practically that means:
-- not this
carrier VARCHAR(40) NOT NULL -- set once at insert
-- closer to reality
carrier VARCHAR(40) -- current best attribution
carrier_source ENUM('declared','detected','reported')
carrier_updated DATETIME
The carrier_source 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.
3. Number shape is a hint for some carriers and noise for others
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.
Of the four carriers above, exactly one has a shape you can lean on:
| Carrier | Shape | Usable as a signal? |
|---|---|---|
| Straightship | five letters + 13 digits, 18 chars, overwhelmingly STRCA…
|
yes — one shape dominates |
| OnTrac | mixed; some D-prefixed, many all-digit |
no |
| YTO Express | mixed; several prefixes in regular use | no |
| Cainiao | mixed; may carry a partner's number entirely | no |
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.
Rule: detection returns three outcomes, not two. recognised / ambiguous / unrecognised. Collapsing ambiguous into unrecognised 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".
// Three outcomes, not a boolean
{ status: 'recognised', carrier: 'straightship', confidence: 'shape' }
{ status: 'ambiguous', candidates: ['ontrac', 'usps'] }
{ status: 'unrecognised', reason: 'no known carrier issues this shape' }
4. Never block a lookup because the name did not match
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.
If your form has a carrier selector, make it optional and make it a filter, not a precondition. The number is the identifier. The name is a label somebody typed.
Why this adds up
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.
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.
I work on 24hTrack, a free multi-carrier package tracker — paste any tracking number, the carrier is detected automatically, no sign-up. There is also a REST API and an MCP server 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.
Top comments (0)