Ads & Recommendations

Surface personalized product recommendations and sponsored placements in your storefront.

What you'll learn

  • The difference between organic and sponsored content
  • How to fetch organic recommendations (user top picks, cart toppers)
  • How to fetch sponsored product listings that blend paid placements into search results
  • How to fetch sponsored recommendations (sponsored top picks, sponsored cart toppers)
  • How to request frequently bought together products
  • The ad client data payload structure required by sponsored endpoints
  • How to distinguish organic vs sponsored products in the response

Prerequisites

  • A Store UUID (staging: e87437f2-3e35-4738-af5e-6307e368255c)
  • The store must have product recommendations enabled in its settings
  • For sponsored endpoints, the store needs a Surfside integration configured with account_id and site_id
  • cURL or any HTTP client

Organic vs Sponsored Content

The API provides two flavors of product recommendations:

Organic — Pure recommendations based on user behavior, popularity, and store configuration. Fetched via GET requests with no additional payload.

Sponsored — Recommendations that blend paid ad placements from the Surfside ad platform alongside organic results. Fetched via POST requests that include a client_data payload with browser, location, and session information.

The frontend automatically upgrades organic calls to sponsored when the store has the relevant feature toggle enabled (e.g., sponsoredProductsEnabled or productPlacementEnabled). As a consumer, you should always prefer the sponsored endpoint when ad integrations are active, falling back to the organic endpoint when they're not.


Organic Recommendations

User Top Picks

Personalized product suggestions for the current user. If no user is authenticated, returns store-level popular picks.

Endpoint: GET /api/v1/products/recommendations/user-top-picks Auth: jwt_optional_authenticated (works with or without a JWT)

Query Parameters

Parameter Type Description
limit integer Max products to return (default: 5, max: 50)
brand string Filter by brand slug
category string Filter by category slug
type string Filter by product type
tag string Filter by tag
delivery_type string One of: all, pickup, express, scheduled_delivery, kiosk
excludes string Product IDs to exclude
cart_total string Current cart total (for relevance tuning)
max_price string Max price filter
min_price string Min price filter

cURL

curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/recommendations/user-top-picks?limit=5" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

JavaScript (fetch)

const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c";
const BASE_URL = "https://ecom-api.staging.blaze.me";

const response = await fetch(
  `${BASE_URL}/api/v1/products/recommendations/user-top-picks?limit=5`,
  {
    headers: {
      "Content-Type": "application/vnd.api+json",
      Accept: "application/vnd.api+json",
      "X-Store": STORE_UUID,
    },
  },
);

const { data, meta } = await response.json();
console.log(`Got ${data.length} top picks`);

Cart Toppers

Products commonly added alongside items already in the cart. Same interface as user top picks.

Endpoint: GET /api/v1/products/recommendations/cart-toppers Auth: jwt_optional_authenticated

Accepts the same query parameters as user top picks.

Endpoint: GET /api/v1/products/recommended Auth: jwt_optional_authenticated

Note: This is a legacy endpoint. Prefer user-top-picks and cart-toppers for new integrations.


When ad integrations are active, the product listing endpoint switches from GET /api/v1/products to POST /api/v1/products/sponsored. This blends sponsored placements into the standard product grid.

Endpoint: POST /api/v1/products/sponsored Auth: jwt_optional_authenticated

The sponsored endpoint accepts all the same query parameters as the regular product listing (passed as URL query params), plus a JSON body containing the ad client data payload.

Sponsored products are interleaved with organic results — the response mixes them together so the product grid appears natural.

cURL

curl -X POST "https://ecom-api.staging.blaze.me/api/v1/products/sponsored?limit=20" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \
  -d '{
    "data": {
      "type": "recommendations",
      "attributes": {
        "url": "https://my-store.blaze.me/products",
        "screen": { "height": 1080, "width": 1920 },
        "navigator": {
          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
          "language": "en-US"
        },
        "mobile": false,
        "account_id": "your-surfside-account-id",
        "site_id": "your-surfside-site-id",
        "channel_id": "your-channel-id",
        "channel_type": "WEB",
        "zone_id": "zone-for-current-page",
        "client_ip": "203.0.113.42",
        "surfside_domain_id": "surf-domain-id-from-cookie",
        "session_id": "current-session-id",
        "store_location_data": {
          "coords": { "latitude": 34.0522, "longitude": -118.2437 },
          "zip": "90001",
          "city": "Los Angeles",
          "country": "US",
          "state": "CA"
        },
        "location_data": {
          "zip": "90210",
          "country": "US",
          "city": "Beverly Hills",
          "region": "CA",
          "utc_offset": -7,
          "timezone": "America/Los_Angeles",
          "coords": {
            "latitude": 34.0901,
            "longitude": -118.4065,
            "accuracy": 20
          }
        }
      }
    }
  }'

