Edge Recommendation API Reference

Real-time product-recommendation endpoints served at the CDN edge. Most are CloudFront Functions that synthesize a JSON response from a KeyValueStore on every viewer request (no origin fetch); two are DMP-backed Lambdas fronted by a caching distribution. All are GET, anonymous, and CORS-open (Access-Control-Allow-Origin: *).

Every endpoint also answers OPTIONS with 204 for CORS preflight. Endpoints marked ?render=html can return a human-readable HTML card view instead of JSON.

CloudFront Functionshttps://addon-reco.cscbandit.com
DMP Lambdas (cdn stack)https://reco-cdn.cscbandit.com
GET /{brand}/v1/

Walk-mode add-on pick

Single best add-on for a cart, found by walking a slugified category hierarchy leaf→root until a co-purchase list matches. `{brand}` is a registered brand prefix (e.g. `bc` = Backcountry).

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
sku repeatable required string Cart item styleids. URL-decoded (`+` = space). First value is the anchor.
category repeatable required string Category hierarchy, root→leaf, one repeated param per level. Walked leaf→root for the first match.

Example request

GET https://addon-reco.cscbandit.com/bc/v1/?sku=XYZ789&category=Bikes&category=Road%20Bikes

Response (application/json; charset=utf-8)

{
  "recommendation": {
    "addon": {
      "styleid": "ABC123",
      "stylename": "Trail Jacket",
      "brandname": "Acme",
      "style_color": "ABC123-RED",
      "picked_sku": "ABC123-RED-M",
      "current_price": 49.99,
      "on_hand": 5,
      "styleid_on_hand_total": 12,
      "lift": 2.5,
      "n_cooccur": 100,
      "n_addon_orders": 45
    },
    "anchorStyleid": "XYZ789",
    "anchorCategoryName": "Road Bikes",
    "anchorSlug": "road-bikes",
    "matchLevel": "leaf"
  },
  "brand": "bc"
}

Errors

StatusWhen
400Missing `sku` or `category`.
404Unknown brand prefix (not in brands.ts).
405Method other than GET/OPTIONS.
500No data source configured for the brand.
GET /{brand}/v1/cat/{slug}

Path-mode add-on pick

Same pick as walk-mode but the anchor category is taken from the URL path (`{slug}`) instead of repeated `category=` params. Used by CC (`/cc/v1/cat/`).

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
sku repeatable required string Cart item styleids. At least one required.

Example request

GET https://addon-reco.cscbandit.com/cc/v1/cat/road-shoes?sku=ABC123

Response (application/json; charset=utf-8)

{
  "recommendation": { "addon": { "styleid": "ABC123", "picked_sku": "ABC123-RED-M", "current_price": 49.99, "...": "see walk-mode" }, "matchLevel": "leaf" },
  "brand": "cc"
}

Errors

StatusWhen
400Missing `sku`.
404Unknown brand prefix.
405Method other than GET/OPTIONS.
GET /cc/v2/ ?render=html

Persona fast-path add-on

Single add-on chosen by a persona model (cart slugs/brands/discipline → cluster → predicate pick), falling back to the v1 co-purchase pick on a miss.

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
sku repeatable required string Cart item styleids.
slug repeatable required string Union of cart item slug ancestors.
brand repeatable optional string Distinct brands on cart items.
discipline repeatable optional string Top-level discipline tokens (e.g. 'road', 'mtb', 'gravel').
priceTier optional 'low'|'mid'|'premium'|'flagship' default: low Price tier of the cart.
cartSize optional integer default: sku count Distinct styleids in cart.
render optional 'html' Return a self-contained HTML card view instead of JSON.

Example request

GET https://addon-reco.cscbandit.com/cc/v2/?sku=ABC123&slug=road-shoes&priceTier=premium

Response (application/json; charset=utf-8)

{
  "recommendation": {
    "addon": {
      "rank": 1,
      "styleid": "ABC123",
      "picked_sku": "ABC123-RED-M",
      "style_color": "ABC123-RED",
      "current_price": 79.99,
      "on_hand": 10,
      "source": "persona"
    },
    "anchorStyleid": "XYZ789",
    "pick_via": "predicate",
    "cluster_id": 42,
    "persona_label": "Weekend Roadie"
  },
  "brand": "cc"
}

