A 3PL client in Fontana closed out November and found October freight spend running nearly $4,000 over plan. It took two weeks to find the cause: 45 days earlier the carrier had quietly changed its remote-area surcharge rules. Nobody updated the TMS, the WMS rating engine was still shipping on the old rules, and the invoice showed up 45 days late. The freight moved fine. The money leaked quietly.
That incident changed how I review every WMS-TMS project. I check the callback path first, not the outbound push. Anybody can push a shipment to a TMS. Everything breaks on the way back.
The chain itself is simple: the WMS pushes shipment data to the TMS or carrier, the carrier picks up, moves, and delivers, and status updates and invoices flow back up the chain. All three of the failure points live in those return trips.
The callback died, and the order is "in transit" forever
The classic scene: the package was delivered two days ago, the customer calls asking about proof of delivery, and the CSR looks in the WMS — still "in transit." The TMS webhook never fired, or the carrier's POD never came back. The order sits stuck, inventory stays allocated, and at month end there's a pile of "shipped" records floating in limbo while finance reconciles them by hand, one tracking number at a time.
The root cause is almost always a state machine drawn too coarse. Most integrations model two states — pushed and complete — and map every TMS status (in transit, out for delivery, delivered) to "complete." Exceptions (returned, voided, damaged) don't even get their own callback channel. What I run now: the WMS shipment state is split into at least five — pushed, picked up, in transit, delivered, and exception. Every TMS status code gets an explicit mapping home, and anything that doesn't fit goes into an exception pool for a human to review.
My mapping table looks like this: TMS picked_up and departed map to picked up; in_transit, arrived_at_hub, and out_for_delivery map to in transit; delivered maps to delivered; exception, returned_to_sender, voided, damaged, and lost all go to the exception pool — they are never allowed to auto-flow into delivered. That last line is the red line. Once an exception gets mislabeled as delivered, customer service, finance, and inventory are all wrong at the same time, and untangling it takes three times the work.
Then add belt and suspenders: webhooks for real time, plus a scheduled pull as backup. The backup job runs hourly and actively queries the TMS for anything stuck "in transit" longer than 24 hours. It's a small piece of code and the highest-ROI patch I know in this space. One more monitor: alert if the webhook receiver goes 30 minutes with zero callbacks. Callback chains die silently. Nobody watching means three days of silence.
The invoice arrives late with mismatched fields, and month end is off by thousands
Carrier invoices routinely land 30 to 45 days after the fact, and fuel surcharge tables and remote-area rules change without ceremony. When the WMS estimate doesn't match the invoice, finance always assumes the WMS is wrong first. More often than not, a rule version never got synced.
Nail the field-mapping checklist first. This is the foundation everything else reconciles against:
| WMS field | TMS / carrier invoice field | Notes |
|---|---|---|
| Shipment ID | tracking number / PRO number | The unique key across the whole chain, the reconciliation primary key |
| Billed weight | billed weight | Watch the actual-vs-dimensional greater-of rule |
| Destination zone | zone | Keep versions when zone tables change |
| Service level | service level | ground / 2-day / overnight, etc. |
| Fuel surcharge | fuel surcharge | Match the rate version by invoice date |
| Accessorial detail | accessorial charges | Remote area, oversize, residential — line by line |
| Quoted at ship time | quoted amount | The estimate at tender time |
| Invoiced | invoiced amount | What the carrier actually bills |
| Invoice date | invoice date | Determines which rate version applies |
I never hard-code specific rates — carrier rate tables and surcharge policies change year to year, so treat the carrier's latest published policy as the source of truth. What the system needs is a rule-version table: every rating rule carries an effective date, and reconciliation matches the rule version against the invoice date. The Fontana client's $4,000 died on the missing rule-version table.
Then run two reconciliation windows instead of one month-end scramble. Seven days after ship date, run the estimate reconciliation: WMS estimate versus the TMS-returned cost, and flag anything deviating more than 5%. Then run the final reconciliation within three days of the invoice arriving. Splitting the windows means problem shipments surface in week one instead of day 45.

The estimate window has a side benefit: it catches systemic drift in the rating engine. If one service level — say overnight — comes in 8% or more under the returned cost for two weeks running, you're almost certainly missing an accessorial in the rating rules. Fix it early and the savings compound. The final reconciliation has a different job: invoice versus returned cost, line by line, and anything that doesn't match goes straight into the dispute process with the carrier. Carrier invoice correction windows are limited — once the window closes, overpaid money is gone for good, so check each carrier's current policy.
Retries without idempotency: the same shipment pushed three times
The network hiccups, the WMS doesn't get the TMS acknowledgment, and the old logic retries the push. Except the TMS actually got the first request — only the response was lost. Now the same shipment exists three times in the TMS, and some carriers will happily generate three labels. Worse, the retry sometimes overwrites the first quote, so the invoice matches nothing at all.
Two fixes, and both are non-negotiable. Send a globally unique request ID with every push; the TMS and carrier dedupe on that ID, and a repeated ID returns the first result without re-executing the business action. Use fixed-interval plus exponential-backoff retries — say 5 minutes, 30 minutes, 2 hours — and anything past the limit goes to a manual queue. Never retry forever. And check the local "sent registry" before every send: a request with a registry entry never gets re-sent. Idempotency costs half a day in design and saves three days of forensic accounting.
One detail people miss: never regenerate the request ID on retry. Every retry of the same shipment must carry the same request ID, or the TMS dedupe is decorative. Timeout tuning matters too. Carrier APIs get slow in peak season; a 5-second timeout fires retries constantly, while 30 seconds plus backoff cuts false retries dramatically.
My verdict on what scale deserves deep integration
Here's the short version. Under 300 orders a day with one or two carriers: CSV reconciliation is enough. Export the cost detail from the TMS weekly, join it against the WMS ship records, and half a day of manual work closes the month. Building real-time integration for that volume — the development and maintenance cost almost certainly exceeds what it saves. Not worth it.
Over 500 orders a day, or three-plus carriers with mixed service levels: deep integration with automated reconciliation is mandatory. At that volume, manual reconciliation labor and the overpay/underpay risk already exceed the build cost. The middle band — 300 to 500 — can go semi-automated: push and status callbacks over API, invoice reconciliation via semi-automated CSV import, and fill in the rest when volume grows.
One hard rule regardless of scale: in the first week after go-live, hand-check 20 shipments a day end to end — WMS push, TMS status, carrier invoice, all three sides. That week of manual work is what buys you a quiet year.
What you can do tomorrow: run one report tonight listing every WMS order stuck "in transit" longer than 48 hours, and check each one's real status in the TMS. Until that gap is closed, every reconciliation, every analysis, every cost-saving project is a house built on sand. Plug the callback holes first, then talk optimization.










