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.
Every non-2xx response from every Luigi’s Box AI endpoint has the same body.
The error body
Section titled “The error body”{ "reason": "Invalid filter expression.", "exception_details": {}, "request_id": "01JQ8H3Z9K2F7M4N6P8R0S2T4V"}| Field | Meaning |
|---|---|
reason | A human-readable, actionable message. Show it to a developer, not to a shopper. |
exception_details | Machine-readable context. {} when there is nothing to add. |
request_id | Correlation ID, matching the X-Request-Id response header. |
Log request_id. Luigi’s Box support uses it to find the request.
Validation failures
Section titled “Validation failures”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"}locis the path to the offending value from the request root.typeis a stable machine code — safe to branch on.msgexplains 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.
Error codes
Section titled “Error codes”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_code | Returned when | What to do |
|---|---|---|
filter_syntax | A filter string does not parse; position, when present, says where parsing stopped | Fix the filter |
filter_too_complex | A filter is deeper or has more conditions than the published caps | Simplify the filter |
filter_unknown_field | A filter or sort names a field the catalog does not have | Check the field name against the catalog’s attributes |
filter_type_mismatch | A filter value’s type does not match the field’s — a number compared to text | Send a value of the field’s type |
cursor_mismatch | The cursor belongs to another result set: other parameters, or another visitor | Drop the cursor and request the first page |
cursor_malformed | The cursor cannot be read — truncated or edited in transit | Drop the cursor and request the first page |
surface_intent_mismatch | The surface declares another intent than the endpoint serves; expected_intent and actual_intent say which | Call the endpoint the surface was made for |
touchpoint_intent_mismatch | The touchpoint does not serve the endpoint’s intent; touchpoint, intent and served_intents say which | Send 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.
Status codes
Section titled “Status codes”| Status | Meaning | What to do |
|---|---|---|
207 | Multi-Status: a Content API batch was applied in part | Read rejected_objects; resend only those objects — fixed, or unchanged when their code is internal_error |
400 | Malformed request — unparseable body, bad encoding | Fix the request; do not retry |
401 | Missing, expired or wrong-audience token | Get a fresh token and retry once |
403 | Authenticated, but not permitted on this resource | Check the permission and the resource you named |
404 | Not found — or not visible to your credentials | See the note below |
409 | Conflict with the resource’s current state | Re-read the resource and retry with what you find |
422 | Validation failed | Read validation_errors, error_code or the other keys of exception_details; do not retry unchanged — except Content API objects rejected with code internal_error |
429 | Rate limit exceeded | Back off — see below |
503 | Temporarily unavailable | Retry unchanged after Retry-After |
Which failures are worth retrying
Section titled “Which failures are worth retrying”| Retry | Do not retry |
|---|---|
429, after the interval it tells you | 400, 422 — the request itself is wrong, except Content API objects with code internal_error |
503, after Retry-After, then with exponential backoff | 403, 404 — permissions do not change on retry |
401, exactly once, with a new token | 409 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 limits
Section titled “Rate limits”Rate-limited endpoints report your budget on every response, not only on a 429:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the current window |
RateLimit-Remaining | Requests left in it |
RateLimit-Reset | Seconds 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 RequestsRetry-After: 42RateLimit-Limit: 100RateLimit-Remaining: 0RateLimit-Reset: 42Honour 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.
Refused traffic
Section titled “Refused traffic”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.
Debug output
Section titled “Debug output”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.
See also
Section titled “See also”- Requests and responses
- Authentication overview — audiences and permissions
- Feed troubleshooting — ingestion-specific problems
Was this page helpful?
Thanks.