Limits and reliability

What to expect from the API under load, what a given response actually
guarantees, and how to build an integration that stays correct when something
goes wrong in the middle.

Rate limiting

Requests are limited per account and per endpoint, counted in a fixed time
window. Over the limit, you get 429.

The limit is not the same for every endpoint, and the values can change. Do not
hard-code them, and do not design a flow that only works at exactly today's
rate. Handle 429 as a normal condition: back off and retry, do not treat it as
a failure of the request.

Two practical consequences:

  • Quote once, not on every keystroke. Estimating is more expensive than it
    looks, and a checkout that re-estimates whenever the customer edits a field
    will hit the limit that a well-behaved integration never reaches. Estimate
    when the address is complete, and cache the result for the life of the basket.
  • Batch where the endpoint allows it. Creating parcels accepts several at
    once; one request with ten parcels is not ten requests.

If you have a launch or a campaign that will genuinely need more throughput,
tell us beforehand rather than discovering the limit in production.

What each response guarantees

Read the status code for what it actually promises:

EndpointSuccessWhat it means
POST /v1/parcels200The parcel is recorded. Nothing has been checked about coverage, price or fleet, and nobody is coming to collect it yet.
POST /v3/parcels/estimate200A price and timings, valid for a limited time. It is not a reservation.
POST /v1/parcels/ship202The shipment request was accepted for processing. The response carries the parcel ids and their tracking URLs, but nothing has been dispatched yet.
POST /v1/parcels/deliver/cancel202The cancellation was accepted for processing. It is not a confirmation that the parcel is cancelled.
POST /v1/parcels/delete202Same: accepted, not done.
❗️

202 is an acknowledgement, not an outcome

Shipping, cancelling and deleting all answer 202: the request was queued,
not carried out. Read the parcel back and check its state before you act on
the outcome.

For cancellation in particular, do not treat 202 as "this parcel will not be
delivered" before you tell a customer their order is cancelled, release stock,
or stop tracking it. If the state has not moved when you check, treat the
cancellation as still open and check again rather than assuming it
succeeded.

Idempotency: external_id is the key

The API has no Idempotency-Key header. external_id does that job, and it is
the reason a retry is safe rather than dangerous.

Every parcel you create should carry an external_id that is unique in your
system — your order number, or something derived from it.

It is not a convenience field:

  • Creating a parcel with an external_id you already used returns 409 with
    the parcels that already exist, instead of silently creating a second one.
    Treat that 409 as success: the parcel is there, use the ids from the
    response.
  • It is how you find a parcel again when you have lost the Cabify id — after a
    crash, a timeout, or a deploy that dropped an in-flight response.
  • It is what we ask for when you report a problem, and it is what lets you match
    our records against yours.

Without it, a lost response leaves you with no way to know whether the parcel
exists, and retrying creates duplicates that someone has to cancel by hand.

Losing a response mid-flight

Network timeouts happen. The safe sequence is always: read before you retry.

  1. A request times out, or you lose the response.
  2. Look the parcel up by its external_id before doing anything else.
  3. If it exists, carry on from its current state. If it does not, retry the
    original request.

This works because external_id is unique per account. Blindly retrying a
create is what produces the duplicate parcels we then have to cancel; blindly
assuming failure is what produces orders that ship twice.

For retries in general: only 5xx and 429 are worth retrying unchanged. A
4xx will keep failing until you change the request. Use exponential backoff
with jitter, and cap the attempts.

Is it safe to repeat a shipment request? Repeating POST /v1/parcels/ship
for parcels that were already dispatched is not idempotent, so do not retry it
blind. Read the parcels back first — by external_id if that is all you kept —
and repeat only for the ones that did not move.

Webhooks are not a ledger

Notifications are the fast path, not the record of truth. They can duplicate,
arrive out of order, and — if your endpoint is unreachable long enough — expire
without ever being delivered. There is no manual replay.

Anything that has to be correct needs a reconciliation pass that polls
GET /v1/parcels/{id} for parcels that have not reached a final state within
the time you expect. See Notifications for the full
delivery policy.

The rule of thumb: webhooks tell you quickly, polling tells you truthfully. Use
both.

Clock and time zones

Timestamps are UTC. If you send a scheduling window or compare event times
against your own records, convert explicitly rather than relying on the server's
local time — most "the event arrived before the one that caused it" reports are
a time zone applied twice.


Did this page help you?