On a shop platform? Check the Shopify and Magento, Shopware, PrestaShop, BigCommerce and WooCommerce adapters first. Their platform-specific pages explain installation availability and which page, cart, customer and order context they supply automatically.
Choosing between the embed and the SDK
Both serve the same recommendations from the same contexts.
The embed suits a website where you want recommendations rendered for you. The
SDK suits cases where you need full control of the markup, or where you are
serving somewhere a browser script cannot run — a mobile app, an email, a
server-rendered page you assemble yourself.
Get a publishable key
1
Open Website embed
In the console, go to Developers → Website embed.
2
List your origins
Add every origin that will render the widget, including staging. A key with no origins does nothing.
3
Copy the snippet
The console gives you the script tag with your key already in it. The key is shown once, when you create it.
4
Check it is live
The same page shows the origins we have received requests from, including any we turned away because they were not on your list.
nsl_pk_…) is readable by anyone who views your page
source, and is built for that. Its permissions are limited to reading
recommendations and reporting impressions: it cannot administer your
workspace, read your catalogue, or create other credentials.
Placeholders
A placeholder is an empty element that marks where a strip should render.auto is what removes the need for integration code: your catalogue is keyed
on canonical URLs, so the page already carries the identifier for the item it
is about.
Placeholders added to the DOM after load are picked up automatically, so
single-page applications work without re-initialising anything.
Styling
The strip renders inside a shadow root. Your stylesheet cannot reach into it, and its styles cannot leak out into your page. CSS custom properties cross that boundary, and are how you restyle it:Choosing a script URL
Two URLs are published. A versioned URL supports Subresource Integrity, because its contents never change; a rolling URL updates itself, which is incompatible with a fixed integrity hash.
Per-version integrity hashes are published with each release.
What the widget sends
- A recommendation request carrying the page’s canonical URL and a random first-party visitor id. Your key travels as a header, never in the URL.
- Impressions, once a card has been at least 50% visible for two seconds. Scrolling past quickly is not an impression, and scrolling back does not count a second one.
- Clicks, delivered with
sendBeaconso they survive the navigation. The widget does not intercept or delay the click itself.
localStorage and sessionStorage.
Where that storage is unavailable — private browsing, blocked site data — the
widget still renders and simply does not recognise the visitor on the next
page.
How an item is resolved
Arelated strip needs to know which catalogue item the page is about. The
widget reads two things and sends both:
- the canonical URL, from
data-nsl-item-url="auto" - an identifier from the page’s JSON-LD:
sku,mpn,productIDorisbn
sku still resolves the item, with no change to your markup.
You never send NeuronSearchLab’s identifiers. To override what the page says,
set data-nsl-item-sku or a literal data-nsl-item-url.
Debugging an empty strip
Add?nsl_debug=1 to any page carrying the widget. A panel lists every
placeholder, what it resolved to, the request id, how many items came back and
how many events were sent, and names the cause when something is wrong:
- Request failed — the key is not valid for this origin. Check the origin list in the console.
- This page is not in your catalogue — neither the URL nor the sku matched an item, so there is nothing for the strip to be related to.
- No request_id — the strip rendered, but views and clicks are not being recorded.
- No context to serve from — the workspace has no contexts. Create one under Contexts.
data-nsl-debug on the script tag enables the panel permanently. The query
parameter is usually more practical: the script tag is normally baked into a
theme, and the parameter needs no deploy.
Page context
The crawler reads your product pages, but it cannot see a basket, it does not know whether anyone is signed in, and it never reaches a thank-you page — those arenoindex and are skipped. Your page can supply that context through
a command queue:
Each command replaces the previous value for that slot rather than adding to
it, so calling
nsl('cart', …) twice describes one basket. Commands and
JSON-LD can be mixed freely — there is no mode to select between them.
Where you cannot run script, one element does the same job:
Context resolves against your catalogue or it is dropped and counted. It can never create an item, which is what keeps a key published in page source from being a route into your catalogue.
Category and search pages
A listing page says what it is listing, and a results page says what was searched:category filters on the category field of your item metadata. Catalogues
disagree about what that field is called, so a value may carry its own field
name: topic:Health filters on topic, while a bare Health filters on
category.
query turns the request into query-driven retrieval. If the workspace has no
query embedder, the request is retried without it, so the strip still renders.
These scope a single strip from the page. To scope every strip on a surface, put the filter on the context the placement points at — one context can serve many placements.
Conversions
order_reported signal, never as
purchase. A browser cannot prove that a sale happened, so the event type the
embed creates for it has weight 0: it is for attribution and revenue reporting,
not training. You can change that weight like any other, but a publishable key
is readable from your page source, so anyone could report sales against it.
Order lines are keyed on the order id, so a reloaded thank-you page cannot
count a sale twice. An order id is therefore required — without one there is
nothing to deduplicate on.
If you also connect Shopify, the verified order replaces the browser’s report
in place rather than adding a second record.
Placements
A placement is a name you give a strip. What that strip serves then becomes a console setting rather than a markup change:grid or list), which fields appear and an optional
heading. Click-through for each placement is then reported without further
instrumentation.
A placement the console does not recognise still renders, using your workspace
defaults, so renaming one does not blank a live page.
Which context serves a strip
Every strip is served by a context, which is what decides the recipe, the pipeline, the rules and the filters behind it. The context is chosen in this order:- The placement’s context, if the strip names a placement that has one.
- Your workspace’s default context.
Events
The embed never sends an event name. It reports signals, and your workspace decides which event ID each one lands on:
Creating an embed key creates an event type for each of these — an integer ID,
a label and a weight — and binds the signal to it.
view uses the View
event your workspace already has. The event types count toward your plan’s
event-type limit like any other; the Free plan has room for the defaults plus
the embed.
These are ordinary event types. Rename them, change their weights, and choose
which ones a training template uses on the Events page. To
send a signal to an event you already track — say your server already records
product views — open Events → Integration signals and pick that event.
Nothing is ever matched by name, so a workspace whose events are called
something else entirely works the same way.
Recommendation impressions are kept apart from view on purpose. An item the
model chose to show is much weaker evidence of interest than someone opening
it, and counting the two as one event teaches the model to agree with itself.
What a browser cannot send
The embed cannot sendpurchase or any event ID directly. Send verified
conversions from your server with the
Events API, or import them with a
backfill.
Strip events are checked against the recommendations actually served, so a key
copied from your page source cannot report engagement for arbitrary items.
view and add_to_cart must come from an allowlisted origin, name an item in
your catalogue, and count once per visitor, item and session. Neither is sent
while nsl('consent', false) is in effect.
Embed events go through the same pipeline as the Events API, so they are
metered, stitched to identities and reflected in the next recommendation the
same way.
Quota
Embed traffic is metered separately from API requests, because a strip makes one request per pageview. When the allowance is exhausted the widget continues to render, serving cached results, and the response carriesX-NSL-Quota: exceeded. It does not begin
failing on your live site.

