Skip to content
Feature · Frontend

Ship visual search to your storefront in one script tag.

Trooply's drop-in widget gives your shoppers text search, image-upload search, and promo banners inside a single embeddable UI. Auth is handled by Origin-bound pk_live_ public keys — no server secret in page source, no CORS config, no CDN of your own.

Auth: public key + Origin allow-list Modes: text · image upload · image URL Ships: promo banners · product cards · feedback Script: /widget/v1/search-widget.js
How it works

Mint, paste, ship.

The widget loads lazily, authenticates against an Origin-bound public key, and renders results inside whatever container you give it.

Step 1

Mint a public key

At /portal/widget → "Public keys" → "Create key". Enter the exact origins the key is allowed to be served from (up to 50).

Step 2

Drop the script tag

Copy the snippet from the portal into your storefront template. The widget reads its config from the script tag's data-* attributes.

Step 3

Ship

Deploy. The widget auto-mounts on first page load, lazy-loads its assets, and starts serving searches from your Trooply index in under 200 ms.

What you get

A production-ready search surface.

No secret in page source

Public keys are read-only and Origin-bound. A key pasted onto another domain fails on the first call — Origin mismatch. Your sk_live_ server secret stays server-side.

Image + text in one UI

Shoppers type a query, drop a file, paste a screenshot, or click to browse. The widget posts directly to /v1/widget/search/upload over multipart — no client_secret in HTML, no data-URL workaround.

Renders promo banners

Top, middle (spliced between rows 4 and 5), and bottom. Merchandisers configure banners at /portal/promos and they appear inside widget results automatically.

Feedback tracking built in

Click tracking posts to /v1/search/feedback automatically — ranking learns from conversions without custom code.

Theme-aware

CSS variables for all colours, fonts, and radii. Override them in your storefront CSS and the widget blends in.

Per-key rate limits

Each public key has its own rate budget. A compromised key hammered by a scraper can't exhaust your server-side search budget.

Use cases

Where the widget earns its keep.

Shopify storefronts

Add visual search to an existing theme

Paste the snippet into theme.liquid. The widget mounts into a container of your choosing and reads theme CSS variables for colour-fidelity with the rest of the store.

Headless + Next.js

Server-render the shell, client-hydrate the widget

Render an empty mount point server-side, let the widget attach on the client. Works under strict CSP by allow-listing the widget's origin.

Multi-brand

Different keys per storefront, same index

Your catalog lives on one Trooply tenant. Each brand's storefront gets its own pk_live_ key with its own Origin allow-list. One index, many storefronts, isolated blast radius.

In-store kiosks

Camera capture + visual search

A storefront kiosk at kiosk.shop.example.com gets its own public key. Shoppers take photos of items they spotted outside, the widget does the rest.

The embed and the auth model.

Minimal embed

<div id="trooply-search"></div>
<script src="https://search.trooply.ai/widget/v1/search-widget.js"
  data-client-id="client_abc123"
  data-public-key="pk_live_54f9..."
  data-mount="#trooply-search"
  data-max-results="10"
  data-api-base="https://search.trooply.ai"
  defer>
</script>

Auth: three things the middleware checks

Every widget call carries X-Trooply-Key (the public key) and the browser-set Origin header. Our middleware validates:

  1. The key exists and is active.
  2. The request's Origin matches one entry in the key's allow-list (falls back to Referer when Origin is absent; rejects when both are missing).
  3. The per-key rate budget isn't exhausted.

Any failure returns 403 without revealing which check failed — we don't help attackers enumerate origins.

Direct multipart uploads

Image-upload search posts the file directly as multipart/form-data to /v1/widget/search/upload with X-Trooply-Key as the only auth header. No data-URL roundtrip, no client_secret in the page, no custom CORS config. The 10 MB cap and JPG/PNG/WebP allow-list are enforced server-side.

Earlier widget builds (≤ v1.2.0) ran image search through /v1/widget/search/url with a base64 data URL; that endpoint still works for "search this image" deep-links where the image already lives on a public URL.

What the widget calls

  • POST /v1/widget/search/text — text queries.
  • POST /v1/widget/search/upload — drag-drop, paste, or file-picker uploads (multipart, since v1.3.0).
  • POST /v1/widget/search/url — "search this image" buttons where you already have a URL.
  • GET /v1/widget/search/autocomplete — type-ahead with live product previews.
  • GET /v1/widget/search/frequently-bought-with/{id} — recommendations after a click.

Read-only search only. Indexing, deletes, merchandising, and analytics are never exposed via the public-key path — if your backend needs those, it authenticates with the server secret.

FAQ

Common questions.

What if a public key gets leaked?

Origin binding means a leaked key only works from the allow-listed domains. Even so, toggle is_active=false in the portal — the key stops working within seconds, and if it turns out to be a false alarm you re-enable it without changing any URLs.

Can I customise the UI?

The widget exposes CSS variables for colours, fonts, radii, and shadow. Override them in your storefront CSS and the widget blends in. For deeper customisation, use /v1/widget/search/* directly and build your own UI.

Is there a web-component version?

Planned but not shipped. Current widget is a vanilla-JS script tag — works in every framework but isn't a framework-idiomatic component.

How do I know when to rotate my server secret?

Once you've migrated everything to public keys and no page references data-client-secret any more, rotate sk_live_ from /portal/developer. See the deep-dive for the safe sequence.

Does the widget work under a strict Content-Security-Policy?

Yes. Allow-list search.trooply.ai in script-src, connect-src, and img-src. The widget doesn't use inline scripts or eval.

Is there a rate limit I should worry about?

Per-key rate limits are generous but real. If you're getting 429s, split traffic across multiple keys or contact support for a tier increase. See Errors & rate limits for the mechanics.

One script tag, visual search everywhere.

Mint a public key, paste the snippet, ship. Free tier includes the widget with up to 1,000 indexed products.