Skip to main content

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 of POST /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 experiment with named variants, each carrying its own factor weights, 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 in assigned_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 to owner, 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 Segment user_id / external_id token). 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).
Factor weights are tuned per-factor with a blank means “use the engine default”. The draft you build persists at account level — a save banner warns if a refresh or another device would lose your edits. Validation runs before any request: a malformed profile id, an empty catalog, or an invalid signal row blocks the button with an inline error rather than a failed call.

3. Reading the per-factor breakdown

The response echoes the blended score 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_affinity trait 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 90witha90 with a 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.
The summary line above the result table counts 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 the signals 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:
The response carries 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:
  1. Run rank for 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).
  2. 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.
  3. For multi-variant optimization, wrap the call in an experiment block and let the assigned variant name drive the message version recorded for attribution; conversions attributed to a strategy tell you which weight profile wins.
Keep 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.
  • 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 422 before any ranking runs.
  • Role gate: requests need an owner, admin, or developer identity; the tester hides the surface for other roles and the API enforces the same gate.
  • Id format: profile_id is a plain identifier token (no URLs or free text) — enforced both in UI validation and API schema, with a 422 on violation.
For the full field-level contract see CDP API reference — predictive models, and for the model-based (trained) alternatives — when you want fit scores rather than content affinity — see CDP predictive models.

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