Skip to content
ComputingAmerica

Systems

EDI 214 in practice: why your ETAs are wrong

The status feed is not a truth feed. Almost every bad ETA we have been asked to fix comes from four failures that are visible in the message itself.

7 min readComputing America

In short

  • A 214 reports a status somebody entered, not a position somebody measured. Treating it as an observation of the truck is the root cause of most ETA defects.
  • X12 standardizes the envelope, not the meaning. Each trading partner maps its internal statuses onto the code list differently, so the mapping belongs in data you can edit, not in code you have to redeploy.
  • Status timestamps arrive late, out of order and duplicated. A shipment view built on last-write-wins will show an arrived load as in transit, and it will be right about the last message and wrong about the world.
  • Every ETA a customer sees needs an age and a source attached. An unqualified time on a portal is a promise your operations team did not make.
  • The fix is an append-only event log per shipment with both the observed time and the received time recorded, and a derived view on top. Correcting history stops being a data-repair ticket.

A shipper calls because the portal said the load was two hours out and it arrived the next morning. A broker calls because the same shipment shows delivered in one screen and in transit in another. In both cases the engineering conversation starts with the tracking integration, and in both cases the integration is doing exactly what it was told.

The X12 214, the transportation carrier shipment status message, is the feed underneath most of these screens. It is a good standard and a poor oracle, and the difference is where the defects live. A 214 tells you that a carrier’s system recorded a status, at a time it believes, for a stop it identifies in its own vocabulary. None of those three things is the same as knowing where the truck is.

One thing has to work before any of the four matters, and it is the one that fails silently. A status message identifies its shipment by whatever reference the carrier holds: a pro number, a bill of lading, your order number if somebody keyed it correctly, and often a different one of those per partner. When the reference does not match, the event does not attach to a shipment, and in most integrations we are handed it is simply discarded. Nobody notices, because a shipment that receives no updates looks exactly like a shipment with nothing to report.

Match on more than one reference, and route the failures to a queue a human can see rather than to a log nobody reads. The size of that queue is the single best health metric a tracking integration has, and it is usually the first number that tells you a partner changed something on their side.

Failure one: the timestamp does not mean what the column says

The single most common defect we find is a local time stored as if it were absolute. A status event carries a time and, separately, a qualifier saying which time zone that time is in. Partners omit the qualifier, send it inconsistently, or send local time for the stop rather than local time for the carrier’s dispatch office. A pipeline that takes the time and drops the qualifier produces errors that are exactly one to three hours wide, which is small enough that nobody spots it until a delivery window is missed.

Daylight saving is the sharpened version of the same problem. Twice a year, an hour is repeated and an hour does not exist. Any freight moving across those two nights produces status sequences that appear to go backwards, and a system that sorts by timestamp will happily render a delivery before its pickup. Store the offset, or store UTC alongside the reported local time and the zone it came from. Do not reconstruct it later from the stop’s address, because by then you have lost the thing that told you which reading was intended.

There is a second time on every event, and it is the one teams forget. The time the status happened and the time you learned about it are different quantities, and both matter. Freshness questions, service-level questions and every dispute after the fact are answered with the received time. If your table has one timestamp column, you have already thrown away half of the record.

Failure two: the code list is standard, the meaning is not

X12 publishes the transaction set and its status and reason codes. What it cannot publish is which of a carrier’s internal operational states each code represents. One partner sends arrived when the driver crosses the geofence at the facility. Another sends it when the truck is at the dock and checked in. A third sends it when a clerk works through a stack of paperwork at the end of the shift, which is why the timestamps from that partner cluster suspiciously at 17:40 every day.

None of those partners is wrong. They are describing their own operation, and the semantics only become inconsistent when you average them into one number on one screen. So do not average them. Keep the mapping from partner code to your internal state as configuration a person can inspect and change, keep the raw code on the event forever, and expect the mapping to be wrong on the first week of any new partner. It is easier to defend a mapping table in a meeting than an if-statement in a deploy queue.

Failure three: messages arrive late, twice and out of order

Status messages are not a stream you can trust to be ordered. A carrier’s system queues them, retries them, and resends a batch after an outage. You will receive a departure after the arrival that followed it, and you will receive the same event twice with slightly different envelope metadata. This is normal, and it is why the shipment view cannot be a row you update in place.

The shape that works is an append-only event log keyed by shipment and stop, with a deterministic identity per event so a resend is recognized rather than double-counted, and a current view derived from that log by ordering on the observed time rather than on arrival. Late data then corrects history automatically, which turns a support ticket into a recomputation. A system built on last-write-wins has no way to express that a message received now describes an earlier moment, so it will overwrite good state with stale state and be entirely confident about it.

Idempotency belongs at the same layer. Deriving the event key from the shipment identifier, the stop, the status code and the reported time costs nothing and removes an entire category of duplicate-alert incident. The alternative, deduplicating on the interchange control number, works only until a partner resets its counters, which they do.

Failure four: nobody computed the ETA you are displaying

An ETA reported by a carrier is a commercial statement. It reflects a plan, a customer relationship and sometimes a reluctance to be the first to say a load is late. An ETA computed from a position feed is a physical estimate. These are different quantities with different failure modes, and displaying them in the same field, in the same font, is a decision to lose that distinction permanently.

Physical estimates have a constraint that route calculators routinely ignore: the driver’s available hours. Federal hours-of-service records exist for every driver on a regulated move, under 49 CFR 395.8, and they set hard limits on when a truck can legally proceed. An ETA that puts a load at a dock during a mandatory rest period is not an optimistic estimate, it is an impossible one, and operations people spot it immediately. That is the moment a portal loses their trust, and it is very hard to win back.

What to display instead

  • Every ETA carries an age and a provenance: computed from position at 14:20, or reported by carrier at 09:05. The customer does not need your architecture, but they do need to know whether they are reading an hour-old fact or a yesterday-old promise.
  • Stale is a state, not a missing value. After a threshold, a shipment stops showing an ETA and starts showing when the last status arrived. A blank is more honest than a number that is quietly nine hours old.
  • Exceptions are surfaced by silence as well as by events. The shipment that has sent nothing for six hours is more interesting than the one that reported a delay, and only a system that tracks the received time can find it.
  • Reason codes reach the screen. An operations user who can see that a delay was consignee-related rather than carrier-related makes a different call, and that difference is the whole value of the feed.

What none of this fixes

None of this makes a bad feed good. If a partner sends statuses in a batch at the end of the day, no architecture recovers the intraday truth, and the correct response is a commercial conversation, not an engineering project. What the design above buys you is the ability to know which of your partners are like that, prove it with your own data, and stop presenting their feed as though it were live.

It also makes the eventual position feed easy to add. Telematics, an ELD provider, or a carrier API become another event source into the same log, ranked above the 214 when both describe the same moment. Teams that build the log first treat that as a Tuesday. Teams that built a status column treat it as a rewrite.

Sources

  1. 1.X12 Transaction Sets, X12
  2. 2.49 CFR 395.8: Driver’s record of duty status, Electronic Code of Federal Regulations

Next step

Tell us what’s breaking.

Forty-five minutes, no charge, no deck. We’ll tell you what we’d do, what it would likely cost, and whether you should be building this at all.