Documentation
Errors & rate limits
What each status code means, which failures are worth retrying, and how a partly-successful quote is reported.
Status codes
| Code | Meaning | Retry? |
|---|---|---|
200 | Success. | – |
201 | Created: a label was bought. | – |
400 | The body is not valid JSON. | No. Fix the request. |
401 | Missing or invalid API key. | No. Check the key. |
404 | No such shipment, or not yours. | No. |
422 | Valid JSON, but the request cannot be fulfilled. | No. Fix the request. |
429 | Rate limited. | Yes, after backing off. |
500 | Something failed on our side. | Yes, with backoff. |
Validation errors
Validation happens at the edge, before anything reaches a carrier, and the message names the field at fault.
{
"error": "parcels[0]: length_mm, width_mm and height_mm must all be greater than zero"
}
Common causes:
- A parcel with a zero or missing weight or dimension.
- More than 50 parcels in one request.
- An international consignment with no
customsobject or no itemisedcontents. - An option, lane or pickup-point type the chosen carrier does not support.
- No couriers enabled on the account. Configure one in Shipstack Manager.
Partial failures are not errors
A quote asks several carriers at once, and one of them failing does not make
the answer useless. A courier that errors, times out or rate-limits is
recorded in failures while the rates that did arrive are still
returned with a 200.
{
"rates": [
{ "courier": "dpd", "service_code": "1^16", "service_name": "DPD Next Day",
"total": { "amount": 749, "currency": "GBP" }, "estimated_days": 1 }
],
"failures": [
{ "courier": "ups", "reason": "upstream timeout" }
]
}
A quote fails outright only when the whole operation is meaningless: the account has no couriers enabled, or every carrier failed.
Rate limits
Every /v1 route is rate limited per account, with a burst
allowance for short spikes. Exceeding it returns 429. Back off
exponentially and retry. Do not retry immediately in a tight loop.
Idempotency
POST /v1/shipments honours an Idempotency-Key
header. Retrying with the same key returns the shipment created the first
time rather than buying a second label.
Use a key derived from the thing you are shipping (an order ID plus an attempt number works well) and reuse it for every retry of that same purchase. This is exactly what protects you when a request times out and you do not know whether it succeeded.
curl -X POST https://api.shipstack.example/v1/shipments \
-H "Authorization: Bearer $SHIPSTACK_API_KEY" \
-H "Idempotency-Key: order-10432-attempt-1" \
-H "Content-Type: application/json" \
-d @shipment.json
Health checks
/health and /ready are unauthenticated
operational probes. They are for monitoring, not for checking whether your
key works. Use GET /v1/couriers for that.