DEV Community

24hTrack
24hTrack

Posted on Fully Autonomous

Your WISMO handler returns a status. It should return an owner.

Every WISMO ("where is my order") handler I have read does the same thing: fetch the latest tracking event, template it into a sentence, return it. Status in, status out.

That is the one answer the customer already has. They are writing because they read it.

The useful output is not a status. It is an owner: who — the seller, the carrier, or nobody yet — can act on this parcel right now. That is computable from the scan history, and it collapses most of the ticket volume.

Why "owner" and not "status"

The shipping contract belongs to whoever bought the label. That is the merchant, not the recipient. So carriers routinely decline to open an investigation for a recipient and bounce them back to the seller. If your support flow answers a WISMO with a status, the customer takes that status to the carrier, loses two days, and comes back angrier. If it answers with an owner, the next step is correct on the first try.

In the EU this is also the legal default: Article 20 of the Consumer Rights Directive keeps the risk of loss or damage with the trader until the consumer takes physical possession where the trader arranged carriage.

The decision function

Four inputs, all of which you already have:

first_scan_at        # null if the carrier has never scanned it
last_scan_at
last_scan_country    # vs destination country
status_text          # the carrier's own wording, not your normalised bucket
Enter fullscreen mode Exit fullscreen mode
def owner(t, today, p90_gap_for_carrier):
    # 1. Explicit wording always wins. These are the only states
    #    where the BUYER is the blocker.
    if matches(t.status_text, DUTIES | DOCUMENTS | BAD_ADDRESS | HELD_FOR_COLLECTION):
        return "buyer_action"

    # 2. Never scanned => the parcel is still with the merchant.
    if t.first_scan_at is None:
        return "seller"            # label bought, box not handed over

    # 3. Terminal-but-wrong.
    if t.status == "delivered" and t.customer_says_missing:
        return "seller"            # only the sender can request POD

    # 4. Still abroad or pre-arrival: nobody is holding it.
    if t.last_scan_country != t.destination_country:
        return "wait" if gap(t, today) < p90_gap_for_carrier else "seller"

    # 5. Arrived in-country, then silence. This is the real escalation.
    return "seller" if gap(t, today) > DOMESTIC_QUIET_DAYS else "wait"
Enter fullscreen mode Exit fullscreen mode

Four notes on the parts that bite.

Rule 1 is first for a reason. Duties owed, documents required, bad address and held-for-collection are the states where nothing moves until a human acts, and the last one expires — often in one to two weeks, after which the parcel is returned. A flow that buckets these into a generic "exception" and then applies a time threshold will sit on them until the deadline passes.

Use the carrier's wording, not your bucket. Normalising to IN_TRANSIT | EXCEPTION | DELIVERED throws away exactly the string that decides rules 1 and 3. Keep the raw text, match on it, and normalise only for display.

p90_gap_for_carrier must be per carrier. A fixed "no update in 7 days = problem" threshold fires on a large share of parcels that arrive perfectly normally, because the quiet stretch is a property of the lane. Measured on delivered parcels, the longest silent gap runs to a median around 4 days, and more than one in four go a full week with no scan at all. Compute the p90 per carrier from your own delivered set and use that.

Measure on delivered parcels only. If you compute gap distributions over everything currently in flight you are measuring censored data: the parcels still moving have not finished their gaps yet, so every percentile comes out short.

The handover is the case people get wrong

On cross-border orders, the carrier named on your tracking often only covers the first leg. A local operator finishes the delivery, sometimes under its own reference. So the origin carrier's feed goes quiet because it no longer has the parcel — not because anything is wrong.

Two consequences for the model: do not treat "origin feed stopped" as an exception, and do not require a second tracking number to exist, because plenty of lanes keep publishing the last leg under the original number. Rule 4 handles this by asking where the last scan was rather than how long ago it was.

What this changes in practice

The reply stops being "no update since Tuesday" and becomes one of four things: wait, and here is how long is normal on this lane; we are opening an investigation; pay this / send this document today; or check with the shop, and here is the exact sentence to send them. Each of those ends the thread. The status restatement does not — it produces the same ticket again on day five.


I work on 24hTrack, a free multi-carrier package tracker — paste any tracking number and the carrier is detected automatically, no sign-up. The gap figures above come from parcels tracked on the platform; the per-carrier dataset is public under CC BY 4.0 in package-tracking-guide, along with a plain-English version of the owner decision tree.

Top comments (0)