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.
https://addon-reco.cscbandit.comhttps://reco-cdn.cscbandit.com/{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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Missing `sku` or `category`. |
404 | Unknown brand prefix (not in brands.ts). |
405 | Method other than GET/OPTIONS. |
500 | No data source configured for the brand. |
/{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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Missing `sku`. |
404 | Unknown brand prefix. |
405 | Method other than GET/OPTIONS. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Missing `sku` or `slug`. |
404 | Unknown path. |
405 | Method other than GET/OPTIONS. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Missing required params or invalid `gap`. |
404 | Unknown path. |
405 | Method other than GET/OPTIONS. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Missing `sku` or `slug`. |
404 | Unknown path. |
405 | Method other than GET/OPTIONS. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
404 | No look computed for the styleid. |
405 | Method other than GET/OPTIONS. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Missing `sku` or `slug`. |
404 | Unknown path. |
405 | Method other than GET/OPTIONS. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
404 | Unknown path. |
405 | Method other than GET/OPTIONS. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | No `sku`, or more than 50 SKUs. |
405 | Method other than GET/OPTIONS. |
502 | DMP query failed. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | No `sku`, or more than 50 SKUs. |
405 | Method other than GET/OPTIONS. |
502 | DMP query failed. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
405 | Method other than GET/OPTIONS. |
502 | Top-converting artifact unavailable in S3. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
405 | Method other than GET/OPTIONS. |
502 | Top-converting artifact unavailable in S3. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
405 | Method other than GET/OPTIONS. |
502 | Cart-addons artifact unavailable in S3. |
/{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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Missing/invalid anchor, an unknown policy, floor outside [0, 1], or min outside [0, 24]. |
404 | No slate for this anchor (cacheable, max-age=300). |
405 | Method other than GET/OPTIONS. |
502 | Slate manifest unavailable in S3. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | Neither `anchor` nor `slug` given, or `anchor` is not a styleid. |
502 | The data file could not be read from S3 (no warm copy). |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
405 | Method other than GET/OPTIONS. |
502 | Cart-addons artifact unavailable in S3. |
/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
| Param | Type | Notes | |
|---|---|---|---|
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
| Status | When |
|---|---|
400 | The kind’s required parameter is missing or malformed (a slate set with no `anchor`/`slug`, `floor` outside [0, 1], …). |
404 | Unknown 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). |
405 | Method other than GET/OPTIONS. |
502 | The serving registry or the set’s artifact could not be read from S3 (a warm container keeps serving its last good copy instead). |