Authentication

Authentication

Two different credentials exist in this API and they are not interchangeable. Almost every authentication problem a partner hits is really this distinction, so it is worth reading before the rest of the page.

An API access token is what your server holds. You mint it from your client credentials at POST /oauth/token, and it is the only thing /v1 accepts.

An SDK session token is minted by POST /v1/sdk/sessions for a browser. It is short-lived and bound to a web origin. Present one to a /v1 route and you get 403 wrong_token_type — not because it expired or was malformed, but because it is a credential for a different kind of caller, and accepting it server-side would erase the origin binding that makes it safe to hand to a browser at all.

Getting a token

curl -sS -X POST https://{your-base-url}/oauth/token \
  -u "$DRIVECARS_CLIENT_ID:$DRIVECARS_CLIENT_SECRET" \
  -d grant_type=client_credentials
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "rentals:quote rentals:book bookings:read"
}

Credentials go in an HTTP Basic header (-u, which the RFC prefers) or as client_id and client_secret in the form body. Both are accepted; pick whichever your HTTP client makes easy. The request body may be application/x-www-form-urlencoded or application/json.

grant_type=client_credentials is the only grant. Anything else — including authorization_code, refresh_token and password — is 400 unsupported_grant_type. There is no refresh token: when the hour is up, you ask for another token exactly the way you asked for the first.

Send it on every /v1 call:

Authorization: Bearer <access_token>

Discovery

If your OAuth client library can configure itself from a document, point it at:

GET https://{your-base-url}/oauth/.well-known/oauth-authorization-server

It reports the issuer, the token endpoint, the one supported grant, both supported client-authentication methods, and every scope that exists.

Narrowing a token’s scope

By default a token carries every scope your credential was granted. You can ask for fewer:

curl -sS -X POST https://{your-base-url}/oauth/token \
  -u "$DRIVECARS_CLIENT_ID:$DRIVECARS_CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope="rentals:quote bookings:read"

scope is a space-separated list. It can only ever narrow what the credential already holds — asking for a scope your credential was not granted is 400 invalid_scope, never a quiet upgrade. Worth doing when one service in your estate only ever reads bookings: if that service is compromised, the token it was holding cannot book anything.

Two edge cases, so they do not surprise you: a scope parameter that is present but empty (whitespace only) is 400 invalid_scope rather than a silent fall back to “everything” — you asked for something, and zero scopes is not a sensible something. And a scope name that does not exist and a scope name you simply were not granted both return the same invalid_scope code, deliberately, so a caller cannot use the error to fingerprint which scope names are real.

Scopes

A credential is granted the scopes its integration needs, and no others.

ScopeCovers
rentals:quoteGET /coverage, GET /rentals/countries, GET /rentals/locations, GET /rentals/cars/{slug}, GET /rentals/availability
rentals:bookPOST /rentals/reservations, POST /rentals/reservations/{reference}/book, DELETE /rentals/reservations/{reference}, POST /rentals/bookings
bookings:readGET /bookings, GET /bookings/{reference}, GET /bookings/ext/{external_ref}, GET /events, GET /events/{event_id}
bookings:cancelPOST /bookings/{reference}/cancel
sdk:sessionsPOST /sdk/sessions
webhooks:manageevery /webhooks route, plus POST /deliveries/{id}/replay

webhooks:manage is deliberately separate from bookings:read and bookings:cancel. An integration that reads or cancels bookings has no reason to also be able to change where booking-lifecycle events get POSTed, and splitting the two means a leaked read-only credential cannot quietly redirect your event stream.

GET /v1/me requires no scope at all. The endpoint that reports which scopes you hold must not itself be gated on holding one.

Reading a failure

POST /oauth/token

StatuserrorWhat happened
400unsupported_grant_typegrant_type was not client_credentials.
400invalid_scopeYou asked for a scope you were not granted, one that does not exist, or an empty scope.
401invalid_clientThe credentials did not authenticate.
503temporarily_unavailableToken issuance is misconfigured on our side. Not your request; retry, and tell us if it persists.

invalid_client is deliberately uniform. “No such client” and “wrong secret” return the byte-identical response, because telling them apart would let anyone confirm which client ids exist by watching which guesses change the error. This is why a 401 here can only ever mean “check both halves” — the API is not being unhelpful, it is refusing to be a client-id oracle.

Note also that the token endpoint is rate limited per source IP, and additionally per client id when you authenticate with a Basic header. A retry loop that mints a token per request will eventually earn a 429 carrying Retry-After; caching the token for its full hour makes that unreachable in normal operation.

Any /v1 route

The checks run in this order, and each one assumes the previous passed.

Statuserror.codeWhat happenedCan you fix it?
401unauthorizedNo Authorization: Bearer header at all.Yes — send one.
401invalid_tokenThe token did not verify: malformed, expired, or signed for somewhere else.Yes — mint a fresh one. Check you are not sending a sandbox token to production.
403wrong_token_typeA real token, but not an API access token — usually an SDK session token.Yes — use a token from POST /oauth/token.
403partner_inactiveYour partner account is suspended or revoked.No. Contact your account manager.
403insufficient_scopeYour token does not hold the scope this route needs. error.details.required_scope names it.Yes — mint a credential that includes it.
403product_not_enabledYour account was never enabled for this product. error.details.required_product names it.No. Contact your account manager.
404Either the thing does not exist, or it is not yours.See below.

Two of those deserve more than a table row.

insufficient_scope and product_not_enabled look identical and are not. The first is about the credential you minted: you asked for a token without the scope, and a new credential with the right scope fixes it in minutes, from your side, today. The second is about the commercial agreement: nobody ever sold your account that product, so no token you can mint will pass. Retrying, rotating credentials, or re-reading this page will not change it — only your account manager can. They are separate codes precisely so you do not spend an afternoon debugging the second as if it were the first.

Suspension takes effect on the very next request, not when your token expires. Your partner record is re-read on every single call with no caching, so a token minted 50 minutes ago stops working the moment ops suspends the account. That is the point: an account suspended for non-payment should not keep booking for another 59 minutes.

Why ownership answers 404

Ask for a booking, a reservation or a search handle that belongs to another partner and you get 404, with the same body you would get for a reference that never existed.

A 403 would confirm the reference is real and merely not yours — which turns every read endpoint into an oracle. Someone walking references and reading only the status code could enumerate genuine DriveCars bookings without ever seeing a response body. So probing another partner’s reference is answered exactly as if it had never been created.

The cost of that lands on you when you debug. A 404 on a reference you are certain you created almost always means you are reading it with a credential belonging to a different partner account — sandbox versus production is the usual culprit — and by design, no error message will ever tell you that. GET /v1/me will: check the slug it reports is the account you expect.

Handling tokens

  • Cache the token, not the credentials round trip. One token per hour per process, not one per request.
  • Refresh slightly early. Mint a new token at around 55 minutes rather than waiting for a 401 in the middle of a booking.
  • Treat invalid_token as “mint and retry once”. Treat every 403 as terminal for that request — retrying an insufficient_scope or a product_not_enabled produces the identical answer forever.
  • Never put a client secret, or an access token, in a browser. For anything browser-side, mint an SDK session token via POST /v1/sdk/sessions from your server. That is what it exists for.