JavaScript (fetch)

const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c";
const BASE_URL = "https://ecom-api.staging.blaze.me";

const clientDataPayload = {
  data: {
    type: "recommendations",
    attributes: {
      url: window.location.href,
      screen: {
        height: window.screen.height,
        width: window.screen.width,
      },
      navigator: {
        user_agent: window.navigator.userAgent,
        language: window.navigator.language,
      },
      mobile: /Android|iPhone|iPad/i.test(navigator.userAgent),
      account_id: "your-surfside-account-id",
      site_id: "your-surfside-site-id",
      channel_id: "your-channel-id",
      channel_type: "WEB",
      zone_id: "zone-for-current-page",
      client_ip: "203.0.113.42",
      surfside_domain_id: null,
      session_id: "current-session-id",
      store_location_data: {
        coords: { latitude: 34.0522, longitude: -118.2437 },
        zip: "90001",
        city: "Los Angeles",
        country: "US",
        state: "CA",
      },
      location_data: {
        zip: "90210",
        country: "US",
        city: "Beverly Hills",
        region: "CA",
        utc_offset: -7,
        timezone: "America/Los_Angeles",
        coords: { latitude: 34.0901, longitude: -118.4065, accuracy: 20 },
      },
    },
  },
};

const response = await fetch(`${BASE_URL}/api/v1/products/sponsored?limit=20`, {
  method: "POST",
  headers: {
    "Content-Type": "application/vnd.api+json",
    Accept: "application/vnd.api+json",
    "X-Store": STORE_UUID,
  },
  body: JSON.stringify(clientDataPayload),
});

const { data, meta } = await response.json();
// Sponsored products have is_promoted: true and a product_placement_campaign_id
data.forEach((p) => {
  if (p.attributes.is_promoted) {
    console.log(`[SPONSORED] ${p.attributes.name}`);
  } else {
    console.log(`${p.attributes.name}`);
  }
});

When ad integrations are active, the frontend upgrades organic recommendation calls to their sponsored counterparts. Sponsored recommendations use POST and include the same client data payload.

Endpoint: POST /api/v1/products/recommendations/sponsored-user-top-picks Auth: jwt_optional_authenticated

Same query parameters as organic user top picks, plus the client data body.

Endpoint: POST /api/v1/products/recommendations/sponsored-cart-toppers Auth: jwt_optional_authenticated

Same query parameters as organic cart toppers, plus the client data body.

How the Frontend Decides

The frontend uses feature toggles to decide whether to call the organic or sponsored variant:

  1. Check if the store has productPlacementEnabled (for recommendations) or sponsoredProductsEnabled (for product listing)
  2. If enabled, gather client data (screen, navigator, location, Surfside cookie) via the https://g.surfside.io/enrich endpoint
  3. Call the sponsored POST endpoint with the client data payload
  4. If disabled or client data fails to load, fall back to the organic GET endpoint

Frequently Bought Together

Returns products commonly purchased alongside a specific product. This endpoint always uses POST with the client data payload (the ad platform determines both organic and sponsored recommendations).

Endpoint: POST /api/v1/products/recommendations/frequently-bought-together/{product_id} Auth: jwt_optional_authenticated

The product_id can be the product's numeric ID or slug.

cURL

curl -X POST "https://ecom-api.staging.blaze.me/api/v1/products/recommendations/frequently-bought-together/12345?limit=5" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \
  -d '{
    "data": {
      "type": "recommendations",
      "attributes": {
        "url": "https://my-store.blaze.me/products/12345",
        "screen": { "height": 1080, "width": 1920 },
        "navigator": {
          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
          "language": "en-US"
        },
        "mobile": false,
        "account_id": "your-surfside-account-id",
        "site_id": "your-surfside-site-id",
        "channel_id": "your-channel-id",
        "channel_type": "WEB",
        "zone_id": "zone-for-product-page",
        "client_ip": "203.0.113.42",
        "surfside_domain_id": null,
        "session_id": "current-session-id",
        "store_location_data": {
          "coords": { "latitude": 34.0522, "longitude": -118.2437 },
          "zip": "90001",
          "city": "Los Angeles",
          "country": "US",
          "state": "CA"
        },
        "location_data": {
          "zip": "90210",
          "country": "US",
          "city": "Beverly Hills",
          "region": "CA",
          "utc_offset": -7,
          "timezone": "America/Los_Angeles",
          "coords": { "latitude": 34.0901, "longitude": -118.4065, "accuracy": 20 }
        }
      }
    }
  }'

JavaScript (fetch)

const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c";
const BASE_URL = "https://ecom-api.staging.blaze.me";
const productId = "12345";

