Skip to content

    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-to-server tokens

    View source

    Server-side code authenticates with the OAuth 2.1 client credentials grant: there is no challenge to solve and no origin to check.

    ItemExampleNotes
    Client IDlbk_live_m2m_eu1_r3b6x2v8g9Identifies the integration. Not secret, but not public either.
    Client secretlbe_live_eu1_…A real secret. Server-side secret manager only.
    Audiencehttps://api.eu1.luigisbox.ai/indexThe 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.

    One POST to the auth host:

    Terminal window
    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:

    Terminal window
    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:

    {
    "access_token": "eyJhbGciOiJSUzI1NiIs…",
    "token_type": "Bearer",
    "expires_in": 300
    }

    Then send it on every call to the API host:

    Terminal window
    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.

    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.

    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.

    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 to return only what you intend to render, so a margin or cost field never lands in your HTML.

    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.

    ResponseLikely cause
    400 from /oauth/tokenA required field is missing — client_id and client_secret are both mandatory
    401 from /oauth/tokenWrong secret, or a client from a different region or environment
    401 from an API endpointExpired token, or a token minted for a different audience
    403 from an API endpointThe token is valid but not granted this permission on this resource
    404 on an object you expect to existYour integration holds no grant on it at all