GuidesErrors

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

StatusMeansRetry?
400The request was malformed, or a stated precondition was missing.Only after fixing it.
401No token, or one that did not verify.Mint a fresh token, retry once.
403Authenticated, but not permitted.No — see below.
404It does not exist, or it is not yours.No.
409Well-formed, but conflicts with current state.Only per the code’s own rule.
422Every field is well-formed; the combination is not.Only after changing the combination.
5xxOur 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

CodeStatusMeaning
unauthorized401No Authorization: Bearer header.
invalid_token401Malformed, expired, or signed for a different environment.
wrong_token_type403A real token, but not an API access token — usually an SDK session token.
partner_inactive403Your account is suspended or revoked.
insufficient_scope403Your token lacks the scope. details.required_scope names it.
first_party_partner403This partner row is not permitted to use the partner API.
product_not_enabled403Your 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

CodeStatusMeaning
idempotency_key_required400No Idempotency-Key header on a creating POST.
unsupported_currency400details.supported lists what is accepted.
invalid_cursor400The cursor was not one this API issued.
not_found404No such car, reservation or booking — or not yours.
price_mismatch409The priced total disagrees with expected_total by more than a cent. Nothing was charged.
search_expired409The search result you booked from is no longer valid. Search again.
delivery_not_available409That vehicle cannot be delivered; book it as a counter pickup.
unavailable409The car is not bookable for those dates.
idempotency_key_in_progress409A request with this key is still running.
idempotency_key_reused409This key was already used with a different body.
not_cancellable409details.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 5xx with exponential backoff, and 429 on the token endpoint after its Retry-After. Everything else is terminal for that request.
  • Retry a 401 invalid_token exactly 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-Key when retrying a network timeout, so you learn the original outcome instead of creating a second booking.
  • Use a new Idempotency-Key after a price_mismatch, because that key is now bound to the refusal.
  • A 5xx on 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 your external_ref via GET /v1/bookings/ext/{external_ref}.

See Idempotency for the full replay contract.