Errors

StatusWhen
400Missing `sku` or `slug`.
404Unknown path.
405Method other than GET/OPTIONS.
GET /cc/v2/cart-filler/ ?render=html

Free-shipping cart filler

Picks one add-on priced inside a band above the current cart total — to nudge the shopper over a free-shipping / promo threshold. Band = [gap, gap × ceiling).

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
sku repeatable required string Cart item styleids.
slug repeatable required string Cart item slug ancestors.
gap required number Dollars remaining to the threshold. Must be finite and > 0, else no pick.
ceiling optional number default: 2 Upper band multiplier. Clamped to (1, 10].
brand repeatable optional string Distinct brands on cart items.
discipline repeatable optional string Top-level discipline tokens.
priceTier optional 'low'|'mid'|'premium'|'flagship' default: low Price tier of the cart.
cartSize optional integer default: sku count Distinct styleids in cart.
render optional 'html' HTML card view instead of JSON.

Example request

GET https://addon-reco.cscbandit.com/cc/v2/cart-filler/?sku=ABC123&slug=road-shoes&gap=25&ceiling=2

Response (application/json; charset=utf-8)

{
  "pick": {
    "styleid": "ABC123",
    "picked_sku": "ABC123-RED-M",
    "stylename": "Ride Socks",
    "brandname": "Acme",
    "current_price": 32.00,
    "on_hand": 3,
    "lift": 1.8,
    "style_color": "ABC123-RED"
  },
  "pick_via": "persona",
  "cluster_id": 42,
  "persona_label": "Weekend Roadie",
  "gap": 25.00,
  "band": { "min": 25.00, "max": 50.00 },
  "brand": "cc"
}

Errors

StatusWhen
400Missing required params or invalid `gap`.
404Unknown path.
405Method other than GET/OPTIONS.
GET /cc/v3/ ?render=html

Ranked add-on list

Returns an ordered array of add-ons (persona predicate picks first, then slug lookups, then v1 fallback), deduped against the cart.

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
sku repeatable required string Cart item styleids.
slug repeatable required string Cart item slug ancestors.
max optional integer default: 5 Max recommendations. Hard cap 10.
brand repeatable optional string Distinct brands.
discipline repeatable optional string Top-level discipline tokens.
priceTier optional 'low'|'mid'|'premium'|'flagship' default: low Price tier of the cart.
cartSize optional integer default: sku count Distinct styleids in cart.
render optional 'html' HTML card view instead of JSON.

Example request

GET https://addon-reco.cscbandit.com/cc/v3/?sku=ABC123&slug=road-shoes&max=10

Response (application/json; charset=utf-8)

{
  "recommendations": [
    {
      "addon": { "rank": 1, "styleid": "ABC123", "picked_sku": "ABC123-RED-M", "current_price": 89.99, "source": "persona" },
      "pick_via": "predicate",
      "cluster_id": 42,
      "persona_label": "Weekend Roadie"
    }
  ],
  "anchorStyleid": "XYZ789",
  "max": 5,
  "brand": "cc"
}

Errors

StatusWhen
400Missing `sku` or `slug`.
404Unknown path.
405Method other than GET/OPTIONS.
GET /cc/ctl/v1/{styleid} ?render=html

Complete-the-look outfit

Precomputed "complete the look" outfit for a PDP styleid: the main item plus matching items (by type/category) with candidate products each.

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
render optional 'html' HTML view instead of JSON.

Example request

GET https://addon-reco.cscbandit.com/cc/ctl/v1/ABC123?render=html

Response (application/json; charset=utf-8)

{
  "styleid": "ABC123",
  "status": "ok",
  "basis": "on-model",
  "main": { "styleid": "ABC123", "name": "Race Jersey", "brand": "Acme", "color": "Red", "category": "Jerseys", "gender": "Men's" },
  "detection_images": ["https://content.competitivecyclist.com/..."],
  "items": [
    {
      "type": "Shorts", "category": "Bottoms", "color": "Black", "gender": "Men's", "match_count": 3,
      "products": [
        { "uniqueId": "prod123", "brand": "Acme", "title": "Bib Shorts", "price": 159.99, "productUrl": "https://...", "imageUrl": "https://..." }
      ]
    }
  ]
}

