Server-Side Recommendations

The Node SDK has the same recommendations API as the browser snippet. Call get() with a recipe, get back plain RecProduct[] data, and render it however you like. Use it for server-rendered pages, transactional emails, or any backend service that needs product recommendations.

You need: @avsbhq/node installed and initialized with your SDK key (see SDK Installation), and at least one enabled recipe with a completed run.

Find your recipe's handle in its Use this recipe panel, in the "On a server (Node SDK)" section:

  1. Copy the Feed ID shown here.
  2. Pass it as the recipe value in your server code, exactly like this example.

The smallest working call

TypeScript
import { AvsbServer, type RecProduct } from '@avsbhq/node'const client = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY ?? '' })await client.onReady()const result = await client.recs.get({  recipe: 'similar-products',       // the recipe handle (its output-dataset slug)  context: { seed: 'SKU-123' },     // or { seeds: [...] }: up to 3  maxItems: 6,  excludeOutOfStock: true,          // optional; defaults to the recipe's setting})if (result.served) {  // result.items is RecProduct[]: the same shape as the browser API  const html = result.items.map((product: RecProduct) => `<li>${product.title}</li>`).join('')}
TypeScript16 lines

The resolve happens at the edge, using the current catalog overlay. That means price, compareAtPrice (integer minor units plus currency), and availability are always live, even if the underlying list was built the night before.

The one mistake people make here: skipping the served check

get() never throws. The SDK might not be ready. The recipe might be unknown. The request might time out (10 seconds) or fail some other way. In every one of these cases, you get { items: [], served: false } back instead of an error. Always check result.served before you render. Skip that check and you'll render an empty list as if it were a real (empty) recommendation, instead of falling back to something else.

The full result shape:

TypeScript
interface RecsResult {  items: RecProduct[]  served: boolean  recipe: string  source: string | null        // slug of the chain step that served  fallbackStep: number | null  // 0 = primary, 1+ = fallback  degraded: boolean            // true when the lookup itself failed}
TypeScript8 lines

Attribution: opt-in on the server

On the browser, a running experiment automatically attaches attribution to options.recs calls. A variation is the specific version of a page a visitor sees: control, or one of the challengers. A vs B already knows which one is active, because it's running the experiment itself.

A server doesn't have that context, so attribution is explicit and opt-in instead. Pass an attribution object and get() records one rec:impression event with those ids. Leave it out and no event fires.

TypeScript
// The three ids come from your own bucketing call, and the seed from the page:const visitorId = 'the-visitor-id-you-bucketed'const experimentId = '300021'const variationId = '2'const sku = 'SKU-123'const attributed = await client.recs.get({  recipe: 'similar-products',  context: { seed: sku },  attribution: { visitorId, experimentId, variationId },})
TypeScript11 lines

Record clicks the same way, from your click-through endpoint or redirect handler:

TypeScript
const productId = 'SKU-456' // the product the shopper clickedclient.recs.trackClick(productId, {  visitorId,  experimentId,  variationId,  recipe: 'similar-products',  // groups click-through per recipe on results  position: 2,                 // optional, 1-based})
TypeScript9 lines

These events ride the SDK's existing batched tracker, so there's nothing new to set up. Revenue attaches through the client.trackPurchase(visitorId, order) call you already use, joined by visitorId. Know a product's canonical product key? Pass it as productKey on the order item. That lets the order line join recommendation analytics directly, instead of A vs B having to resolve it from the SKU itself.

Types

Everything is exported from the package root:

TypeScript
import type {  RecsClient,  RecsGetRequest,  RecsResult,  RecProduct,  RecsAttribution,} from '@avsbhq/node'
TypeScript7 lines

Freshness and failure posture

  • The recipe registry ships inside the SDK's datafile: the small JSON file your server downloads, listing every live experiment, flag, and recipe for your project. Edit a recipe's chain, merchandising, or stock filter, and every server picks it up on its next datafile poll. No redeploy needed.
  • Before the first datafile arrives, get() returns { served: false } instead of blocking your server's startup.
  • degraded: true means the edge lookup itself failed (a timeout or network error). That's worth alerting on if it keeps happening. A plain served: false with degraded: false is normal: it just means there's honestly nothing to recommend for that seed right now.
Was this helpful?