What a good shipping API looks like – technical perspective

What a good shipping API looks like – technical perspective

A shipping API is what lets your system and a carrier's system communicate – you send a shipment's details, and you get back a label, a tracking number, or an error, all through code. Whether that exchange is any good comes down to two separate questions: does it give the business enough to trust the carrier before committing, and does it behave predictably once someone is actually writing code against it. We covered the first question in part one of this series: What a good shipping API looks like: business perspective. This is the second.

Same request, sent twice – one shipment or two?

A request to create a shipment can time out without telling you whether the carrier actually processed it. The label may already exist on their end, or it may not; the timeout itself carries no information either way. Retrying blindly risks a second shipment for the same parcel. Not retrying risks having no shipment at all.

The fix is idempotency: repeating the same logical operation shouldn't create another shipment. The response doesn't need to be byte-for-byte identical each time – what matters is that the operation only takes effect once.

Carriers handle this in a couple of different ways. The cleanest is a client-generated idempotency key that the carrier stores against the operation and checks on every retry. A workable alternative is a client-generated shipment reference, unique on your side, that you can later look up to confirm whether the shipment was actually created before trying again.

A status lookup only helps if there's something stable to look up in the first place. If the carrier is the one generating the shipment identifier, and the response containing it is what got lost, a status endpoint keyed on that identifier doesn't solve anything – you're back to not knowing what to search for.

Without one of these mechanisms, retry logic has to be guessed. A wrong guess under load produces two shipment records for one physical parcel – potentially two labels, two tracking numbers, and depending on the carrier, two chargeable shipments.

Carrier integrations done right
Contact us

Shipment-level vs. parcel-level properties

This becomes relevant the moment an API supports more than one parcel per shipment. A shipment and a parcel aren't the same resource. A shipment (some carriers call it a consignment) can contain several physical parcels, and a data model needs to make explicit which properties belong to the shipment as a whole and which belong to each parcel individually.

Sender, receiver, and the selected service usually apply at the shipment level. Weight, dimensions, and a package reference belong to the parcel. Tracking sits in between: a carrier might return one consignment number plus separate tracking numbers and labels per parcel. Customs adds another layer on top of that, with declaration-level data attached to the shipment while individual commodity lines describe what's inside each parcel.

None of this is something an integrator should have to infer from field naming. If weight shows up at shipment level on one endpoint and parcel level on another, or the documentation never says whether cancelling a shipment cancels every parcel in it, that ambiguity eventually turns into a production incident.

It's also worth getting right even when the first version of an integration only handles single-parcel shipments. Multi-parcel support tends to arrive later, and a model that quietly treats "shipment" and "parcel" as interchangeable becomes expensive to untangle once a single shipment can contain five independently tracked packages.

An identifier isn't forever

The tracking number a carrier hands back at creation isn't necessarily the only identifier a shipment will carry through its journey. A shipment can move between networks – handed off to a local delivery partner in the destination country, for example – and pick up a second, last-mile tracking ID along the way.

That doesn't always mean the original identifier stops working. Some carriers keep publishing the full journey under their own number; others expose the downstream carrier and its identifier separately. Either way, the data model can't safely assume a shipment has exactly one tracking identifier for its whole lifecycle.

If a system only stores the original identifier, it loses the ability to reconcile events coming directly from another network, or to explain to a support agent why two systems refer to the same parcel by two different numbers. A good API returns multiple identifiers together with enough context to make sense of them: which carrier issued each one, what it identifies, and whether one supersedes the other or just supplements it.

Identifier scope matters here too. A shipment ID, a parcel ID, a tracking number, a customer reference, and an API request ID are five different things. An API that lets those blur into each other causes problems long before the first parcel ever changes hands

Sync request, async reality

Creating a shipment can look like a single request-response exchange, but the actual work behind it – validating an address, checking service availability, allocating an identifier, talking to another system inside the carrier's network – doesn't always finish inside that one call.

Some carriers keep the connection open until processing completes. Others accept the request and finish the work afterwards, then report the outcome through a webhook or a status endpoint you poll. Neither approach is wrong as long as the contract makes the distinction explicit.

If processing has genuinely finished, the API can return the created resource with its final identifiers. If it's only been accepted, the response should say so clearly – for an HTTP API, a 202 Accepted is the typical fit – and hand back a stable reference you can use to retrieve the eventual result. A "success" response that actually means "accepted, we'll confirm shortly" is fine, as long as it says that. The same response described as a final confirmation, when it's really the start of a longer process, is where integrations quietly break: your system assumes the shipment exists in full while the carrier still considers it pending.

