From the outside, a shipping integration looks simple: get a rate, buy a label, track a parcel. After building connections for 31 carriers, we can say that the hard part is everything that’s different between them.
First, an honest note
These connections were built from each carrier’s public documentation. Most have only been tested against stand-ins we wrote, not the carriers’ own test systems, because that needs an account with each one. A connection isn’t proven until it has run against a real account, so we say that on our product pages too.
No two carriers agree
Canada Post’s developer APIs use XML and Basic authentication. Others use JSON and OAuth, and each has its own rules for required fields, sender details and test environments. Some carriers don’t offer the kind of rate lookup we’d want, so for those we use rate tables that the customer keeps. Trying to hide all of this behind one simple model didn’t work. What did work was a small common shape, with each carrier checking its own settings before anything is sent.
Treat each account’s credentials as its own
Credentials are encrypted when saved, and each encrypted value is tied to its own account, so it can’t be copied onto another. If the server has no encryption key, saving a credential is refused instead of being stored in the clear.
The bug that froze everything
Our worst integration bug had nothing to do with any carrier. We were creating a new HTTP client for each call. On our Java version, each live client holds a thread that it doesn’t give back, and the runtime stops at 256 of them. The 257th client meant every outgoing HTTP call in the whole application waited forever, and the request’s own timeout didn’t help.
It only showed up when enough connections ran together. Each one passed its own tests.
We’d seen a relative of this before. Creating a new storage client on every call once built up about 4,000 open sockets on a test machine, until networking failed for everything on it. Now the rule is simple: share one client, and never create one per call.
Every outgoing call gets a limit
Calls to carriers now go through one shared path that sets a total deadline and a size limit on the response. An outside service is the one part of your system you can’t fix, so a slow or strange answer from one carrier mustn’t be able to hold up the others.
If you’re building integrations
- Test many connections running together, not just each one alone.
- Assume every vendor will be slow, wrong or down at some point, and decide what happens then.
- Keep each vendor’s quirks in its own piece of code, so a change at one vendor doesn’t touch the rest.
- Budget for testing against real accounts, because no documentation is complete.