--- title: Sending events description: Report clicks, cart additions and purchases to Luigi's Box AI — the envelope every event shares, the calls for each one, and how attribution works. slug: analytics/sending-events docKind: guide hub: luigisbox-ai --- 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 One endpoint, one event per request: ```text POST /events/v1/events ``` A click on a search result: ```bash curl -X POST 'https://api.eu1.luigisbox.ai/events/v1/events' \ -H 'Authorization: Bearer ' \ -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: ```text 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..luigisbox.ai/events` in the token request's `audience` — it accepts an array, so the same token can also cover discovery. See [Browser tokens](/authentication/browser-tokens/). ## 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](#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 Anywhere an event names a catalog object, it uses a reference: ```text /: ``` ```text product/@id:product/sku-1001 product/sku:ABC-123 product/ean:5901234123457 ``` Any field the object carries can be the field, so you can send the identifier the page already has. [Object references](/analytics/references/) covers that and what happens when a reference resolves to nothing. ## Clicks ```json { "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](#attribution). ## Cart additions Same event type, a different `interaction_type`, plus quantity and value: ```json { "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 ```json { "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 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 `reference` that resolves to the catalog object** — see [Object references](/analytics/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 `consent_granted` should come from the same place as the `personalize` flag on your discovery requests: ```js 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 - **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 `sendBeacon` for browser events on unload.** A `fetch` in progress when the page navigates away is cancelled. - **Retry with a stable `event_id`.** A `503` or a network failure is worth one retry; reusing the `event_id` is 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 | 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 `/:` 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 `/:` values against real `@id`s. ## See also - [Event reference](/analytics/event-reference/) — every field - [Analytics overview](/analytics/overview/) — what the data is for - [Browser tokens](/authentication/browser-tokens/) — a token for the events audience - [Personalization](/discovery/personalization/) — the matching consent flag