Errors

StatusWhen
404No look computed for the styleid.
405Method other than GET/OPTIONS.
GET /cc/oh/v1/ ?render=html

Cross-order recommendations

Blends two cross-order signals: replenishment picks for owned consumables that are "due", then next-purchase styles bought after owning the cart's categories. Replenish picks lead.

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
sku repeatable required string Owned styleids from recent orders.
slug repeatable required string Owned category slugs (seo_url_token).
daysSince repeatable optional integer Days since purchase, aligned positionally with `sku`. Without it an item is never "due".
max optional integer default: 5 Total recommendation cap. Hard cap 10.
maxReplenish optional integer default: 2 Cap on replenish picks placed first.
render optional 'html' HTML view instead of JSON.

Example request

GET https://addon-reco.cscbandit.com/cc/oh/v1/?sku=ABC123&sku=DEF456&slug=road-shoes&daysSince=45&daysSince=30&max=5

Response (application/json; charset=utf-8)

{
  "recommendations": [
    { "styleid": "ABC123", "rep_sku": "ABC123-RED-M", "current_price": 19.99, "tier": "replenish", "due_ratio": 1.5, "group": "chamois-cream" },
    { "styleid": "DEF456", "rep_sku": "DEF456-BLK-L", "current_price": 129.99, "tier": "next-purchase", "lift": 2.3 }
  ],
  "anchorStyleid": "ABC123",
  "max": 5,
  "brand": "cc"
}

Errors

StatusWhen
400Missing `sku` or `slug`.
404Unknown path.
405Method other than GET/OPTIONS.
GET /cc/{buyer-selected|alan-selected}/v1/ ?render=html

Static curated pick map

Two hand-curated category→sku maps baked into the bundle (no KVS, no I/O), backing the CC mini-cart "People Also Bought" A/B test: `buyer-selected` (merch picks, Variant 1) and `alan-selected` (Variant 0). With `slug`, returns one pick; without, dumps the whole map.

Base https://addon-reco.cscbandit.com   Cache-Control no-store

Query parameters

ParamTypeNotes
slug optional string Category slug to look up. Omit to dump the full map.
render optional 'html' Debug table view instead of JSON.

Example request

GET https://addon-reco.cscbandit.com/cc/buyer-selected/v1/?slug=road-shoes

Response (application/json; charset=utf-8)

// with ?slug=road-shoes
{ "variant": "buyer-selected", "slug": "road-shoes", "picked_sku": "ABC123" }

// without slug
{ "variant": "buyer-selected", "count": 124, "map": { "road-shoes": "ABC123", "mtb-pedals": "XYZ789" } }

Errors

StatusWhen
404Unknown path.
405Method other than GET/OPTIONS.
GET /bc/v1/addons ?render=html

Add-on assignments

Live add-on assignments from the DMP (`sku_bc_addon_assignments`) for one or more parent SKUs. Edge-cached on the `cdn/` distribution; each cache miss is a signed SQL round-trip to DMP.

Base https://reco-cdn.cscbandit.com   Cache-Control public, max-age=300 (success); no-store (errors)

Query parameters

ParamTypeNotes
sku repeatable required string Parent SKUs. De-duped, order preserved. Max 50; blanks ignored.
account optional string default: bc DMP account id.
render optional 'html' HTML view instead of JSON.

Example request

GET https://reco-cdn.cscbandit.com/bc/v1/addons?sku=SKU1&sku=SKU2&account=bc

Response (application/json; charset=utf-8)

{
  "account": "bc",
  "requested": ["SKU1", "SKU2"],
  "results": [
    { "parentSku": "SKU1", "addons": [ { "addOnSku": "ADDON-SKU", "slot": 1, "source": "affinity-model", "createdAt": "2025-01-15T10:00:00Z" } ] }
  ],
  "count": 3,
  "timing_ms": { "total": 145.5 }
}

Errors