The same logic applies to labels. If shipment creation succeeds but label generation happens asynchronously, that state needs to be represented in the API. Returning an empty label field and leaving the client to figure out that it should try again later is a serious gap.

An error should tell you what to do next

A 400 Bad Request on its own isn't enough for an integration to act on. Neither is a 500 Internal Server Error. When a shipment request fails, the client needs to know at least three things: what failed, where it failed, and whether retrying the same request has any reasonable chance of succeeding.

An invalid postcode, an expired access token, a temporary downstream timeout, and an unavailable carrier service can all produce a failed request, but each one calls for completely different handling. One needs the customer to fix something. One needs re-authentication. One might be worth retrying. One means picking a different service entirely.

A useful error contract has stable, machine-readable codes alongside the human-readable message, identifies the offending field on validation errors where possible, and makes temporary failures distinguishable from permanent ones. If the carrier wants the client to retry later, the response should carry enough information to do that safely. Error messages are for the person debugging the integration. Error codes are for the code that has to react automatically. A production-grade API needs both.

Staying authenticated, staying within limits

A single test call during integration tells you very little about what a long-running production integration actually has to deal with. A token can expire mid-batch. A rate limit that's invisible at low volume becomes a hard stop the moment you're sending thousands of requests an hour.

Authentication documentation needs to describe the lifecycle, not just the login step: how long an access token lives, whether it can be refreshed, whether multiple tokens can coexist, what happens when credentials get rotated, whether access is scoped per customer, per organisation, or per carrier account. A code sample showing how to fetch the first token answers none of that. It also helps when the API distinguishes authentication failure from authorisation failure – "we don't recognise these credentials" and "these credentials are valid but can't access this shipping account" are different problems operationally, and collapsing them into one generic error just pushes the diagnosis work onto whoever's debugging it.

Rate limiting has the same shape of problem. A useful limit is measurable and scoped: is it per API key, per account, per endpoint, is there also a daily quota, are short bursts tolerated, and what actually happens once the limit is exceeded. For an HTTP API, a 429 Too Many Requests paired with a Retry-After header gives the client something concrete to act on. Clients should then back off rather than retrying immediately in a tight loop, ideally with some jitter so a fleet of workers doesn't all retry at the same instant. Without documented limits and retry behaviour, scaling an integration turns into experimentation against a production system that wasn't built for that.

Wrapping up

A business read tells you whether a carrier's API covers what the operation actually needs. A technical read tells you whether it's safe to build against. Neither is enough on its own.

Shipping makes the failures unusually tangible. A duplicate shipment isn't only a duplicate row in a database, it can become a second label, a second charge, and a second parcel movement that someone in a warehouse has to physically stop before it goes out the door. An ambiguous identifier can turn into a customer being told their parcel is lost when it's actually moving under a different carrier's number.

A technically good shipping API is the one whose behaviour stays unambiguous when the network fails, traffic increases, a parcel changes hands, and someone has to work out what happened three days later.

Related articles

Article cover image for What a good shipping API looks like – business perspective
Logistics
eCommerce
28/07/26

What a good shipping API looks like – business perspective

Reading carrier documentation properly is a skill of its own. Here's what to look for.

Article cover image for What shipping labels don't tell you
Logistics
eCommerce
27/04/26

What shipping labels don't tell you

Label creation vs. label correctness in logistics

Article cover image for ‘Last-yard’ problem – the software challenge of the ‘last yard’
Logistics
Technology
27/02/26

‘Last-yard’ problem – the software challenge of the ‘last yard’

What happens after “delivered”? Explore the overlooked steps in logistics.

<Our latest articles>

Stay informed with our insightful blog posts

View all posts
Article cover image for What a good shipping API looks like – business perspective
Logistics
eCommerce
28/07/26

What a good shipping API looks like – business perspective

Reading carrier documentation properly is a skill of its own. Here's what to look for.

Article cover image for Your EV is now a software platform
eMobility
30/06/26

Your EV is now a software platform

What software-defined vehicles (SDVs) mean for drivers and developers

Article cover image for How to assess a developer at the screening stage
Outsourcing
31/05/26

How to assess a developer at the screening stage

Learn how non-technical recruiters can better assess developers.

Integrating with a carrier?
Start here

Looking for the IT partner recognised for excellence?

We’ve earned industry-leading awards for delivering top-notch solutions across multiple sectors.

Let’s start your project
Forbes 2025 badgeForbes 2024 badgeClutch badgeEMEA 500 badge