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.

    Errors and rate limits

    View source

    Every non-2xx response from every Luigi’s Box AI endpoint has the same body.

    {
    "reason": "Invalid filter expression.",
    "exception_details": {},
    "request_id": "01JQ8H3Z9K2F7M4N6P8R0S2T4V"
    }
    FieldMeaning
    reasonA human-readable, actionable message. Show it to a developer, not to a shopper.
    exception_detailsMachine-readable context. {} when there is nothing to add.
    request_idCorrelation ID, matching the X-Request-Id response header.

    Log request_id. Luigi’s Box support uses it to find the request.

    When the problem is a specific value, exception_details.validation_errors lists every field-level problem in the request:

    {
    "reason": "Request validation failed",
    "exception_details": {
    "validation_errors": [
    {
    "loc": ["query", "size"],
    "msg": "Input should be less than or equal to 100",
    "type": "less_than_equal"
    },
    {
    "loc": ["body", "objects", 0, "@id"],
    "msg": "Field required",
    "type": "missing"
    }
    ]
    },
    "request_id": "01JQ8H3Z9K2F7M4N6P8R0S2T4V"
    }
    • loc is the path to the offending value from the request root.
    • type is a stable machine code — safe to branch on.
    • msg explains the problem in words — safe to log, not to branch on.

    A Content API batch that is well-formed but has every object rejected carries exception_details.rejected_objects instead, naming each object with a stable code and a message — unless every rejection is internal_error, which is a 503 to resend later. exception_details holds at most one of validation_errors, rejected_objects and mapping_problems, never two at once.

    A refused mapping carries exception_details.mapping_problems, one entry per rule that cannot apply, each with a stable code.

    When a request is well-formed but cannot be served as sent, exception_details.error_code names the cause instead. A code is stable and safe to branch on; reason is not. Every code is snake_case and prefixed by its domain.

    A discovery request — search, collection, recommendations — can return these:

    error_codeReturned whenWhat to do
    filter_syntaxA filter string does not parse; position, when present, says where parsing stoppedFix the filter
    filter_too_complexA filter is deeper or has more conditions than the published capsSimplify the filter
    filter_unknown_fieldA filter or sort names a field the catalog does not haveCheck the field name against the catalog’s attributes
    filter_type_mismatchA filter value’s type does not match the field’s — a number compared to textSend a value of the field’s type
    cursor_mismatchThe cursor belongs to another result set: other parameters, or another visitorDrop the cursor and request the first page
    cursor_malformedThe cursor cannot be read — truncated or edited in transitDrop the cursor and request the first page
    surface_intent_mismatchThe surface declares another intent than the endpoint serves; expected_intent and actual_intent say whichCall the endpoint the surface was made for
    touchpoint_intent_mismatchThe touchpoint does not serve the endpoint’s intent; touchpoint, intent and served_intents say whichSend one of the touchpoints the endpoint accepts

    A request that can fail in several places at once — saving a business rule — reports a code per problem instead, inside the list that names it: targeting_problems[].code for targeting, filter_problems[].code for a condition, validation_errors[].type for a field. Those codes follow the same rule, and a condition problem uses the filter codes above.

    StatusMeaningWhat to do
    207Multi-Status: a Content API batch was applied in partRead rejected_objects; resend only those objects — fixed, or unchanged when their code is internal_error
    400Malformed request — unparseable body, bad encodingFix the request; do not retry
    401Missing, expired or wrong-audience tokenGet a fresh token and retry once
    403Authenticated, but not permitted on this resourceCheck the permission and the resource you named
    404Not found — or not visible to your credentialsSee the note below
    409Conflict with the resource’s current stateRe-read the resource and retry with what you find
    422Validation failedRead validation_errors, error_code or the other keys of exception_details; do not retry unchanged — except Content API objects rejected with code internal_error
    429Rate limit exceededBack off — see below
    503Temporarily unavailableRetry unchanged after Retry-After
    RetryDo not retry
    429, after the interval it tells you400, 422 — the request itself is wrong, except Content API objects with code internal_error
    503, after Retry-After, then with exponential backoff403, 404 — permissions do not change on retry
    401, exactly once, with a new token409 without re-reading the resource first

    Add jitter to the backoff, so that a fleet of workers does not retry at the same instant.

    207 is a success status, so a client that only checks for 2xx — raise_for_status(), response.ok — treats it as one. Read the body of every 2xx from the Content API: a 207 means some objects were not applied.

    Rate-limited endpoints report your budget on every response, not only on a 429:

    HeaderMeaning
    RateLimit-LimitRequests allowed in the current window
    RateLimit-RemainingRequests left in it
    RateLimit-ResetSeconds until the window resets

    Exceeding the limit gives you 429 with the standard error body and a Retry-After header in seconds:

    HTTP/1.1 429 Too Many Requests
    Retry-After: 42
    RateLimit-Limit: 100
    RateLimit-Remaining: 0
    RateLimit-Reset: 42

    Honour Retry-After. It is an upper bound on the wait; a client that respects it does not retry into the same window.

    RateLimit-Remaining on successful responses shows a job approaching its budget before it starts receiving 429.

    If the limits do not fit your traffic, contact Luigi’s Box — they are set per account and can be raised.

    A 403 that is not about permissions is usually bot control — most often a server-side integration attempting the browser token flow, or a page origin that was never registered for its publishable key.

    Some discovery responses can carry a debug object describing ranking decisions. It is gated on a permission granted only to Luigi’s Box staff, and its contents carry no compatibility guarantee. Passing debug=true without that permission is ignored.