Between October 2024 and April 2025, we built the RouteLyft carrier feed layer — integrating enough EU freight carrier tracking APIs to cover the major lanes on our platform. The span was roughly 30 carrier connections, ranging from large pan-European carriers to mid-size regional operators running specific corridors. We were prepared for the technical work. We were less prepared for the specific character of the problems we'd encounter.
This is not a post about how hard carrier APIs are in the abstract. It is a post about the specific things that bit us, in the order that felt most surprising when they happened.
1. Legacy EDI is not going away — it is running production traffic
Before we started this project, we assumed EDIFACT-based EDI was a legacy concern for large-enterprise integrations. That turned out to be wrong. Several of the mid-size EU carriers we integrated still run their tracking feeds as IFTSTA messages over SFTP or AS2 — file-based, periodic, with no webhook option. One carrier's tracking feed was a flat CSV dropped to an FTP server every 15 minutes. The column names were in German, inconsistently formatted, and the timestamp encoding was a European date-time format with a period as the date separator that broke our initial parser silently — it parsed without error, but produced timestamps off by 8–10 hours due to timezone ambiguity in the source format.
The lesson is not that EDIFACT is bad engineering. IFTSTA works. DESADV and IFTMIN carry the data they need to carry. The lesson is that if your integration layer assumes REST + JSON as the baseline and treats EDI as an exception, you will spend a disproportionate amount of time on carriers whose traffic volume matters. Some of the highest-volume Central and Eastern European corridor carriers — the Antwerp→Warsaw and Hamburg→Prague lanes in particular — run on EDIFACT infrastructure and have no near-term plans to migrate.
2. Rate limits are documented; rate limit enforcement is not
Carrier tracking APIs universally have rate limits. They are typically stated in documentation as "N requests per minute" or "N requests per hour." What the documentation almost never tells you is how enforcement is implemented. We hit two distinct failure modes here.
The first: a carrier whose documented limit was 60 requests per minute began returning 429 errors at around 40 requests per minute during peak hours. On investigation, the limit was per-endpoint, not per-account — a distinction the documentation did not make. The second: a different carrier had a limit that was enforced at the IP address level, not the API key level. Our staging and production environments shared a NAT gateway IP during a brief configuration period, and both environments were drawing against the same quota. The carrier's API returned 200 responses with empty result sets rather than 429 errors when the quota was exhausted — a particularly insidious behaviour because it looked like a data gap rather than a rate-limit condition.
The practical response is to instrument your integration layer not just for HTTP error codes but for semantic data quality: if a carrier feed that typically returns 15–30 tracking events per hour starts returning consistently empty responses, that is an anomaly to alert on regardless of the HTTP status code.
3. Carrier reference numbers and our shipment identifiers are different things
This sounds obvious, and it is obvious — but the specific failure mode it produces is subtle. Each carrier uses its own shipment reference scheme. Our platform needs to resolve from our internal shipment identifier to the carrier's reference in order to query the carrier API. In an ideal world, the freight forwarder provides the carrier reference when creating the shipment in our system, and we store it. In practice, the carrier reference is sometimes not assigned at booking time, it is assigned at collection. The forwarder sends us a shipment at 09:00; the carrier assigns its reference at 13:00 when the driver collects.
If our integration layer attempts to query the carrier API between 09:00 and 13:00 using an identifier that does not yet exist in the carrier's system, we get a 404 — which looks identical to a 404 returned for an invalid reference. We had a brief period where the retry logic treating these as "invalid" was silently writing off shipments as untrackable that simply hadn't been collected yet. The fix was to maintain a per-shipment "reference confirmed" flag and hold queries until the carrier reference was validated, rather than retrying indefinitely on 404.
4. ETA fields are populated by different logic across carriers — the field name is not the contract
Most carrier tracking APIs expose an estimated delivery timestamp. We assumed — reasonably, we thought — that this field represented the carrier's best current estimate of delivery, updated based on current position and conditions. It does not, for a significant subset of carriers.
For several carriers we integrated, the ETA field in the tracking response is populated from the original booking's committed delivery window, not from a dynamically updated estimate. It does not change when the truck is delayed. It does not change when the truck is running two hours ahead of schedule. It is, functionally, a delivery commitment window disguised as a current estimate. If you pass this value directly to a freight forwarder's TMS as an ETA update, you are giving them stale commitment data as if it were real-time prediction — which is worse than no ETA at all, because it actively suppresses exception detection.
We now maintain a carrier metadata record that includes the ETA field semantics: dynamic_estimate, commitment_window, or unknown. For carriers tagged as commitment_window, the ETA field is displayed in the coordinator UI with a label indicating it is the booked delivery window, not a current estimate, and it is excluded from our ETA deviation alert logic.
5. Webhook reliability varies more than REST polling reliability
We're not saying REST polling is better than webhooks as an architectural pattern — for most carrier integrations, a well-implemented webhook reduces latency and load significantly compared to polling. But webhook delivery reliability in practice is lower than REST polling reliability, and in ways that are not visible without explicit monitoring.
Carriers that support webhooks deliver them over HTTP POST to an endpoint we control. Delivery failures — our endpoint returning a non-200, network timeouts, TLS handshake failures — are handled inconsistently. Some carriers retry on 5xx with exponential backoff; others make exactly one attempt and mark the event as delivered regardless of our response. For carriers in the second category, a 30-second window of our endpoint being unavailable (a deployment, a momentary network issue) can result in several hours of missed events with no recovery mechanism on the carrier side.
The architecture that handles this reliably uses webhooks as the primary channel but maintains a polling fallback that runs on a longer interval specifically to detect gaps in the webhook stream. A carrier feed that has delivered no webhook events in 4 hours when the shipment is active gets polled directly to check for missed events. This dual-channel approach adds engineering overhead but is the only way to avoid silent data gaps.
6. Normalising status codes across carriers requires a semantic taxonomy, not a string map
Every carrier has its own status code vocabulary. "In transit," "out for delivery," "at depot," "customs hold," "delivery attempted" — these concepts are present across virtually all carrier tracking systems, but they are expressed in dozens of different code sets, sometimes in multiple languages within the same carrier's API. Naive string mapping — carrier status code X maps to our status Y — breaks every time a carrier updates its status taxonomy, which happens more often than you might expect.
We moved to a semantic classification layer early in the project. Rather than mapping carrier codes directly to our display statuses, we classify each carrier status code into a small set of normalised states with defined semantics: in_transit, at_facility, awaiting_customs, customs_hold, delivered, exception. The classification is per-carrier and versioned — when a carrier updates their status taxonomy, we update the classification map for that carrier in isolation without touching the downstream display logic or alert rules.
The broader lesson from six months of carrier integrations is that the heterogeneity is load-bearing — it is not an anomaly to be normalised away but a structural feature of a market composed of hundreds of independent operators running infrastructure built over different decades. The integration layer that scales is the one designed around heterogeneity as the default, not the exception.