Promo banners that know when to fire.
A shopper searching for "sneaker" and a shopper searching for "wedding dress" shouldn't see the same banner. A flash sale that ended last night shouldn't still be bannered. Trooply's promo banners are scope-aware and time-bounded — merchandisers configure them, the search engine decides when to render them, the widget places them inside results.
Configure, match, render.
Banners compose with merchandising rules — same scope semantics — and ride on top of every search response.
Merchandisers configure
At /portal/promos, create a banner with title, body, CTA, optional image and colours, scope (always / query / category), position, and an optional time window.
Search engine matches
On every search, the resolver checks which active banners match the query scope and the result set's categories. Priority ascending wins when more than 3 match.
Widget renders in position
The response includes a promo_banners array. Trooply's widget renders top above the grid, middle between rows 4 and 5, bottom below — or your own frontend can use the position hint however it likes.
Campaigns that manage themselves.
Scope-aware firing
A "Sneaker drop" banner only renders on sneaker searches. A "Free shipping on books" banner only renders when book-category results come back. Nothing global, nothing noisy.
Time-bounded campaigns
Optional start_at and end_at. A banner goes live at midnight Friday, stops at midnight Monday, and nobody has to remember to turn it off.
3-banner cap per response
A product decision, not a technical limit. More than three banners trains shoppers to ignore the whole region. Priority decides which three render when more than three match.
Optional styling overrides
Background and foreground hex colours per banner — or leave them null and the widget uses your theme defaults. The same campaign can render consistently across different tenants.
CTA links
Optional cta_text and cta_url. The widget renders a styled pill button that opens in a new tab. Perfect for "Shop the sale" or "See the collection".
Composes with merchandising
Banners and merchandising rules share scope semantics. Pin a SKU for "sneaker" and show a "Sneaker drop" banner on the same query — two different surfaces, one mental model.
Where banners actually convert.
"Sitewide 10% off this weekend"
Scope: always, position: top, 72-hour window. Every shopper sees the banner during the sale; Monday morning it disappears automatically.
"Free shipping on books"
Scope: category_match: "Books". Fires whenever results include book-category products, regardless of query phrasing — picks up visual searches and text searches alike.
"New sneaker drop"
Scope: query_contains: "sneaker", position: middle. Shoppers who type anything containing "sneaker" see a mid-grid banner linking to the collection page.
"Earn double loyalty points on Beauty"
Scope: category_match: "Beauty", position: bottom. Soft nudge after the shopper has scrolled the grid — converts well on category-specific loyalty promotions.
The technical shape.
Creating a banner
POST /v1/promo-banners
Authorization: Bearer $TOKEN
Content-Type: application/json
{
"title": "Sitewide 10% off",
"body": "This weekend only — auto-applied at checkout.",
"cta_text": "Shop the sale",
"cta_url": "https://example.com/sale",
"background_color": "#1E8F3E",
"foreground_color": "#FFFFFF",
"position": "top",
"scope_type": "always",
"priority": 50,
"start_at": "2026-04-25T00:00:00Z",
"end_at": "2026-04-28T00:00:00Z"
}
On the search response
Every search endpoint (/v1/search/text, /v1/search/url, /v1/search/fusion, /v1/search/voice, plus the widget endpoints) now returns a promo_banners array:
{
"query_time_ms": 142,
"count": 10,
"results": [...],
"promo_banners": [
{
"id": "...",
"title": "Sitewide 10% off",
"body": "This weekend only — auto-applied at checkout.",
"cta_text": "Shop the sale",
"cta_url": "https://example.com/sale",
"background_color": "#1E8F3E",
"foreground_color": "#FFFFFF",
"position": "top"
}
]
}
The payload is intentionally narrow — no scope internals, no audit fields, just what the storefront needs to render. Cache invalidation is automatic on every create / update / delete.
Common questions.
Do I need the widget to render banners?
No — any custom frontend can read promo_banners from the search response and render however it wants. The widget just ships with sensible default placements so new merchants get banners working without custom code.
How do banners compose with merchandising rules?
Scope semantics are identical, which is deliberate. Configure a pin rule for "sale" plus a query_exact: "sale" banner and both fire together — one moves products, the other renders a callout. They don't interact beyond that.
Can I schedule a banner for a future date?
Yes. Set start_at to a future timestamp; the banner is a no-op until then. Set end_at to an earlier date and it stops firing without needing to be deleted.
What happens on a search-result cache hit?
Banners are resolved fresh on every search, even when results come from cache. A campaign that ended 30 seconds ago doesn't haunt a still-warm cache entry.
Can banners link to external URLs?
Yes — cta_url can point anywhere. The widget renders CTAs with target="_blank" and rel="noopener". For on-site links, pass a relative URL.
Is there click tracking?
Not inside Trooply itself. Banner CTAs are simple links; track them with whatever analytics stack already handles conversions. For full attribution, use POST /v1/search/feedback with a banner ID in metadata.
Run campaigns from the portal, not the codebase.
Free tier includes promo banners. Editor at /portal/promos.