const response = await fetch(
  `${BASE_URL}/api/v1/products/recommendations/frequently-bought-together/${productId}?limit=5`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/vnd.api+json",
      Accept: "application/vnd.api+json",
      "X-Store": STORE_UUID,
    },
    body: JSON.stringify(clientDataPayload), // same payload structure as above
  },
);

const { data, meta } = await response.json();
console.log(`Recommendation ID: ${meta.recommendation_id}`);
data.forEach((p) => console.log(p.attributes.name));

Response

The response includes a recommendation_id in the meta, which can be used for tracking/attribution:

{
  "meta": {
    "recommendation_id": "rec-abc-123"
  },
  "data": [
    {
      "id": "67890",
      "type": "products",
      "attributes": {
        "name": "Rolling Papers",
        "is_promoted": true,
        "product_placement_campaign_id": "campaign-xyz",
        "extras": { ... }
      }
    },
    {
      "id": "67891",
      "type": "products",
      "attributes": {
        "name": "Grinder",
        "is_promoted": false,
        "product_placement_campaign_id": null
      }
    }
  ]
}

Ad Client Data Payload

All sponsored endpoints require a JSON body with the following structure:

{
  "data": {
    "type": "recommendations",
    "attributes": {
      "url": "string — current page URL",
      "screen": {
        "width": "integer — screen width in pixels",
        "height": "integer — screen height in pixels"
      },
      "navigator": {
        "user_agent": "string — browser user agent",
        "language": "string — browser language (e.g., 'en-US')"
      },
      "mobile": "boolean — whether the client is a mobile device",
      "account_id": "string — Surfside account ID from store integration",
      "site_id": "string — Surfside site ID from store integration",
      "channel_id": "string — Surfside channel ID",
      "channel_type": "string — 'WEB' or 'KIOSK'",
      "zone_id": "string — Surfside ad zone/placement ID for the current page",
      "client_ip": "string — client IP address",
      "surfside_domain_id": "string|null — Surfside domain cookie value",
      "session_id": "string|null — current browsing session ID",
      "store_location_data": {
        "coords": {
          "latitude": "float|null",
          "longitude": "float|null"
        },
        "zip": "string|null",
        "city": "string|null",
        "country": "string|null",
        "state": "string|null"
      },
      "location_data": {
        "zip": "string|null",
        "country": "string|null",
        "city": "string|null",
        "region": "string|null",
        "utc_offset": "integer|null",
        "timezone": "string|null",
        "coords": {
          "latitude": "float|null",
          "longitude": "float|null",
          "accuracy": "integer|null"
        }
      }
    }
  }
}

Where to get the values

  • screen, navigator, mobile — From the browser's window.screen and window.navigator APIs
  • account_id, site_id — From the store's Surfside site integration (GET /api/v1/store/site/integrations/surfside, keys key_1 and key_2)
  • channel_id — A constant for your Surfside channel
  • zone_id — The Surfside ad placement ID mapped to the current page/route
  • client_ip — Fetched from the Surfside enrich endpoint (https://g.surfside.io/enrich)
  • location_data — Also returned by the Surfside enrich endpoint
  • store_location_data — From the store's address and coordinates (returned by GET /api/v1/store)
  • surfside_domain_id — A cookie set by the Surfside tracking pixel
  • session_id — Your application's session identifier

Distinguishing Organic vs Sponsored Results

In the response, every product includes two fields that identify sponsored placements:

  • is_promoted (boolean) — true if the product is a paid/sponsored placement, false or null for organic results
  • product_placement_campaign_id (string|null) — The ad campaign ID that sponsored this placement. null for organic results
  • extras (object|null) — Additional ad metadata from the placement platform. null for organic results

Use these fields to:

  • Render a "Sponsored" badge on promoted products
  • Track ad impressions and clicks for attribution
  • Report conversions back to the ad platform
function isSponsored(product) {
  return product.attributes.is_promoted === true;
}

// Render products with sponsorship indicator
data.forEach((product) => {
  const label = isSponsored(product) ? "[Sponsored] " : "";
  console.log(`${label}${product.attributes.name}`);
});

Endpoint Summary

Endpoint Method Purpose
/api/v1/products/recommendations/user-top-picks GET Organic personalized picks
/api/v1/products/recommendations/cart-toppers GET Organic cart add-on suggestions
/api/v1/products/sponsored POST Sponsored product listing (replaces GET /products when ads are active)
/api/v1/products/recommendations/sponsored-user-top-picks POST Sponsored personalized picks
/api/v1/products/recommendations/sponsored-cart-toppers POST Sponsored cart add-on suggestions
/api/v1/products/recommendations/frequently-bought-together/{product_id} POST Products commonly bought together
/api/v1/products/recommended GET Legacy recommended (deprecated)

What's Next?