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.
Luigi’s Box already knows what it showed a shopper — the results a surface returned are recorded when the discovery request is served. What it cannot see is what happened next.
There are three things to report:
| Report | As |
|---|---|
| A shopper clicked a result | interaction, interaction_type: "click" |
| A shopper added something to the cart | interaction, interaction_type: "add_to_cart" |
| An order completed | transaction |
The endpoint
Section titled “The endpoint”One endpoint, one event per request:
POST /events/v1/eventsA click on a search result:
curl -X POST 'https://api.eu1.luigisbox.ai/events/v1/events' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -H 'X-Lbx-Visitor-Id: 8f14e45f-ea0f-4b5c-9a1d-2b3c4d5e6f70' \ -d '{ "type": "interaction", "channel_id": "lbn_4hj9tv", "reference": "product/@id:product/sku-1001", "interaction_type": "click", "listing_ab_test": false, "consent_granted": true }'A successful call answers:
HTTP/1.1 202 Accepted{ "status": "ok" }Three requirements on every call:
| Requirement | Why |
|---|---|
Content-Type: application/json | Anything else is a 400 |
X-Lbx-Visitor-Id header | Identifies the browser. Missing it is a 400. |
A token for the …/events audience | Granted to send events for this channel |
202 means validated and queued. Processing is asynchronous.
Browser integrations get their token from a publishable key. Include
https://api.<region>.luigisbox.ai/events in the token request’s audience — it accepts an
array, so the same token can also cover discovery. See
Browser tokens.
The shared envelope
Section titled “The shared envelope”Both event types carry the same base fields.
| Field | Required | Notes |
|---|---|---|
type | Yes | interaction or transaction |
channel_id | Yes | lbn_… — the storefront this happened on |
event_id | No | Your own idempotency key. Generated if omitted. |
event_timestamp | No | Unix seconds. Set to receipt time if omitted. |
catalog_id | No | lbc_… |
user_id | No | Your opaque key for a signed-in shopper |
consent_granted | No | Defaults to false. See Consent. |
currency | No | ISO currency code |
platform | No | web, ios, android, … — your own vocabulary |
referrer | No | Referring URL |
context | No | Flat map of string keys to string values, for your own segmentation |
ab_test / variant | No | Your own external A/B test |
app_version | No | Your build identifier |
Booleans are strict: true, false, or the strings "true" / "false". 1 and 0 are
rejected.
Set event_id yourself if your client retries. It is the value that lets a retried delivery
be recognized as the same event.
Object references
Section titled “Object references”Anywhere an event names a catalog object, it uses a reference:
<type>/<field>:<value>product/@id:product/sku-1001product/sku:ABC-123product/ean:5901234123457Any field the object carries can be the field, so you can send the identifier the page already has. Object references covers that and what happens when a reference resolves to nothing.
Clicks
Section titled “Clicks”{ "type": "interaction", "channel_id": "lbn_4hj9tv", "reference": "product/@id:product/sku-1001", "interaction_type": "click", "listing_ab_test": false, "consent_granted": true}| Field | Required | Notes |
|---|---|---|
reference | Yes | The object clicked |
interaction_type | Yes | click |
listing_ab_test | Yes | Whether the click came from a result set under an A/B test |
url | No | Where it happened |
On a results page, reference is the @id straight from the response — see
Attribution.
Cart additions
Section titled “Cart additions”Same event type, a different interaction_type, plus quantity and value:
{ "type": "interaction", "channel_id": "lbn_4hj9tv", "reference": "product/sku:ABC-123", "interaction_type": "add_to_cart", "listing_ab_test": false, "consent_granted": true, "count": 2, "price": 29.9}| Field | Required | Notes |
|---|---|---|
count | No | Quantity added |
price | No | Unit price. Must be ≥ 0. |
A SKU is a complete reference, so a cart page does not need the @id.
Agree the exact interaction_type string with your Luigi’s Box contact and then keep it
stable — reporting groups on the exact value, so add_to_cart and addToCart are reported
separately.
Purchases
Section titled “Purchases”{ "type": "transaction", "channel_id": "lbn_4hj9tv", "currency": "EUR", "listing_ab_test": false, "consent_granted": true, "items": [ { "reference": "product/sku:ABC-123", "title": "Blue Cotton T-Shirt", "count": 2, "total_price": 59.8 }, { "reference": "product/sku:ABC-987", "title": "Blue Linen Shirt", "count": 1, "total_price": 44.9, "was_discounted": true } ]}At least one item is required. total_price is the line total, not the unit price.
Send it when the order completes. A server-side order handler is the more reliable place to send it from: browser-side events can be lost to ad blockers or a closed tab. Sending it from the browser also works.
Attribution
Section titled “Attribution”You do not join events to result sets yourself. Luigi’s Box records what each surface returned when the request was served, and attributes an event to it by matching the object within the shopper’s session.
That needs two things from you:
- A stable
X-Lbx-Visitor-Id, so the events and the result sets belong to the same session. - A
referencethat resolves to the catalog object — see Object references. An event whose reference resolves to nothing cannot be attributed to anything, because there is no object to match on.
A purchase is attributed through the click that preceded it in the session.
Consent
Section titled “Consent”consent_granted should come from the same place as the personalize flag on your
discovery requests:
const consent = () => consentManager.hasAnalyticsConsent();Send events either way — they still count toward volume reporting — but do not claim
consent that was not given. And keep personal data out of event fields entirely, including
out of context.
Reliability
Section titled “Reliability”- Consider sending from the server where it is easy. Browser-side events can be lost to ad blockers or a closed tab; purchases are the ones where that matters most.
- Use
sendBeaconfor browser events on unload. Afetchin progress when the page navigates away is cancelled. - Retry with a stable
event_id. A503or a network failure is worth one retry; reusing theevent_idis what keeps the retry from double-counting. - Never block the UI on an event. Fire and forget. A failed event should never break a page.
Troubleshooting
Section titled “Troubleshooting”| Response | Cause |
|---|---|
400, missing header | No X-Lbx-Visitor-Id |
400, content type | Content-Type is not application/json |
400, malformed JSON | The body did not parse, or was not an object |
422, channel_id | Not a valid lbn_… identifier |
422, reference | Not in <type>/<field>:<value> form |
422, boolean | A boolean field received 1, 0 or another non-boolean |
403 | The token is not granted to send events for this channel |
Events accepted with 202 but not visible in reports usually mean the references do not
resolve to catalog objects — check the <type>/<field>:<value> values against real @ids.
See also
Section titled “See also”- Event reference — every field
- Analytics overview — what the data is for
- Browser tokens — a token for the events audience
- Personalization — the matching consent flag
Was this page helpful?
Thanks.