StatusWhen
400No `sku`, or more than 50 SKUs.
405Method other than GET/OPTIONS.
502DMP query failed.
GET /bc/v1/complements ?render=html

Knowledge-graph complements

Complementary products from the DMP product knowledge graph (edge or projection source) for one or more source styleids. Edge-cached on the `cdn/` distribution.

Base https://reco-cdn.cscbandit.com   Cache-Control public, max-age=300 (success); no-store (errors)

Query parameters

ParamTypeNotes
sku repeatable required string Source styleids. De-duped, order preserved. Max 50; blanks ignored.
brand optional string default: bc Brand code for node-id hashing.
limit optional integer default: 10 Complements per SKU. Clamped to [1, 50].
render optional 'html' HTML view instead of JSON.

Example request

GET https://reco-cdn.cscbandit.com/bc/v1/complements?sku=SKU1&sku=SKU2&brand=bc&limit=10

Response (application/json; charset=utf-8)

{
  "brand": "bc",
  "requested": ["SKU1"],
  "limit": 10,
  "results": [
    {
      "sku": "SKU1",
      "source": "edges",
      "complements": [
        { "weight": 0.85, "confidence": 0.92, "sharedActivities": ["road-cycling"], "source": "edges", "detail": { "styleid": "COMP-SKU", "name": "..." } }
      ]
    }
  ],
  "count": 15,
  "timing_ms": { "total": 234.2 }
}

Errors

StatusWhen
400No `sku`, or more than 50 SKUs.
405Method other than GET/OPTIONS.
502DMP query failed.
GET /sac/v1/top-converting ?render=html

Top-converting products

Precomputed Steep & Cheap top-converting products (30d CVR, category-diversified) for the 404-page carousel. Pass the 404'd URL as `path` and messy/misspelled slugs (`/jackkkets`, `/mens-ski-jaket-clearence`) are fuzzy-matched to product categories; categories tied for the top score (e.g. men's/women's variants of a genderless query) blend round-robin at max 2 items each, padded from the global list. A gender is never guessed: a stated gender excludes other genders' categories, and a genderless path never resolves to a lone gendered category (falls back to global; ties of 2+ gendered categories blend). Unmatched paths fall back to the global list. Served from a static S3 artifact — no live queries on the request path. No prices are returned — the artifact is rebuilt every few days and edge-cached, so any price it carried would be stale; price each card at display time from the storefront's own products API (/v1/products/{ids}, batched).

Base https://reco-cdn.cscbandit.com   Cache-Control public, max-age=300 (success); no-store (errors)

Query parameters

ParamTypeNotes
path optional string The 404'd URL path to bias recommendations. Omitted or unmatched → global list (`matched: null`).
max optional integer default: 12 Item count. Clamped to [1, 24].
render optional 'html' Return a self-contained HTML card view instead of JSON.

Example request

GET https://reco-cdn.cscbandit.com/sac/v1/top-converting?path=/mens-ski-jackets-clearance&max=8

Response (application/json; charset=utf-8)

{
  "brand": "sac",
  "matched": "men-ski-jackets",
  "matchedCategories": ["men-ski-jackets", "women-ski-jackets"],
  "generatedAt": "2026-07-15T00:00:00.000Z",
  "items": [
    { "styleid": "PAT01AB", "name": "Nano Puff Jacket", "brand": "Patagonia", "category": "Men's Ski Jackets", "slug": "patagonia-nano-puff-jacket-mens", "image": "https://content.backcountry.com/images/items/medium/PAT/PAT01AB/BLK.jpg", "cvr": 0.0312, "purchases": 61 }
  ],
  "count": 8,
  "timing_ms": { "total": 1.2 }
}

Errors

StatusWhen
405Method other than GET/OPTIONS.
502Top-converting artifact unavailable in S3.
GET /sac/v1/cart-addons ?render=html

One-click cart add-ons

