The recommended mechanism is OAuth 2.0 with the client_credentials grant.
You exchange a client_id + client_secret for a short-lived
bearer token and send that token on every API call. HTTP Basic and API-key
credentials are also offered for callers that cannot run a token exchange.
OAuth 2.0 client-credentials flow
1. Request a token
Send a form-encoded POST to the token endpoint, on the same host and
/api/v1 prefix as every other call:
curl -s -X POST \
https://ari.console.adrasis.com/api/v1/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET'
Request fields (all form-encoded, application/x-www-form-urlencoded):
| Field | Required | Notes |
|---|---|---|
grant_type |
yes | Must be client_credentials. Any other value is rejected with unsupported_grant_type. |
client_id |
yes | Issued at credential creation. |
client_secret |
yes | Issued at credential creation; shown once. |
scope |
no | Space-delimited subset of your granted scopes to down-scope this token. Omit to receive your full granted set. |
Your client library may instead send client_id and client_secret as HTTP Basic
credentials, as RFC 6749 section 2.3.1 describes: Authorization: Basic followed by the
base64 of client_id:client_secret, each form-encoded first. The body then carries only
grant_type and, if you want it, scope. Use one of the two ways on a request, not both:
a request with both answers invalid_request.
curl -s -X POST \
https://ari.console.adrasis.com/api/v1/oauth/token \
-u 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
-d 'grant_type=client_credentials'
2. Read the response
A successful exchange returns 200 OK:
{
"access_token": "<ACCESS_TOKEN>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "distribution:read distribution:booking",
"issued_at": 1748851200
}
| Field | Meaning |
|---|---|
access_token |
The signed bearer token. Treat it as opaque. |
token_type |
Always Bearer. |
expires_in |
Token lifetime in seconds (3600 = one hour). |
scope |
The space-delimited scopes actually granted to this token. |
issued_at |
Epoch seconds at which the token was issued. |
A failed exchange returns a standard OAuth error envelope:
{
"error": "invalid_client",
"error_description": "Client authentication failed.",
"requested_scope": null
}
error |
HTTP | Cause |
|---|---|---|
invalid_request |
400 | A required form field (grant_type, client_id, client_secret) was missing, the body was not form-encoded, or the client credentials came both in the Authorization header and in the body. error_description names the missing fields. |
unsupported_grant_type |
400 | grant_type was not client_credentials. |
invalid_scope |
400 | Requested scope is not a subset of your granted scopes. requested_scope, present on every token error and null on the others, is here an array holding what you asked for, so you can diff it against your granted set. |
invalid_client |
401 | Unknown client_id, wrong secret, a disabled/revoked credential, or a Basic header that cannot be read. All map to the same status, the same error, and the same error_description, so the body cannot be used to tell them apart. |
3. Send the bearer token
Attach the token to the Authorization header on every Distribution call:
curl -s \
https://ari.console.adrasis.com/api/v1/properties \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
The token carries your partner-account identity and granted scopes. The service verifies both the token and its live credential/account binding on every request. There is no session or tenant header to send.
Partner account and profile selection
A credential always authenticates one partner account. Credentials are owned by the account, not by an individual profile, so you do not need a different credential for each catalogue audience.
Profile-scoped REST operations accept this optional header:
X-Adrasis-Partner-Profile-Id: <16-character-profile-public-id>
Omit the header on the first request to select the account's stable Default profile. Send it to select another profile owned by the same account. Every successful profile-scoped response returns the exact identity used:
X-Adrasis-Resolved-Partner-Profile-Id: <16-character-profile-public-id>
Persist opaque cursors, selection proofs, and prebook tokens unchanged. Each
one seals the profile that created it, so a continuation can restore that
profile without the header. If you repeat the header, it must match the sealed
identity. Unknown or cross-account profiles return 404; inactive explicit
profiles return 403; a temporarily unavailable profile resolver returns
retryable 503.
GET /bookings is the reporting exception. It is account-wide and accepts
profile_public_id as an optional query filter for the immutable originating
profile, including a profile disabled after booking. It does not use the
profile-selector header.
4. Handle expiry
Tokens are valid for expires_in seconds (3600). No refresh token is issued
for the client-credentials grant. When a token expires, request a new one from
the token endpoint with the same client_id / client_secret.
A missing, malformed, expired, or signature-invalid token returns 401 with a
WWW-Authenticate: Bearer challenge header and one of these codes. The same
three codes cover the Basic and API-key mechanisms below, so a client that
handles them once handles every mechanism:
{
"error": {
"code": "CREDENTIAL_UNAUTHORIZED",
"message": "Credential is missing, invalid or revoked",
"trace_id": "01J8ZC4M2P6QK5R7T9VXA3B1DN",
"retryable": false
}
}
code |
Meaning |
|---|---|
CREDENTIAL_MISSING |
No Authorization header was sent. |
CREDENTIAL_MALFORMED |
The header was present but its scheme or shape was not recognised. |
CREDENTIAL_UNAUTHORIZED |
The credential was well-formed but rejected because it was unknown, revoked, disabled, expired, or signature-invalid. |
INVALID_TOKEN |
Not a credential failure, and not a 401. The book endpoints answer 400 INVALID_TOKEN when your credential is accepted but the request body carries a prebook_token that is malformed, tampered with, or minted for a different caller. Refreshing the access token does not help; prebook again. |
Every Distribution endpoint uses UPPER_SNAKE codes inside the standard error
envelope described in Errors. The token exchange in step 2 is the
one exception: it returns the OAuth error envelope above, as its own
specification requires. Branch on code; the message is human-readable and
may change between releases.
CREDENTIAL_UNAUTHORIZED is deliberately one code rather than several. Which
verification step failed is not disclosed, so an attacker cannot use the error
body to distinguish "this client exists but the secret is wrong" from "this
client does not exist". Treat all three as "obtain a fresh credential or token
and retry"; none of them is worth an automatic retry with the same input.
A practical strategy is to cache one token in memory and refresh it slightly
before expires_in elapses (for example, at ~90% of the window) rather than
requesting a token per call.
Scopes
Two scopes gate the surface. A token must carry the scope an endpoint requires,
or the request returns 403 with code: INSUFFICIENT_SCOPE.
| Scope | Gates |
|---|---|
distribution:read |
GET /countries, GET /reference/categories, GET /reference/attributes, GET /reference/place-types, GET /reference/currencies, GET /places, GET /places/{place_id}, GET /properties, GET /properties/{public_id}, the transfer, tour and flight catalogue reads (GET /service-zones, GET /transfer-vehicle-types, GET /tours, GET /flights and their /{public_id} details), GET /catalog/changes, POST /properties/availability, POST /places/availability, POST /properties/{property_public_id}/offers/search, POST /availability/check |
distribution:booking |
POST /prebook, POST /book, GET /bookings, GET /bookings/{public_id}, POST /bookings/{public_id}/cancellation-quote, POST /bookings/{public_id}/cancel, every /webhook-endpoints* operation, and every product-vertical endpoint (/transfer/*, /tour/*, /flight/*, /package/*, /trip), including the vertical search steps |
A read-only integration only needs distribution:read. That covers both the
bulk and single-property accommodation searches, availability check, and the
content feed. The vertical
search endpoints (/transfer/search, /tour/search,
/flight/search, /package/search) sit under distribution:booking. A full
booking integration needs both scopes. Use the scope request parameter to
down-scope a token when a worker only needs a subset (for example, mint a
read-only token for a search service even though the credential is granted
both).
Alternative mechanisms
The seller can also issue, from Connectivity > Sales channels > API
partners in its Console, HTTP Basic and
API-key credentials for callers that cannot run a token exchange. Unlike
OAuth, these do not use the token endpoint. The credential material is sent
directly on each call (Basic on the Authorization: Basic header; an API key
with the adr_ prefix as Authorization: Bearer adr_...). They are gated by the same distribution:read and
distribution:booking scopes. OAuth 2.0 is recommended for new integrations
because tokens are short-lived and the long-term secret never travels on data
requests.
Next steps
- Getting started: Make the first authenticated call.
- The booking flow: Review the endpoints these scopes gate.
- Booking webhooks: Configure signed push notifications.
- Errors: See the full status-code reference.