Next-best-product recommendations
Recommendations ranks one question per profile: given the products you could put in front of this person right now, which are they most likely to buy next? You pass a candidate catalog and the profile’s recent purchase and browse signals, and the rank endpoint returns the catalog ordered by blended affinity with a per-factor score breakdown on every item. This guide covers how the engine ranks, what inputs the tester needs, how to read each factor’s contribution, a worked run against a stored profile, and how to carry the top-ranked items into a campaign goal. The same endpoint backs the dashboard page at Audience → Recommendations. The tester and the API run the same ranking — the tester is a rendering ofPOST /api/v1/cdp/recommendations/rank, so anything you validate in the UI transfers verbatim to automation.
Recommendations is one of three per-profile decision surfaces — pick the one that matches your question:
1. How the engine ranks the catalog
The engine is a pure content-affinity scorer — no stored model, no training step, no stored state. Each call rank-scores the candidate catalog you pass against the signals you pass and returns the ranking it derived on the spot. For every in-stock, non-excluded candidate, it blends up to five factors, each normalized to[0,1]:
Signals do not all count equally. Each signal carries an intent weight by interaction kind —
purchase 1.0, cart 0.6, wishlist 0.5, view 0.25, click 0.15 — and is decayed by recency: a signal as old as the half-life (default 30 days) counts half as much. Weights are relative, not absolute, and the blend divides by the sum of the applicable factors only — so a cold-start profile with no spend history is scored fairly on category and brand rather than penalized for the missing price-fit factor.
Two consequences follow from the stateless design:
- Deterministic for A/B. Pass an
experimentwith namedvariants, each carrying its own factorweights, and the profile is bucketed by a stable hash of(experiment.key : profile_id)— every variant applies the ranking under its own weights, the assigned variant is echoed back inassigned_variant, and the assignment is reproducible from the key. There is no randomness to seed. - Never dispatches. The surface ranks; it never sends. The catalog send path (an RCS / WhatsApp / Apple-Messages in-thread carousel) renders the result. Excluded products stay excluded, and out-of-stock candidates never surface.
2. Where the tester lives and what it needs
Open Audience → Recommendations in the dashboard. Access is role-gated toowner, admin, and developer (the API hierarchy applies to the tester too). The page assembles a rank request in three blocks:
Candidate catalog. One row per product: product_id, title, category, brand, price, tags (comma-separated), and an in_stock toggle. Out-of-stock and excluded items are deducted before scoring, so the score column never rewards an unavailable product.
Profile signals. One row per recent interaction: type (purchase / cart / wishlist / view / click), the item’s category and brand, its product_id, and age (days) for recency decay, plus value for purchase signals to build the spend profile. The tester seeds a small outdoor-retail catalog with mixed-intent signals so the first click gets a real ranking; replace the seed with the profile’s rolled-up events.
Scoring & scope. The refine block takes four optional inputs:
- Profile id — a contact id (
cnt_…) or an external CDP profile id (a Segmentuser_id/external_idtoken). Entering one ties the ranking to a stored profile and drives A/B bucket assignment; the page resolves the id live and warns when no profile matches. Leave it blank to rank on the entered signals alone. - Limit — how many ranked items to return (default 10, max 100).
- Half-life days — the recency decay window (default 30). Raise it for long purchase cycles (furniture), lower it for fast trends (apparel drops).
- Suppress purchased — exclude any product id the signals list already shows as bought (on by default; turn off to test replenishment journeys).
3. Reading the per-factor breakdown
The response echoes the blendedscore and a factors map with each of the five factor scores in [0,1]. Read the breakdown row-by-row:
- Category says “what they browse.” A high category score means the signals and the
category_affinitytrait point at that category — it dominates the blend at 0.40 by default. - Brand says “whose ~they buy from.” Useful for spotting brand lock-in even inside a category the profile otherwise rarely touches.
- Tag says “which attributes match.” Tag is the fine-grained differentiator within a category (
lightweight,wool,2-person). - Price fit says “is this in their spend band.” A profile averaging 39 vs a $240 candidate will see a price-fit gap between them even if the category and brand scores tie.
- Popularity says “what the crowd picks.” The tie-breaker for profiles with thin signals — zeros on the other four factors fall back to it.
considered (candidates that passed the gates), excluded (out of stock, exclude_product_ids, or suppression), and returned (capped by the limit), plus the effective half-life. When every candidate is out of stock or excluded, the tester says so directly instead of showing an empty rank.
4. Worked example: rank one production profile
Goal: pick the next-best product to feature in an RCS carousel for a known contact. Start in the tester to see the shape, then automate through the API. 1. Pull the signals. From the CDP profile you roll up the recent purchase/browse events into thesignals array — one row per event with its kind, category, brand, product id, age in days, and (for purchases) value. The tester’s seed shows the intended shape; in production those rows come from your event rollups or the profile API rather than manual entry.
2. Enter the candidate catalog. The tenant’s commerce product set — the same products an RCS / WhatsApp carousel would render — one row per SKU.
3. Set the scope. Paste the contact’s cnt_… id into Profile id. The identity chip resolves the profile; a no-match warning means the ranking would not tie to a stored profile, so fix the id before trusting it for a real send. Leave Limit and Half-life at defaults unless the catalog is large or the purchase cycle is unusual.
4. Rank. The result table orders the catalog descending with the top pick highlighted. A typical output row for a runner who bought a summit pack 45 days ago and has browsed basecamp tents:
The tent wins on brand and tag (recent basecamp views outweigh the older summit purchase); the suppressed already-bought pack drops to #3 through
suppress_purchased. Read the gap between #1 and #2 before you commit a carousel slot — a margin under ~0.05 usually means the A/B experiment is a better call than a single pick.
5. Same shape over the API. What the tester sends is exactly what your service sends:
recommendations (rank, product_id, title, category, brand, price, score, and the factors breakdown), considered_count, excluded_count, and the effective weights and half_life_days — everything the tester renders comes from the same fields.
5. Carrying top-ranked items into a campaign goal
The ranking is a targeting input for your campaign, not a send itself:- Run
rankfor each profile in the send segment (a nightly batch call per profile is the common shape — the endpoint is rate-limited at 60 calls/min per tenant, so batch rather than burst). - Feed the top
product_id(s) into the campaign’s message template — the RCS / WhatsApp / Apple-Messages catalog carousel renders the same fields the tester shows. - For multi-variant optimization, wrap the call in an
experimentblock and let the assignedvariantname drive the message version recorded for attribution; conversions attributed to a strategy tell you which weight profile wins.
suppress_purchased on for replenishment-sensitive journeys, off for “buy again” flows, and let exclude_product_ids drop seasonal or embargoed SKUs at call time.
6. Quota, rate, and related APIs
- Rate limit: 60 requests/min per tenant — the same compute-surface quota every CDP decision endpoint carries. Burst protection so a batch loop doesn’t crowd out interactive tester traffic; batch with a small sleep between items.
- Limits: catalog 1–2000 items, signals up to 5000, limit 1–100. A misuse returns a structured
422before any ranking runs. - Role gate: requests need an
owner,admin, ordeveloperidentity; the tester hides the surface for other roles and the API enforces the same gate. - Id format:
profile_idis a plain identifier token (no URLs or free text) — enforced both in UI validation and API schema, with a422on violation.
See also
- AI decisioning — pick the next-best message action for the same profile
- CDP predictive models — trained churn / conversion / LTV / fatigue scores behind segment audiences
- CDP audiences — build the segment the batch rank call serves
- CDP API reference — the full endpoint contracts for the recommendations, decisioning, and offers surfaces