Skip to content

Documentation

Errors & rate limits

What each status code means, which failures are worth retrying, and how a partly-successful quote is reported.

Status codes

CodeMeaningRetry?
200Success.
201Created: a label was bought.
400The body is not valid JSON.No. Fix the request.
401Missing or invalid API key.No. Check the key.
404No such shipment, or not yours.No.
422Valid JSON, but the request cannot be fulfilled.No. Fix the request.
429Rate limited.Yes, after backing off.
500Something 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.

422 Unprocessable Entity
{
  "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 customs object or no itemised contents.
  • 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.

200 OK, with one carrier down
{
  "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.

Safe to run twice
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.