How to Cut Over a Live Logistics Integration Without Losing Shipments
For: COO or Head of Technology at a mid-sized freight or 3PL company who has signed off on replacing their legacy TMS or carrier API layer and is now staring at a go-live date with trucks, bookings, and in-transit shipments all depending on a system they're about to turn off
The only safe way to cut over a live logistics integration is to run both systems in parallel, route new bookings to the new system on day one, and let the old system own every shipment it originally booked until that shipment reaches a terminal state. Do not migrate in-flight shipments. Do not turn the old system off on a date. Turn it off when its last active shipment is delivered, returned, or written off. Everything else in this post is the mechanics of doing that without dropping webhooks, double-booking carriers, or losing status events.
This playbook is for the COO or Head of Technology at a freight or 3PL company who has already signed off on replacing a legacy TMS or carrier API layer, and now has trucks moving, bookings landing, and webhooks firing on a system that has to be retired without a maintenance window. If that is you, read the seven steps in order. They are opinionated on purpose.
The situation this playbook assumes
- You have a legacy TMS, carrier integration middleware, or shipment orchestration layer being replaced.
- You cannot take a downtime window. Bookings and status events flow 24/7.
- Carriers push webhooks (delivered, exception, POD, returned) that can arrive hours or days after a booking.
- You have shipments already in transit that were booked on the old system.
- Your customers (shippers, consignees, internal ops) expect uninterrupted tracking.
If any of those are not true — for example, if you have a genuine maintenance window, or you only have one carrier — you can use a simpler plan. This one is for the hard case.
The insight that determines everything else
The failure point in a logistics system integration cutover is never the data migration. Master data (customers, lanes, rate cards, carrier credentials) is a solved problem. You export, transform, load, reconcile. Boring.
The failure point is the in-motion shipment. A load booked on Tuesday on the old system will emit a delivery webhook on Friday. If Friday's webhook lands on the new system, the new system has no idea what shipment that carrier reference belongs to. It never issued the booking. It has no consignee, no PO, no billing context. The event either gets dropped, gets rejected, or worse — gets attached to the wrong shipment because reference numbers collided.
Every step below exists to keep in-motion shipments on the system that booked them, until they finish.
Step 1: Freeze the contract surface, not the code
Before you write a line of migration code, freeze the external contract of your logistics platform. That means: the shape of a booking request, the shape of a status event as it appears to downstream consumers (ERP, customer portal, billing, BI), the webhook signatures your carriers push, and the API your shippers call.
Both the old system and the new system must speak this contract identically during cutover. If they don't, every downstream system becomes a migration project of its own.
In practice, this means putting a thin routing layer in front of both systems — call it the shipment router — that normalizes inbound requests and outbound events. Not a rewrite. A shim. It reads a single field (which system owns this shipment) and forwards accordingly.
Anti-pattern: letting the new system have a “better” API and asking downstream consumers to migrate at the same time. You are now running two migrations. Do not.
You'll know this step is done when a booking request or a carrier webhook can be replayed against either system through the router and produce identical downstream events.
Step 2: Build a shipment ownership registry
Create a single table — outside both the old and new systems — that answers one question for every shipment ID and every carrier reference number: which system owns this?
Schema, roughly:
shipment_id | carrier_ref | owner_system | booked_at | terminal_state | terminal_atThis registry is the source of truth for the router. When a webhook arrives with carrier reference ABC123, the router looks up ABC123, sees owner_system = “legacy”, and forwards to the old system. When a new booking is created after cutover date, it gets written with owner_system = “new”.
Two things matter here:
- Carrier reference is the join key, not your internal shipment ID. Carriers push events with their reference, not yours. If you only index by internal ID, you cannot route webhooks.
- The registry must be populated for every historical shipment that is still in a non-terminal state on cutover day. Run this export the night before. Then run it again the morning of.
Anti-pattern: trying to make the new system aware of legacy shipments by importing them. You will import stale state. Carriers will push updates the new system misinterprets. Don't.
You'll know this step is done when you can query the registry for any shipment ID or carrier reference from the last 90 days and get a definitive owner.
Step 3: Cut new bookings over on a hard date. Cut nothing else.
Pick a date. At 00:00 on that date, every new booking is created on the new system. Every booking created before that date stays on the old system until it terminates.
This is the entire cutover. There is no big-bang. There is no migration of active loads. The old system keeps running, but its booking API is closed to new work. It only exists to service the shipments it already owns.
Two operational details:
- Booking API on the old system returns a hard error after cutover. Not a silent redirect. You want to catch any client still calling the old endpoint immediately.
- The router enforces this. If a booking request lands and the client hasn't been migrated, fail loudly. Do not helpfully forward.
The reason this works is that logistics shipments have a bounded lifetime. A parcel terminates in days. LTL and FTL in a week or two. Ocean freight in weeks. International returns and claims can drag out for a couple of months. So the old system has a known, finite tail — not an indefinite obligation.
You'll know this step is done when the new system is receiving 100% of new bookings and the old system's booking count is monotonically decreasing every day.
Step 4: Route webhooks by ownership, not by system
This is the step everyone gets wrong. Carriers do not know you migrated. They will push status events to whatever webhook URL is on file. If you have one URL per carrier, all events land in one place — and that place has to route them.
The shipment router is the webhook endpoint. When a webhook arrives:
- Parse the carrier reference from the payload.
- Look it up in the ownership registry.
- Forward the raw payload to the owning system.
- Log the routing decision with a correlation ID.
Some carriers let you configure webhook URLs per shipment at booking time. Use that where possible — it removes the router lookup for new shipments because the new system's URL is registered directly with the carrier at booking. But you still need the router for legacy shipments whose webhook URL was set months ago.
Anti-pattern: letting the new system “handle unknown shipments gracefully.” It shouldn't handle them at all. Unknown shipment = router misconfiguration = page someone.
You'll know this step is done when you can replay a week of production webhook traffic through the router in a staging environment, and every event lands in the correct system with zero drops and zero cross-routes.
Step 5: Reconcile daily, not weekly
During the parallel period, run a reconciliation job every morning at, say, 06:00 local. It answers three questions:
- How many shipments are still active on the old system? (Should trend down.)
- How many shipments on either system have not received a status update in longer than expected for their lane and mode? (Stuck shipments.)
- How many webhooks were routed yesterday, and did any hit the “unknown shipment” path?
Publish this to a channel where ops leadership sees it. Not a dashboard nobody opens. A daily message.
Stuck shipments are the tell. If a load has been in transit for eight days on a two-day lane and no status has come through, either the carrier stopped emitting events or the router dropped them. Either way, an operator needs to call the carrier and manually update state. Do not let this pile up.
Anti-pattern: waiting for a customer to call and ask where their shipment is. By then you have lost visibility on dozens more.
You'll know this step is done when the daily reconciliation runs unattended for two weeks and the ops team trusts the numbers enough to act on them without asking engineering.
Step 6: Drain the old system deliberately
Every week during the parallel period, look at the remaining shipments on the old system and classify them:
- Normal in-transit — leave alone.
- Long-tail but progressing — international, awaiting POD, in customs. Leave alone.
- Stuck with no realistic path to terminal state — carrier lost the load, POD never received, claim in dispute. These need a manual decision: force-close, write off, or migrate to the new system as a fresh record with a note.
The third category is why cutovers drag. There will always be a handful of shipments that never cleanly terminate. Set a policy — for example, after 90 days past expected delivery, force-close with an exception code — and apply it uniformly. Otherwise the old system runs forever for the sake of six loads nobody wants to touch.
You'll know this step is done when the old system has fewer than a handful of active shipments and every one has a named owner and a decision date.
Step 7: Decommission on evidence, not on calendar
Only turn off the old system when:
- Zero active shipments remain (or all remaining ones have been force-closed per policy).
- No webhook has been routed to the old system in a defined quiet period — say, 14 consecutive days.
- Downstream systems (billing, BI, customer portal) confirm they no longer read from the old system.
- You have a full archival snapshot of the old system's database, stored somewhere durable, with a documented restore path in case a claim, audit, or dispute surfaces months later.
Then, and only then, put the old system in read-only mode for 30 days. Then shut it down.
Anti-pattern: decommissioning on a calendar date because someone in finance wants the license fee off the books. Every logistics team that has done this has regretted it the first time a chargeback dispute needs data from a shipment three months back.
You'll know this step is done when the old system is off, downstream consumers didn't notice, and you can still answer a “where was this shipment on March 14” question from the archive.
Failure modes to watch for
Things that have gone wrong on live logistics API migrations:
- Carrier reference collision. Two carriers issued the same reference format and the router matched the wrong shipment. Fix: always key on (carrier_id, carrier_ref), never on carrier_ref alone.
- Webhook retries after ownership change. A carrier retried a webhook 24 hours later, after a shipment had been force-closed on the old system. The router forwarded it, the old system rejected it, and the retry queue backed up. Fix: the router should recognize terminal-state shipments and ack-and-drop rather than forward.
- Silent double-booking. A client integration cached the old system's endpoint and kept calling it. The old system dutifully accepted the booking. Nobody noticed for a week. Fix: hard-error on the old booking endpoint from day one of cutover.
- Timezone drift in reconciliation. Daily reconciliation ran in UTC, ops team read it in IST or CST, and the “yesterday” window didn't match. Stuck shipments were undercounted. Fix: reconcile in the ops team's timezone and label the window explicitly.
- Rate card divergence. The new system had slightly different rate lookups. Shipments booked on it were priced differently than the same shipment would have been on the old one. Finance caught it in month-end. Fix: run a rate parity check on the first 500 bookings on the new system before you trust it.
How CodeNicely can help
We've built and rebuilt logistics platforms where cutover risk was the entire project risk. The one most relevant here is Vahak — a trucking marketplace where bookings, carrier assignments, and shipment status all had to keep moving while the underlying platform was rebuilt. The work that mattered was not the new features. It was designing the boundary between old and new so that in-flight loads didn't get orphaned and driver-facing flows didn't break during the transition.
If you're staring at a TMS or carrier integration replacement and you need someone who has actually stood up a shipment router, ownership registry, and webhook routing layer in production — rather than someone selling you a platform — that's the shape of engagement we take on. You keep full IP. No vendor lock-in. See our digital transformation work or services overview for the broader picture.
Frequently Asked Questions
Can I do a TMS cutover with a maintenance window instead?
Only if your carriers, shippers, and in-transit shipments can genuinely pause — which is almost never true for a live 3PL or freight operation. Even a two-hour window means missed webhooks that carriers won't retry indefinitely. The parallel-run approach in this playbook exists because a real maintenance window is not available to most logistics operators.
What happens to shipments that were booked on the old system but haven't shipped yet on cutover day?
They stay on the old system. The old system dispatches them, tracks them, and closes them out. The rule is: whichever system created the booking owns the shipment until it reaches a terminal state (delivered, returned, cancelled, or force-closed). Do not try to migrate pending shipments to the new system — you will lose carrier context.
How long should the parallel run last?
Long enough for every shipment booked on the old system to reach terminal state, plus a quiet period on webhooks. For domestic parcel and LTL that's typically short; for ocean freight, international, and claims-in-dispute it's longer. Set a force-close policy for long-tail stuck shipments so the old system doesn't run forever. For a specific plan against your shipment mix, talk to CodeNicely for a personalized assessment.
Do I need to change carrier webhook URLs during cutover?
Ideally no — you point all carrier webhooks at your own router, and the router forwards to whichever system owns each shipment. This avoids coordinating URL changes with every carrier and prevents lost events during the switch. For carriers that support per-shipment webhook URLs, new bookings on the new system can register the new system's URL directly, but you still need the router for legacy shipments.
How do we handle billing and reconciliation during the parallel period?
Both systems generate billing events into a shared downstream billing pipeline via the normalized contract from Step 1. Finance should not care which system a shipment ran on. Run a parity check in the first weeks — pick a sample of shipments on each system and verify rate application, fuel surcharge, and accessorial charges match expectations. Catch divergence before month-end close, not after.
What if we discover the new system has a bug after cutover?
Because new bookings and legacy bookings are cleanly separated by ownership, you can pause new bookings on the new system without touching legacy shipments still running on the old one. That's the operational advantage of not migrating in-flight state — rollback of new bookings is a routing-layer config change, not a data restore.
Building something in Logistics?
CodeNicely partners with founders and tech teams to ship AI-native products that move metrics. Tell us about the problem you're solving.
Talk to our team_1751731246795-BygAaJJK.png)