--- title: Server-to-server tokens description: Exchange an OAuth client ID and secret for an access token, and use it to index catalog content, configure feeds, report events, or serve discovery from your backend. slug: authentication/server-to-server docKind: guide hub: luigisbox-ai --- Server-side code authenticates with the OAuth 2.1 **client credentials** grant: there is no challenge to solve and no origin to check. ## What you need | Item | Example | Notes | |---|---|---| | Client ID | `lbk_live_m2m_eu1_r3b6x2v8g9` | Identifies the integration. Not secret, but not public either. | | Client secret | `lbe_live_eu1_…` | A real secret. Server-side secret manager only. | | Audience | `https://api.eu1.luigisbox.ai/index` | The service this token is for. | Both credential values carry the environment and region they belong to. A client registered in `eu1` cannot authenticate against `us1`; if you operate in both, you hold two clients. :::caution A client secret must never reach a browser, a mobile app binary, a public repository, or a front-end build. If one leaks, ask Luigi's Box to rotate it straight away: until it is rotated, anyone holding it can keep minting tokens with your integration's full access. ::: ## Getting a token One `POST` to the auth host: ```bash curl -X POST 'https://auth.eu1.luigisbox.ai/oauth/token' \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "client_credentials", "client_id": "lbk_live_m2m_eu1_r3b6x2v8g9", "client_secret": "lbe_live_eu1_…", "audience": "https://api.eu1.luigisbox.ai/index" }' ``` Form encoding works too, if your HTTP client prefers it: ```bash curl -X POST 'https://auth.eu1.luigisbox.ai/oauth/token' \ -d grant_type=client_credentials \ -d client_id=lbk_live_m2m_eu1_r3b6x2v8g9 \ -d client_secret=lbe_live_eu1_… \ -d audience=https://api.eu1.luigisbox.ai/index ``` The response is a standard token response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIs…", "token_type": "Bearer", "expires_in": 300 } ``` Then send it on every call to the API host: ```bash curl -X POST 'https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/' \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIs…' \ -H 'Content-Type: application/json' \ -d '{"objects": [{"@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton T-Shirt"}]}' ``` There is no refresh token in this flow. When the token expires, call `/oauth/token` again. ## Caching tokens Tokens last minutes. Cache one per audience and refresh a little early. ```python import time import httpx AUTH_HOST = "https://auth.eu1.luigisbox.ai" class TokenCache: """Hands out a cached access token per audience, refreshing shortly before expiry.""" def __init__(self, client_id: str, client_secret: str, http: httpx.Client) -> None: self._client_id = client_id self._client_secret = client_secret self._http = http self._tokens: dict[str, tuple[str, float]] = {} def get(self, audience: str) -> str: token, expires_at = self._tokens.get(audience, ("", 0.0)) if time.monotonic() < expires_at: return token response = self._http.post( f"{AUTH_HOST}/oauth/token", json={ "grant_type": "client_credentials", "client_id": self._client_id, "client_secret": self._client_secret, "audience": audience, }, ) response.raise_for_status() payload = response.json() # Refresh 30s early so an in-flight request never races expiry. self._tokens[audience] = (payload["access_token"], time.monotonic() + payload["expires_in"] - 30) return payload["access_token"] ``` Also retry once on a `401`: a token that worked a moment ago and now does not has almost certainly expired. ## One token per service A token is valid for the audience you asked for and nothing else. An integration that both indexes content and reports events holds two tokens at once: ```python index_token = tokens.get("https://api.eu1.luigisbox.ai/index") events_token = tokens.get("https://api.eu1.luigisbox.ai/events") ``` Unlike [browser tokens](/authentication/browser-tokens/), the client-credentials grant takes a single `audience` per request — so one token per service, cached separately. What the token may then do inside that service is decided by the grants your client holds, each attached to a specific catalog, channel or surface. There is no scope parameter: you cannot ask for more than the client was given, and you cannot grant it more yourself. Tell Luigi's Box what the integration needs to do and they configure it. See [Permissions](/authentication/overview/#permissions). ## Serving discovery from your backend If your storefront renders search server-side, request a token for `https://api.eu1.luigisbox.ai/discovery` and call the discovery endpoints from your backend. Two things are then your responsibility, because there is no browser in the loop to supply them: - **Forward the shopper's identifier.** Generate `X-Lbx-Visitor-Id` in your own frontend and pass it through verbatim on every discovery call. Substituting a server-side value collapses every shopper into one, which breaks personalization and analytics alike. - **Decide what reaches the page.** A server-side token *can* read `private:` fields. Use [field projection](/discovery/fields/) to return only what you intend to render, so a margin or cost field never lands in your HTML. ## Rotating credentials Ask Luigi's Box to issue a second client before retiring the first, then: 1. Deploy the new client ID and secret alongside the old one. 2. Point traffic at the new client and confirm calls succeed. 3. Ask for the old client to be disabled. Tokens are short-lived, so the old client's last token expires within minutes of the client being disabled. ## Troubleshooting | Response | Likely cause | |---|---| | `400` from `/oauth/token` | A required field is missing — `client_id` and `client_secret` are both mandatory | | `401` from `/oauth/token` | Wrong secret, or a client from a different region or environment | | `401` from an API endpoint | Expired token, or a token minted for a different audience | | `403` from an API endpoint | The token is valid but not granted this permission on this resource | | `404` on an object you expect to exist | Your integration holds no grant on it at all | ## See also - [Authentication overview](/authentication/overview/) — audiences and permissions - [Backend quickstart](/start-here/backend-quickstart/) — first indexing call end to end - [Browser tokens](/authentication/browser-tokens/) — the in-browser equivalent - [Errors and rate limits](/api-basics/errors-and-rate-limits/)