DEV Community

life
life

Posted on Edited on

Validate the address before you print the label, not after the parcel bounces

Every ecommerce engineer has written the "is this address filled in" check. Non-empty, maybe a regex on the postcode, done. That check is not validation, and on cross-border orders it is the most expensive shortcut in the pipeline, because a bad address discovered after the parcel has left the country costs a multiple of the order value.

Here is the pipeline we run at the order-intake step for China-origin small parcels, before anything reaches a pick list.

Step 1: normalise before you judge

Never validate raw user input. Carriers match on their own canonical forms, so your job first is to get the string into a shape a carrier will recognize.

function normalise(addr) {
  return {
    line1: addr.line1.replace(/\s+/g, ' ').trim(),
    unit: (addr.unit || '').replace(/(apartment|apt|suite|ste|unit|#)\s*/i, '').trim(),
    city: addr.city.replace(/\s+/g, ' ').trim().toLowerCase(),
    postcode: cleanPostcode(addr.country, addr.postcode),
    phone: toE164(addr.country, addr.phone),
  };
}
Enter fullscreen mode Exit fullscreen mode

Two things matter more than they look. Unit numbers get typed as 48 when the customer meant 4B, so keep the unit as its own field and never concatenate it into line1 before validation. And phone: a local carrier will call or SMS the recipient when a delivery fails, so a phone in the wrong format is not a cosmetic problem, it is the difference between a held parcel and an abandoned one.

Step 2: ask the carrier, not a generic database

Every serious destination-market carrier exposes an address verification endpoint. Use it, because it knows things a global geocoder does not: whether this specific subbuilding is deliverable, whether the postcode resolves to that city, whether the street exists in that district.

async function scoreAddress(addr) {
  const res = await carrierVerify(normalise(addr));
  return {
    verdict: res.verdict,        // 'clean' | 'corrected' | 'ambiguous' | 'unknown'
    suggestion: res.suggestion,  // carrier's canonical version
    confidence: res.confidence,  // 0..1 where the API provides it
  };
}
Enter fullscreen mode Exit fullscreen mode

The trap here is treating it as binary. Most of these APIs return a correction rather than a rejection, and silently applying a carrier's rewrite means your customer's saved address drifts into something they no longer recognize. Show the correction, ask once, and record which version you shipped against.

Step 3: route on the verdict, do not just log it

This is the part that actually saves money. A validation call that only writes to a log changes nothing.

const POLICY = {
  clean:     'release',      // go to pick
  corrected: 'confirm',      // one message to buyer, hold until reply
  ambiguous: 'confirm',
  unknown:   'hold',         // never ship an unresolvable address internationally
};
Enter fullscreen mode Exit fullscreen mode

confirm should be a single question with the carrier's suggested version pre-filled and one button. Not a support ticket. If you have ever watched a "please confirm your address" email go out as free text, you know the reply rate.

Step 4: keep the result attached to the order

Store the verdict, the carrier response, and the timestamp on the order record. When a parcel comes back and you want to know whether it was your failure or the customer's, that record is the whole argument. Without it you are guessing, and the carrier claim process will guess the same direction.

What this buys you

The reason this is worth a day of engineering is that the failure it prevents is not recoverable downstream. Once a small parcel is in international transit, a wrong address means either a local disposal, a return leg billed as a fresh shipment, or an import declaration in your own name on goods you already exported. None of those end well, and all of them start with 48 where the buyer meant 4B.

We run this in front of the three Chinese warehouses FulfillNexa by SBT (fulfillnexa.com) works from (Shenzhen 3,000 m², Suzhou 13,000 m², Dongguan 8,000 m²), and the unglamorous truth about it is that it produces no interesting metrics. The number of returns it prevents is a number of tickets nobody filed.

Validate the address while it is still a string. After it is a tracking number you are only choosing which kind of loss you want.

Top comments (0)