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.

    Sending events

    View source

    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:

    ReportAs
    A shopper clicked a resultinteraction, interaction_type: "click"
    A shopper added something to the cartinteraction, interaction_type: "add_to_cart"
    An order completedtransaction

    One endpoint, one event per request:

    POST /events/v1/events

    A click on a search result:

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

    RequirementWhy
    Content-Type: application/jsonAnything else is a 400
    X-Lbx-Visitor-Id headerIdentifies the browser. Missing it is a 400.
    A token for the …/events audienceGranted 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.

    Both event types carry the same base fields.

    FieldRequiredNotes
    typeYesinteraction or transaction
    channel_idYeslbn_… — the storefront this happened on
    event_idNoYour own idempotency key. Generated if omitted.
    event_timestampNoUnix seconds. Set to receipt time if omitted.
    catalog_idNolbc_…
    user_idNoYour opaque key for a signed-in shopper
    consent_grantedNoDefaults to false. See Consent.
    currencyNoISO currency code
    platformNoweb, ios, android, … — your own vocabulary
    referrerNoReferring URL
    contextNoFlat map of string keys to string values, for your own segmentation
    ab_test / variantNoYour own external A/B test
    app_versionNoYour 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.

    Anywhere an event names a catalog object, it uses a reference:

    <type>/<field>:<value>

    Any 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.

    {
    "type": "interaction",
    "channel_id": "lbn_4hj9tv",
    "reference": "product/@id:product/sku-1001",
    "interaction_type": "click",
    "listing_ab_test": false,
    "consent_granted": true
    }
    FieldRequiredNotes
    referenceYesThe object clicked
    interaction_typeYesclick
    listing_ab_testYesWhether the click came from a result set under an A/B test
    urlNoWhere it happened

    On a results page, reference is the @id straight from the response — see Attribution.

    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
    }
    FieldRequiredNotes
    countNoQuantity added
    priceNoUnit 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.

    {
    "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.

    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. 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_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.

    • 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.
    ResponseCause
    400, missing headerNo X-Lbx-Visitor-Id
    400, content typeContent-Type is not application/json
    400, malformed JSONThe body did not parse, or was not an object
    422, channel_idNot a valid lbn_… identifier
    422, referenceNot in <type>/<field>:<value> form
    422, booleanA boolean field received 1, 0 or another non-boolean
    403The 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.