Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.
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
Section titled “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.
Getting a token
Section titled “Getting a token”One POST to the auth host:
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:
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/indexThe response is a standard token response:
{ "access_token": "eyJhbGciOiJSUzI1NiIs…", "token_type": "Bearer", "expires_in": 300}Then send it on every call to the API host:
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
Section titled “Caching tokens”Tokens last minutes. Cache one per audience and refresh a little early.
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
Section titled “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:
index_token = tokens.get("https://api.eu1.luigisbox.ai/index")events_token = tokens.get("https://api.eu1.luigisbox.ai/events")Unlike 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.
Serving discovery from your backend
Section titled “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-Idin 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 to return only what you intend to render, so a margin or cost field never lands in your HTML.
Rotating credentials
Section titled “Rotating credentials”Ask Luigi’s Box to issue a second client before retiring the first, then:
- Deploy the new client ID and secret alongside the old one.
- Point traffic at the new client and confirm calls succeed.
- 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
Section titled “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
Section titled “See also”- Authentication overview — audiences and permissions
- Backend quickstart — first indexing call end to end
- Browser tokens — the in-browser equivalent
- Errors and rate limits
Was this page helpful?
Thanks.