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:
| Endpoint | Success | What it means |
|---|---|---|
POST /v1/parcels | 200 | The parcel is recorded. Nothing has been checked about coverage, price or fleet, and nobody is coming to collect it yet. |
POST /v3/parcels/estimate | 200 | A price and timings, valid for a limited time. It is not a reservation. |
POST /v1/parcels/ship | 202 | The 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/cancel | 202 | The cancellation was accepted for processing. It is not a confirmation that the parcel is cancelled. |
POST /v1/parcels/delete | 202 | Same: accepted, not done. |
202is an acknowledgement, not an outcomeShipping, 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
202as "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
external_id is the keyThe 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_idyou already used returns409with
the parcels that already exist, instead of silently creating a second one.
Treat that409as 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.
- A request times out, or you lose the response.
- Look the parcel up by its
external_idbefore doing anything else. - 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.
Updated 2 days ago
