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, most recent first. Only the first 12 are read.
slug repeatable required string Owned category slugs (seo_url_token), most recent first. Only the first 12 are read.
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). Price the pictured variant, not the style: image is one colorway and each item carries skuId (the cheapest sellable sku of that color at build) + styleColor; the CRO price step must pin to it (skuPath: "skuId") and the card should deep-link ?skid=, because a style's cheapest OTHER colorway under this photo is a price the PDP will not honor.

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", "styleColor": "PAT01AB-BLK", "skuId": "PAT01AB-BLK-M", "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 /bc/v1/top-converting ?render=html

Top-converting products

Precomputed Backcountry 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). Price the pictured variant, not the style: image is one colorway and each item carries skuId (the cheapest sellable sku of that color at build) + styleColor; the CRO price step must pin to it (skuPath: "skuId") and the card should deep-link ?skid=, because a style's cheapest OTHER colorway under this photo is a price the PDP will not honor.

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/bc/v1/top-converting?path=/mens-ski-jackets-clearance&max=8

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

{
  "brand": "bc",
  "matched": "men-ski-jackets",
  "matchedCategories": ["men-ski-jackets", "women-ski-jackets"],
  "generatedAt": "2026-09-02T00: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", "styleColor": "PAT01AB-BLK", "skuId": "PAT01AB-BLK-M", "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 eligible — skuId 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). Price the pictured variant, not the style: image is one colorway and skuId is that colorway's purchasable sku — crossSell prices exactly it via recSource.livePrice; never show a style-level price under it.

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.
GET /{bc|sac}/v1/slate ?render=html

Per-anchor add-on slate

Precomputed add-on slate for one anchor style — /bc/v1/slate for backcountry.com, /sac/v1/slate for steepandcheap.com (S&C slates are mined over BC + S&C carts and served from S&C-live styles) — the data-driven successor to the merch-curated PDP add-on map. Built offline (scripts/bc-addons-slate.ts) from a year of BC orders on a backoff ladder: style→style co-purchase where the anchor has enough carts, then product-group→product-group cells hydrated with the best live styles in the target group, then merch-group cells, each ranked by lift shrunk toward its support and floored on carts; a Bedrock Claude pass types every category cell (complement / substitute / unrelated / incompatible — so a vest is demoted under a jacket), judges each candidate on the house 1–4 rubric (1 = embarrassing, dropped), and for thin or new anchors proposes what a shopper would want, which the build resolves against the catalog. Hard constraints: never the anchor's own family (head noun), never the other gender under a stated gender, never adults' goods under kids' (and vice versa), only live in-stock styles with an image, and the merch team's Immediate-Removal rows are a standing blocklist. Everything else ships with a calibrated confidence q in [0, 1] (typed cell relation × judge rating × how the pair was found × co-purchase lift × measured add-on performance — the pair's shown→bought rate from our own impression stream and GA4 add-on toggles, shrunk toward its category pair and the site rate, and a pair shown to 300+ visitors that was never bought is dropped outright; a typed substitute lands near 0.01, an unrelated pad near 0.02, an unjudged category pad ≈ 0.2, a style-proven complement the judge loved ≈ 0.9) together with the facts behind it (relation, judge, pad), and floor decides at request time — floor=0 is the stored slate unchanged. Each anchor also stores a fill of constraint-checked division/site best sellers: when a floor leaves fewer than min items the strip is padded from it, AFTER the real items, so a strict floor never serves an empty strip. Two orderings are stored per anchor — ranked (score order) and kit (a greedy pick that rewards three-way co-purchase with what is already in the slate and penalizes same-family / same-merch-group / same-brand repeats) — so a bandit can allocate between policies. Per request: the anchor's shard is read from a warm cache (256 shards keyed by a hash of the styleid), cart/exclude styles are dropped, the survivors keep the build's ranking order, re-spaced round-robin across product families (≤ 2 per family) so every prefix the widget renders is diverse. why is the typed cell's phrase only for a complement cell (for any other relation it is a description, not a reason, and the rung default is served instead). Served from a static S3 artifact — no live queries on the request path. No prices or stock are returned — price and stock-check each card at display time from the storefront's products API (/v1/products/{ids}, batched).

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

Query parameters

ParamTypeNotes
anchor optional string The PDP styleid (a sku is accepted; its style is used). One of anchor / slug is required. Unknown anchor → 404 (cacheable until the next build).
slug optional string The PDP's URL slug, resolved through the build's slug index — the id every page has without any lookup.
cart optional string Comma-separated skus or styleids already in the cart; their styles are excluded. First 50 entries considered.
exclude optional string Comma-separated styleids to leave out (e.g. styles the native add-ons accordion already shows).
policy optional 'ranked' | 'kit' default: ranked Which stored ordering to serve. The bandit arms differ by this.
max optional integer default: 6 Item count. Clamped to [1, 24].
floor optional number default: 0 Drop items whose confidence q is below this (0–1). 0 serves the stored slate unchanged; ignored (and echoed back as 0) on an artifact built before items carried q. Bandit arms differ by this.
min optional integer default: 3 Serve at least this many items when the floor left fewer: pad first with the anchor's own best items the floor removed, in rank order (the best-evidenced add-on even if it sat under the bar), then from the stored fill (constraint-checked high-performers, rung 'fallback') when no real item is left. Always after the survivors. `padding: { real, fill }` in the response says which. 0 = never pad. Clamped to [0, 24].
render optional 'html' Return a self-contained HTML card view instead of JSON, headed by the anchor product itself (image, name, brand, PDP link) so it is obvious what the slate hangs off.

Example request

GET https://reco-cdn.cscbandit.com/bc/v1/slate?anchor=ARMN3YL&max=6&floor=0.25

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

{
  "brand": "backcountry",
  "anchor": "ARMN3YL",
  "policy": "ranked",
  "floor": 0.25,
  "min": 3,
  "rung": "group",
  "generatedAt": "2026-08-27T10:12:03.412Z",
  "items": [
    { "styleid": "SALN0K3", "slug": "salomon-strive-gw-14-ski-binding-2027", "url": "https://www.backcountry.com/salomon-strive-gw-14-ski-binding-2027", "image": "https://content.backcountry.com/images/items/medium/SAL/SALN0K3/BLK.jpg", "brand": "Salomon", "name": "Strive GW 14 Ski Binding - 2027", "rung": "group", "why": "mounts your skis", "score": 0.81, "evidence": { "carts": 12, "lift": 9.4 }, "cellId": "group:Big Mountain Freeride Skis|Alpine Ski Bindings", "family": "binding", "fit": null, "q": 0.6, "relation": "complement", "judge": 4, "pad": false, "position": 1 }
  ],
  "count": 6,
  "padded": 0,
  "excluded": { "inCart": 0, "explicit": 0, "familyCap": 1, "floor": 2 },
  "timing_ms": { "total": 0.8 }
}

Errors

StatusWhen
400Missing/invalid anchor, an unknown policy, floor outside [0, 1], or min outside [0, 24].
404No slate for this anchor (cacheable, max-age=300).
405Method other than GET/OPTIONS.
502Slate manifest unavailable in S3.
GET /bc/v1/one-reco

One add-on per anchor (merch one-reco sheet)

Backcountry one-reco merch sheet: exactly one add-on style per anchor (skis → Phantom Glide DPKC05Z, complete bikes → RideWrap RWP000P, everything else merch's first live pick) plus the title Claude wrote for the pair. Its own stack (`edge-addon-reco/bc-one-reco-lambda`) at its own hostname; built for the `bc-pdp-one-reco-h1` XP. No prices — the widget resolves the add-on's live product record. An anchor the sheet does not name answers 200 with an EMPTY `items` list (`reason: "no-row"`), never a 404, so the XP's `hideOnEmpty` hides the native section on every PDP the sheet skips.

Base https://bc-one-reco.cscbandit.com   Cache-Control public, max-age=300 (CloudFront caches per query string)

Query parameters

ParamTypeNotes
anchor optional string Anchor styleid (or a sku — the style part is used). One of `anchor` / `slug` is required.
slug optional string The anchor's PDP slug (`elan-ripstick-88-ski-2026-womens`), resolved through the sheet's slug index.

Example request

GET https://bc-one-reco.cscbandit.comhttps://bc-one-reco.cscbandit.com/bc/v1/one-reco?slug=elan-ripstick-88-ski-2026-womens

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

{
  "anchor": "ELAC04U",
  "slug": "elan-ripstick-88-ski-2026-womens",
  "heading": "Wax Before the First Storm",
  "label": null,
  "items": [
    {
      "styleid": "DPKC05Z",
      "brand": "DPS Skis",
      "name": "Phantom Glide: Professional Application Before Shipment",
      "slug": "dps-skis-phantom-glide-application-purchase",
      "url": "https://www.backcountry.com/dps-skis-phantom-glide-application-purchase",
      "position": 1
    }
  ],
  "count": 1,
  "generatedAt": "2026-09-24T12:58:00+00:00"
}

Errors

StatusWhen
400Neither `anchor` nor `slug` given, or `anchor` is not a styleid.
502The data file could not be read from S3 (no warm copy).
GET /bc/v1/cart-addons ?render=html

Backcountry mini-cart add-ons

Backcountry twin of the S&C cart add-ons pool (scripts/bc-cart-addons.ts, artifact reco/backcountry/cart-addons.json): same request/response contract, BC inputs on every signal.

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/bc/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.
GET /dyn/v1/{brand}/{set} ?render=html

Serve a registered recommendation set

{brand} is the brand slug (backcountry, steepandcheap, competitivecyclist) and {set} the set key from Bandit’s Recommendations section (bc-pdp-slate, sac-cart-addons, bc-top-converting, …). The set’s kind decides the query parameters and the response shape: a pdp_slate set answers exactly like /{brand}/v1/slate, a cart_addons set like /{brand}/v1/cart-addons, a top_converting set like /{brand}/v1/top-converting, a pdp_bindings set (no v1 twin) answers the ski-binding engine’s good / better / best picks for one ski, and a pdp_skins set the climbing-skin engine’s picks for one ALPINE TOURING ski, each item with its size table (sizes[]) and length=<cm> choosing the sku — plus set and kind echoed in the body. Merch pins lead a slate (rung: "pin", why = the merchandiser’s note) and merch blocks never appear. A slate set’s max defaults to the build’s learned depth for the anchor’s category (attach rate by widget slot), falling back to 6 — an arm that passes max always wins; the body reports max, maxSource (request / depth / default) and depth. It also answers heading and framing. In a framed set each anchor is framed kit (“Complete the Kit”), goes-with (“Goes Great With”) or collection (“All the <X> Collection”), and the items are built for that framing. A nudge (merch’s expert context), the anchor’s own collection, merch’s category map or the set default decides which. heading is the title that goes with it: a nudge’s words, merch’s copy for the category, the framing’s title, or the set’s default. Both are null when the set has none. An experiment reads the title as "heading": "{<var>.heading}". The Lambda resolves the set through a per-brand serving registry Bandit writes to S3 (reco/serving/<brand>.json) — no database at request time; a set created, re-enabled or re-published in Bandit is served within 60 s.

Base https://reco-cdn.cscbandit.com   Cache-Control public, max-age=300, stale-while-revalidate=86400, stale-if-error=86400 (registry misses: max-age=60 only; `fresh`: no-store)

Query parameters

ParamTypeNotes
anchor optional string <em>pdp_slate sets</em>: The PDP styleid (a sku is accepted; its style is used). One of anchor / slug is required. Unknown anchor → 404 (cacheable until the next build).
slug optional string <em>pdp_slate sets</em>: The PDP's URL slug, resolved through the build's slug index — the id every page has without any lookup.
cart optional string <em>pdp_slate sets</em>: Comma-separated skus or styleids already in the cart; their styles are excluded. First 50 entries considered.
exclude optional string <em>pdp_slate sets</em>: Comma-separated styleids to leave out (e.g. styles the native add-ons accordion already shows).
policy optional 'ranked' | 'kit' default: ranked <em>pdp_slate sets</em>: Which stored ordering to serve. The bandit arms differ by this.
max optional integer default: 6 <em>pdp_slate sets</em>: Item count. Clamped to [1, 24].
floor optional number default: 0 <em>pdp_slate sets</em>: Drop items whose confidence q is below this (0–1). 0 serves the stored slate unchanged; ignored (and echoed back as 0) on an artifact built before items carried q. Bandit arms differ by this.
min optional integer default: 3 <em>pdp_slate sets</em>: Serve at least this many items when the floor left fewer: pad first with the anchor's own best items the floor removed, in rank order (the best-evidenced add-on even if it sat under the bar), then from the stored fill (constraint-checked high-performers, rung 'fallback') when no real item is left. Always after the survivors. `padding: { real, fill }` in the response says which. 0 = never pad. Clamped to [0, 24].
render optional 'html' <em>pdp_slate sets</em>: Return a self-contained HTML card view instead of JSON, headed by the anchor product itself (image, name, brand, PDP link) so it is obvious what the slate hangs off.
anchor optional string <em>cart_addons sets</em>: Styleid or sku just added to the cart. Excluded from results; its style is never recommended back.
category optional string <em>cart_addons sets</em>: 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 <em>cart_addons sets</em>: Comma-separated skus (or styleids) already in the cart; their styles are excluded. First 50 entries considered.
max optional integer default: 4 <em>cart_addons sets</em>: Item count. Clamped to [1, 8].
render optional 'html' <em>cart_addons sets</em>: Return a self-contained HTML card view instead of JSON.
path optional string <em>top_converting sets</em>: The 404'd URL path to bias recommendations. Omitted or unmatched → global list (`matched: null`).
max optional integer default: 12 <em>top_converting sets</em>: Item count. Clamped to [1, 24].
render optional 'html' <em>top_converting sets</em>: Return a self-contained HTML card view instead of JSON.
anchor optional string <em>pdp_skins sets</em> (required): The SKI styleid (`SALZB3V`); or `slug` = the ski PDP slug. The answer is the climbing-skin engine’s picks for that ALPINE TOURING ski: `ski {type, waist, tip, tail, lengths}` and `items[]`, each one skin style at one width with `tier` (price order), `rank` (1 = the pick), `name`, `brand`, `slug`, `image`, `width`, `sizing` (`letter` / `cm` / `universal`), `material`, `precut`, `specs`, `why`, and `sizes[] {skuId, styleColor, min, max, label}` — the ski lengths (cm) each size covers (null = every length). `byLength {cm: skuId}` is the engine’s cut for each of the ski’s own lengths (the band the length sits deepest in; precut charts overlap). `skuId` as served is the size for the ski’s middle length; pass `length` for a chosen one. No price — price `skuId` live and deep-link it (`?skid=`). 404 with `reason` (`not-touring`, `includes-skins` — the ski is sold with its skins —, `package`, `no-live-page`, `missing-waist`, `no-candidates`, `not-a-stocked-ski`) for a ski with no picks.
slug optional string <em>pdp_skins sets</em>: The ski PDP slug, instead of `anchor`.
length optional integer <em>pdp_skins sets</em>: The chosen ski length in cm (`173`). Every item’s `skuId` / `styleColor` become the size covering it; an item with no such size is dropped. Three digits, else 400.
max optional integer default: 3 <em>pdp_skins sets</em>: Picks returned, in engine rank order; capped at 6.
anchor optional string <em>pdp_bindings sets</em> (required): The SKI styleid (`SALZB3V`); or `slug` = the ski PDP slug. The answer is the merch ski-binding engine’s picks for that ski: `ski {type, waist, skill}` and `items[]`, each one exact binding sku (`skuId`, the brake width that fits this waist) with `tier` (price order: `good` = cheapest, `best` = priciest), `rank` (the engine’s: 1 = the pick), `name`, `brand`, `slug`, `image`, `brake`, `maxDin`, `why`. No price — price `skuId` live and deep-link it (`?skid=`). 404 with `reason` (`system-ski`, `package`, `no-live-page`, `missing-waist`, `no-candidates`, `not-a-stocked-ski`) for a ski the engine gave no picks.
slug optional string <em>pdp_bindings sets</em>: The ski PDP slug, instead of `anchor`.
max optional integer default: 3 <em>pdp_bindings sets</em>: Picks returned, in engine rank order; capped at 6.
render optional string `html` returns a self-contained preview page instead of JSON.
fresh optional string Any value (Bandit’s inspect page sends a timestamp). Skips the Lambda’s warm copy of the artifact and answers `Cache-Control: no-store`, so the response reflects a build the moment it publishes. For operator tools only — a storefront call must never send it.

Example request

GET https://reco-cdn.cscbandit.com/dyn/v1/backcountry/bc-pdp-slate?anchor=ARMN3YL&max=6&floor=0.25

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

{
  "brand": "backcountry",
  "anchor": "ARMN3YL",
  "policy": "ranked",
  "floor": 0.25,
  "min": 3,
  "rung": "group",
  "generatedAt": "2026-08-27T10:12:03.412Z",
  "items": [
    { "styleid": "SALN0K3", "slug": "salomon-strive-gw-14-ski-binding-2027", "url": "https://www.backcountry.com/salomon-strive-gw-14-ski-binding-2027", "image": "https://content.backcountry.com/images/items/medium/SAL/SALN0K3/BLK.jpg", "brand": "Salomon", "name": "Strive GW 14 Ski Binding - 2027", "rung": "group", "why": "mounts your skis", "score": 0.81, "evidence": { "carts": 12, "lift": 9.4 }, "cellId": "group:Big Mountain Freeride Skis|Alpine Ski Bindings", "family": "binding", "fit": null, "q": 0.6, "relation": "complement", "judge": 4, "pad": false, "position": 1 }
  ],
  "count": 6,
  "padded": 0,
  "excluded": { "inCart": 0, "explicit": 0, "familyCap": 1, "floor": 2 },
  "timing_ms": { "total": 0.8 }
}

Errors

StatusWhen
400The kind’s required parameter is missing or malformed (a slate set with no `anchor`/`slug`, `floor` outside [0, 1], …).
404Unknown path, no sets registered for the brand (`reason: no-registry`), unknown set key (`unknown-set`), or the set is disabled (`disabled`); a slate set also 404s an anchor or slug the build did not cover; a bindings or skins set 404s a ski with no picks and says why (`reason`). Cached 60 s (registry misses) / for the cache TTL (anchor misses).
405Method other than GET/OPTIONS.
502The serving registry or the set’s artifact could not be read from S3 (a warm container keeps serving its last good copy instead).