Precomputed Steep & Cheap add-on pool for the minicart quick-adds widget. Every item is one-click eligibleskuId is a purchasable variant, so add-to-cart needs no size picker — and cheap, in stock, and ranked by multi-style cart attach (90d) blended with GA4 CVR (30d). Per request the pool is filtered (the anchor's style and every cart style are dropped, as are add-ons of the same kind as the anchor), then biased toward the anchor by whichever is stronger of two signals: measured co-purchase affinity for the anchor's category and fuzzy token affinity as the fallback. affinityCategory reports which co-purchase cell the anchor resolved to, or null when none did.

The co-purchase signal is category-to-category, learned offline from Backcountry's carts: S&C sells the same catalog as BC's closeout channel but not the same assortment, so evidence tied to one specific style is almost never servable here. A cell instead says anchors like this one attract this kind of add-on — ranked by lift, shrunk toward its own support and cut off below a hard support floor — and the request satisfies it out of whatever S&C has live in that group: a different sku, a different brand, a different style entirely. A group the pool does not stock at all is satisfied at a discount by a near neighbour sharing the same head noun (a Water Bottles cell pays out on Bottles & Cages), never across a stated gender. When a cell resolves, the item satisfying its strongest servable target leads the row — one slot, decided by measured evidence, because the bounded boost alone cannot promote an evidenced add-on past a much stronger seller. Order is then round-robin across product families — the head noun, not the category name, since the taxonomy spells one family a dozen ways (Lightweight Running Socks, Casual Socks, Midweight Hiking Socks, …) and a per-name cap lets a single family fill the whole widget — capped at 2 per family. Diversifying the order, rather than only the row as a whole, is deliberate: a caller that over-fetches and renders a prefix (as the minicart widget does, asking for spare candidates so a card with no live price can't shorten the row) would otherwise see slots 1 and 2 both come from one family. A gender is never guessed: a stated gender excludes other genders' add-ons, a genderless anchor blends all. Bias is a bounded boost on top of the offline rank, never a re-ranking, so a useless anchor degrades to the pool's best add-ons. Served from a static S3 artifact — no live queries on the request path. No prices are returned — the artifact is rebuilt every few days and edge-cached, so any price it carried would be stale; price each card at display time from the storefront's own products API (/v1/products/{ids}, batched).

Base https://reco-cdn.cscbandit.com   Cache-Control public, max-age=300 (success); no-store (errors)

Query parameters

ParamTypeNotes
anchor optional string Styleid or sku just added to the cart. Excluded from results; its style is never recommended back.
category optional string The anchor's category name or page path (e.g. `Men's Mountain Bike Tires`). Fuzzy-matched to a co-purchase cell key, so a scraped breadcrumb resolves without matching the taxonomy exactly. Drives the cell lookup, the token bias, the same-kind-of-product filter and the gender filter. Omitted → unbiased pool (`biased: false`).
cart optional string Comma-separated skus (or styleids) already in the cart; their styles are excluded. First 50 entries considered.
max optional integer default: 4 Item count. Clamped to [1, 8].
render optional 'html' Return a self-contained HTML card view instead of JSON.

Example request

GET https://reco-cdn.cscbandit.com/sac/v1/cart-addons?anchor=CANN123&category=Mountain+Bike+Tires&cart=CANN123-BLK-LG&max=4

Response (application/json; charset=utf-8)

{
  "brand": "sac",
  "anchor": "CANN123",
  "biased": true,
  "affinityCategory": "Mountain Bike Tires",
  "generatedAt": "2026-08-10T09:00:31.059Z",
  "items": [
    { "skuId": "DFT000L-BLKGRA-M", "styleid": "DFT000L", "styleColor": "DFT000L-BLKGRA", "name": "Aireator 6in Sock", "brand": "DeFeet X Backcountry", "category": "Bike Socks", "categorySlug": "bike-socks", "slug": "defeet-x-backcountry-aireator-6in-sock", "image": "https://content.backcountry.com/images/items/medium/DFT/DFT000L/BLKGRA.jpg", "attachCarts": 27, "cvr": 0.0412, "score": 0.438 }
  ],
  "count": 4,
  "excluded": { "inCart": 0, "redundant": 0, "gender": 0, "familyCap": 2 },
  "timing_ms": { "total": 1.1 }
}

Errors

StatusWhen
405Method other than GET/OPTIONS.
502Cart-addons artifact unavailable in S3.