I have spent the past two years reviewing shipping integration code from teams across Europe. Some of it was clean and thoughtful. Most of it was not. And honestly, that is not because the developers were bad at their jobs. It is because carrier APIs are uniquely hostile territory that breaks your normal assumptions about how software should work.
Here are the seven mistakes I keep seeing. I am writing this partly as a guide and partly as therapy, because if I see one more hardcoded API key in a public repository I might actually lose it.
1. Hardcoding carrier credentials in the application
This sounds basic. It is basic. And yet I have seen production codebases with DHL API keys sitting in a config file committed to GitHub. One memorable case involved a company that had their DPD production credentials baked into a Docker image pushed to a public registry.
Carrier API credentials carry financial weight. Someone with your DHL Express credentials can generate shipping labels billed directly to your account. These are not feature flags. Treat them like payment credentials.
Use a secrets manager. Rotate credentials quarterly. And please - add your environment files to your ignore list before the very first commit, not after you have already pushed them. I cannot stress this enough.
2. Ignoring rate limits until they bite you
Most carrier APIs have rate limits. Sometimes the documentation mentions them. Sometimes it does not, and you discover them at two in the morning on Cyber Monday when your label generation starts failing.
I watched this exact scenario play out last year. An e-commerce platform batched five hundred orders and fired off five hundred simultaneous label requests. The carrier allowed ten requests per second. Four hundred and ninety requests failed. The error handling retried them all immediately. The carrier started returning server errors. Suddenly nobody in the entire organization could generate a single label.
The fix is not complicated. Build a queue. Even a simple in-memory queue with a rate limiter saves you from this disaster. Respect retry-after headers when they are present. Use exponential backoff when they are not. And if you are processing bulk shipments, consider using a unified shipping API that handles carrier rate limiting on its side so you do not have to think about it.
3. Polling for tracking updates instead of using webhooks
I understand the appeal of polling. It is simpler. You call the tracking endpoint every hour, you get the status, done. No webhook endpoints to set up, no signature verification, no retry handling.
But polling does not scale. If you are tracking ten thousand active shipments and polling each one every hour, that is two hundred and forty thousand API calls per day. Most carriers will throttle you long before you get there. And the data is stale - a lot can happen in sixty minutes.
Webhooks give you real-time updates. A package gets delivered, you know within seconds. A delivery attempt fails, you can notify the customer immediately instead of waiting for your next polling cycle.
Our Tracking API pushes webhook events in a normalized format across all carriers. One webhook handler for DHL, InPost, DPD, and everyone else. That alone saves weeks of integration work.
4. Treating carrier errors as unexpected exceptions
Here is a mindset shift that took me far too long to internalize. Carrier API errors are not exceptional. They are a normal part of the flow.
An address validation might fail. A postcode might not match the city. A parcel might exceed the carrier's maximum dimensions. The carrier's API might be temporarily down - and this happens more often than their status pages suggest.
I have seen code that wraps the entire shipment creation flow in a single error handler and shows the user "Something went wrong." That is not good enough. You need granular error handling that distinguishes between a validation problem the customer can fix, a transient failure you should retry, and a carrier outage that requires a fallback.
Map carrier-specific error codes to user-friendly messages. Build retry logic for transient failures. Have a fallback carrier ready when your primary is unavailable. And validate addresses before attempting to create a shipment - our Address API exists specifically for this reason.
5. Building carrier selection logic as a chain of conditionals
This starts innocently. If the destination is Poland and the customer wants a locker, use InPost. If the destination is Poland but they want door delivery, use DPD. If the destination is Germany, use DHL. Otherwise, use DHL Express.
Then someone adds weight-based logic. Then dimensional weight. Then price comparison. Then service level agreements. Six months later you are staring at a four-hundred-line function that nobody wants to touch, and it is wrong for edge cases nobody anticipated.
Carrier selection should be data-driven, not hardcoded. Build a rules engine, or better yet, use rate shopping to query multiple carriers and pick the best option based on price, transit time, and reliability. This is exactly the kind of problem where a unified shipping API shines - you define your preferences, and the routing handles the rest.
6. Skipping real carrier sandbox testing
Unit tests with mocked responses are great for your application logic. They are terrible for catching carrier integration issues.
Carrier APIs have quirks that mocks will never capture. DHL might reject a shipment because the street name exceeds thirty-five characters - their specific limit, barely documented. A carrier might change their response format slightly in what they call a non-breaking update, except it breaks your parser completely.
I remember helping a client debug an issue where their InPost integration had been silently failing for three days. Their mocked tests were all green. The actual carrier had changed a field name in their sandbox two weeks earlier and the team never caught it because they only ran mocked tests.
Use sandbox environments for integration tests. Run them on a schedule, not just in your deployment pipeline. When a carrier changes something - and they will - you want to know before your customers do.
7. Forgetting about label printing edge cases
You got the API integration working. Labels generate correctly on your development machine. Ship it, right?
Then someone in the warehouse reports that DPD labels print with a white strip on the right side because they are 100x150 millimeters and the printer expects 4x6 inches. Or InPost labels come back as A4 PDFs and the thermal printer cannot handle them. Or GLS returns a label with the barcode positioned differently and the warehouse scanner cannot read it after printing.
Label normalization is tedious, unglamorous work. But it is the difference between a shipping integration that works on your laptop and one that works in a real warehouse. Our Printing API normalizes labels across carriers to consistent formats and sizes because we have already dealt with every weird edge case so you do not have to.
The pattern underneath all of this
Most of these mistakes share a common thread. Developers underestimate how messy real-world carrier integrations are. The API call itself is the easy part. Error handling, rate limiting, format normalization, carrier selection logic, and operational resilience - that is where eighty percent of the work lives.
You can build all of this yourself. Teams do it every day. But if you would rather spend your engineering time on your actual product instead of wrestling with carrier API quirks, that is exactly why we built UniShip. One integration. All the carriers. All the edge cases handled.
Your call. But maybe bookmark this article for the day you are three weeks into a DPD integration and questioning your career choices. I have been there. It gets better.