Errors
Every /v1 failure uses one envelope, so you write your error handling once:
{
"error": {
"type": "invalid_request_error",
"code": "price_mismatch",
"message": "The booking total does not match `expected_total`. ...",
"details": {
"expected_total": 1840,
"current_total": 1920,
"currency": "MAD",
"reference": "DC-8F2K91"
}
},
"request_id": "req_01JC7X2M8P4QK9"
}Key on error.code. It is the machine-readable half and it is stable.
error.message is written for a human reading a log and may be reworded without
notice — never parse it.
error.type is invalid_request_error for anything below 500 and api_error
at 500 and above. error.details appears only where there is something
structured worth carrying, and its keys vary by code. request_id may be
null; when it is present, quote it to support and we can find the exact
request.
Schema failures use the same envelope. A malformed date or an out-of-range hour
comes back as 400 invalid_payload with details.issues carrying the field
paths — not as a differently-shaped validation blob. That matters because the
failures a new integrator causes most often are their own malformed requests,
and those are exactly the ones a split error shape would make invisible to a
handler keyed on error.code.
POST /oauth/token is the one exception
The token endpoint speaks OAuth’s own error shape, not this one:
{ "error": "invalid_client" }Flat, no error.code, no request_id. See
Authentication for its four codes and why
invalid_client is deliberately uniform across “no such client” and “wrong
secret”.
It is also the one endpoint with a documented rate limit, and its 429 uses a
third shape again:
{ "error": "rate_limited", "retryAfter": 43, "bucket": "oauth-token-ip" }with Retry-After and the standard X-RateLimit-* headers alongside. Honour
Retry-After. Caching your token for its full hour makes this unreachable in
normal operation.
Status codes
| Status | Means | Retry? |
|---|---|---|
| 400 | The request was malformed, or a stated precondition was missing. | Only after fixing it. |
| 401 | No token, or one that did not verify. | Mint a fresh token, retry once. |
| 403 | Authenticated, but not permitted. | No — see below. |
| 404 | It does not exist, or it is not yours. | No. |
| 409 | Well-formed, but conflicts with current state. | Only per the code’s own rule. |
| 422 | Every field is well-formed; the combination is not. | Only after changing the combination. |
| 5xx | Our fault. | Yes, with backoff — except where noted. |
The 400/422 split is deliberate. A 400 means the request was not even
well-formed — a bad date, an hour of 47. A 422 means every individual field
was fine but the request as a whole cannot be processed: a journey with no
airport end, a party that will not fit the class you chose. Retrying a 422
unchanged will never work, and the distinction tells you that without a support
ticket.
Auth and permission
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | No Authorization: Bearer header. |
invalid_token | 401 | Malformed, expired, or signed for a different environment. |
wrong_token_type | 403 | A real token, but not an API access token — usually an SDK session token. |
partner_inactive | 403 | Your account is suspended or revoked. |
insufficient_scope | 403 | Your token lacks the scope. details.required_scope names it. |
first_party_partner | 403 | This partner row is not permitted to use the partner API. |
product_not_enabled | 403 | Your account was never enabled for that product. |
Two of these look the same and need opposite responses.
insufficient_scope is yours to fix. The credential was issued without that
scope. Mint one that has it and you are unblocked in minutes, today, without
talking to anyone.
product_not_enabled is not. It means the account was never sold that
product — a commercial state, not a bug. details.required_product names which
one, because an integrator running a flow of several calls otherwise cannot tell
which one was refused. Retrying will not help, and neither will a new
credential. Contact your account manager.
It is a 403 rather than a 404 on purpose: the route genuinely exists and you
are genuinely authenticated, and a 404 would send you hunting for a typo in a
URL that is perfectly correct.
404 on something you know exists
Ownership checks answer 404, never 403. A 403 would confirm the reference
is real and merely not yours, which would let anyone enumerate genuine DriveCars
bookings by walking references and reading status codes alone.
So probing another partner’s booking, reservation or search handle is answered
exactly as if it had never been created. When you get a 404 on a reference you
are sure you made, the first thing to check is
GET /v1/me — the slug it
reports tells you which account you are actually calling as. Sandbox versus
production is the usual answer.
Rentals
| Code | Status | Meaning |
|---|---|---|
idempotency_key_required | 400 | No Idempotency-Key header on a creating POST. |
unsupported_currency | 400 | details.supported lists what is accepted. |
invalid_cursor | 400 | The cursor was not one this API issued. |
not_found | 404 | No such car, reservation or booking — or not yours. |
price_mismatch | 409 | The priced total disagrees with expected_total by more than a cent. Nothing was charged. |
search_expired | 409 | The search result you booked from is no longer valid. Search again. |
delivery_not_available | 409 | That vehicle cannot be delivered; book it as a counter pickup. |
unavailable | 409 | The car is not bookable for those dates. |
idempotency_key_in_progress | 409 | A request with this key is still running. |
idempotency_key_reused | 409 | This key was already used with a different body. |
not_cancellable | 409 | details.status names the state. A completed booking cannot be cancelled. |
Recovering from price_mismatch needs a new Idempotency-Key alongside the
corrected expected_total. The old key is now permanently associated with that
409, and replaying it returns the same 409 forever.
Retrying safely
- Retry
5xxwith exponential backoff, and429on the token endpoint after itsRetry-After. Everything else is terminal for that request. - Retry a
401 invalid_tokenexactly once, after minting a fresh token. Twice means something else is wrong. - Never retry a
403. Every one of them returns the identical answer forever. - Reuse the same
Idempotency-Keywhen retrying a network timeout, so you learn the original outcome instead of creating a second booking. - Use a new
Idempotency-Keyafter aprice_mismatch, because that key is now bound to the refusal. - A
5xxon a creating POST is genuinely ambiguous. Do not blind-retry with a new key. Reuse the original key to learn what happened, or look the booking up by yourexternal_refviaGET /v1/bookings/ext/{external_ref}.
See Idempotency for the full replay contract.