> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.westiq.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.westiq.ai/_mcp/server.

# Authentication & Access

The WestIQ Platform API uses the **OAuth 2.0 client credentials** grant for
server-to-server access — there is no user/password or browser-redirect flow.
Each approved partner receives a `client_id` and `client_secret` during
onboarding (delivered through your onboarding pack, never over email or in
these docs).

> **Note**
>
> [Completed Update Delivery](/guides/completed-delivery) is **not available on
> deployed endpoints and is disabled by default**. Its prepared contract requires
> additional permission for the exact calling service identity; an ordinary API
> token is insufficient. Tenant/client ownership remains server-bound, and the
> authorized service must validate and attest browser-session cookie freshness.
> Read its [trust boundary](/guides/completed-delivery#authentication-and-ownership)
> before designing an integration.

> **Warning**
>
> Treat your `client_secret` like a password. Store it in a secrets manager,
> never in source control, and rotate it if you suspect exposure.

## 1. Get a token

Exchange your client credentials for a short-lived bearer **access token** at
the staging token endpoint:

```text
POST https://auth.staging.westiq.ai/oauth2/token
```

Authenticate the token request itself with **HTTP Basic**
(`client_id:client_secret`), and request the `westiq/search` scope in the
form body:

```bash
curl -X POST "https://auth.staging.westiq.ai/oauth2/token" \
  -u "$WESTIQ_CLIENT_ID:$WESTIQ_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=westiq/search"
```

```json
{
  "access_token": "eyJraWQiOi…",
  "expires_in": 3600,
  "token_type": "Bearer"
}
```

If your HTTP client can't send Basic auth, `client_id` and `client_secret`
may instead go in the form body alongside `grant_type` and `scope` — both
styles are accepted.

Send the token on every request as `Authorization: Bearer <token>`, and cache
it until `expires_in` (about an hour) elapses rather than requesting one per
call.

Only **access tokens** are accepted. Sending an ID token — or any token whose
`token_use` is not `access` — is rejected with `401 auth.invalid_token`.

> **Note**
>
> To use the [API Reference](/api-reference) playground, fetch a token with
> the curl above and paste the `access_token` value into the playground's
> auth field — every "Try it" request then sends it as the bearer. The token
> endpoint itself is documented in the reference too, under
> **Authentication**; its "Try it" takes your `client_id` and `client_secret`
> in the form body and needs no token.

## 2. Your credential IS your airline

Every partner credential is bound to its airline **on the WestIQ side**: the
token's `client_id` is resolved through a server-side mapping to your
`airline_id`. You never send an airline identifier — not as a header, not as
a body field, not as a token claim. There is nothing to configure and nothing
to get wrong.

This is a deliberate isolation property, not a convenience:

* A request can only ever act as the airline its credential is bound to.
  Tenancy cannot be forged, because the request carries no tenancy input.
* A credential with no airline mapping is rejected with
  `403 auth.unknown_client` — never resolved to a default tenant.
* Your searches see your airline's catalog namespace plus the shared `global`
  namespace, and nothing else.

You can observe the resolution in every search response:
`parsed.filters.airlineId` is the airline your credential resolved to.

## 3. One scope: `westiq/search`

Every `/v1` endpoint — search, events, and all of the personalization
endpoints alike — requires the single `westiq/search` scope on the access
token. There are no per-endpoint scopes. Request it in the token call (as in
the example above). A token without it is rejected with
`403 auth.insufficient_scope`.

## 4. Some endpoints also need a session locator

Authentication gets you in; it does not by itself identify a *passenger
session*. The session-scoped personalization endpoints additionally require an
`X-Session-Id` header (or, for the SSE routes, a session id in the path), and
the per-member calls take `seatId` / `memberSlot` locators. Those mechanics —
which endpoints, the exact value rules, and the `400 session.unresolved` you
get without them — are covered in **[Sessions & Member Locators](/guides/sessions)**.

## 5. Auth failures you will see

All auth failures return the standard [error envelope](/guides/errors):

| HTTP | Code                      | Cause                                                                                                         |
| ---- | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| 401  | `auth.invalid_token`      | Missing, malformed, expired, bad-signature, or wrong-issuer token — or an ID token instead of an access token |
| 403  | `auth.insufficient_scope` | Token valid, but the `westiq/search` scope is absent                                                          |
| 403  | `auth.unknown_client`     | Token valid, but the client has no airline mapping                                                            |
| 403  | `integration.not_enabled` | Token valid, but this endpoint is not enabled for your integration. See [Errors & Limits](/guides/errors)     |

> **Note**
>
> Without a token, unmatched routes also return **401**, by design (the API
> denies by default). With a valid token, an unmatched route returns an empty
> `404`. If a request 401s unexpectedly, check the path before debugging your
> token.

## Rate limits

Two independent layers, tied to different identities:

* **Per client credential** — AI-backed `/v1` endpoints enforce a per-client,
  per-endpoint-class request-rate limit. Over it you get `429 rate.limited`
  (standard error envelope) with a `Retry-After` header. Back off for that
  many seconds.
* **Per IP, at the edge** — the platform edge applies a limit of **2000
  requests per 5-minute window**. Over it, requests are blocked at the edge
  with a plain `403` — without the API's error envelope — and the block
  clears on its own once your request rate drops.

Both layers, how to tell the responses apart, and the endpoint classes are
covered in [Errors & Limits](/guides/errors#rate-limits). The edge sends the
same plain 403 for other causes too, for example unauthenticated traffic
without a `User-Agent` header; [the edge 403](/guides/errors#edge-403)
describes it. We recommend a `User-Agent` on every call.