Initializing Surfside Ads
Before you can request ads, you need to initialize the Surfside SDK for the current request. How you do that depends on which integration method you're using.
- Headless
- React Components
- React Hooks
- Web Components
Surfside Ads is broken up into 4 kinds of functions:
- Loading dependencies — Surfside Ads makes a couple network requests in order to load clientside configurations, geo-location information, and other settings. By default, Surfside Ads requests dependencies from the Surfside CDN, but this behavior can be overridden to implement local caching, mock services, or custom configuration workflows.
- Initializing Services — Surfside Ads manages its own dependency injection, so you must call the Surfside Ads Initialization functions at the correct time. Services are initialized based on the current site's metadata and the current page URL, so it is important that you re-inject dependencies in an optimized way.
- Sending Bid Requests — Surfside Ads provides utilities for generating OpenRTB 2.6 bid requests, including Surfside's custom extensions for supporting product carousel ads.
- Parsing Bid Responses — Surfside Ads performs macro expansion on bid responses and parses through the custom Surfside extensions to return data which is more useful than the standard OpenRTB 2.6 Bid Response. For carousel bid responses, Surfside Ads sends an HTTP request to the Surfside CDN in order to load product data. This behavior can be overridden if you want to use your own product database.
Initialization loads your account configuration, resolves targeting context, and wires together the services you'll use; returning a single API object that's ready to go.
Call LoadSurfAdsCoreHeadless once per request to get that object.
Installation
The headless workflow ships in @surfside/ads-core. It has no browser dependency and runs on Node 18+, Bun, or Deno.
npm install @surfside/ads-core
TypeScript types are bundled — there is no separate @types package. The package publishes both ESM and CommonJS builds, so import and require both work.
Quick start
import { LoadSurfAdsCoreHeadless } from '@surfside/ads-core';
const adsRes = await LoadSurfAdsCoreHeadless({
accountId: 'your-account-id',
siteId: 'your-site-id',
channelId: 'your-channel-id',
locationId: 'your-location-id',
userId: 'user-123', // optional
ip: req.ip,
userAgent: req.headers['user-agent'] ?? '',
url: 'https://example.com/some-page',
});
if (!adsRes.ok) {
console.error(adsRes.error);
return;
}
const ads = adsRes.value;
Configuration reference
LoadSurfAdsCoreHeadless takes a single config object. Every field it accepts is listed here.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
accountId | string | Yes | — | Your Surfside account ID |
siteId | string | Yes | — | The site ID configured in your account |
channelId | string | Yes | — | The channel ID for this placement context |
locationId | string | Yes | — | The location ID for this placement context |
userId | string | No | undefined | An identifier for the current user, used for personalization |
ip | string | Yes | — | The IP address of the end user |
userAgent | string | Yes | — | The user-agent string of the end user's browser |
url | string | Yes | — | The absolute URL of the page being rendered |
logging | boolean | No | false | Enable debug logging to the console |
overrides | object | No | undefined | Replace the SDK's internal services or supply dependencies yourself. See Overrides |
Account identifiers
accountId, siteId, channelId, and locationId are all required and all come from your Surfside account setup.
| Field | What it identifies |
|---|---|
accountId | Your Surfside account. Fixed for your organization. |
siteId | The property being monetized. Determines which publisher configuration is loaded — product card templates, clickthrough templates, category mappings, and site settings. |
channelId | The surface the placement lives on, so the same site can distinguish web, app, kiosk, and so on. |
locationId | The store, dispensary, menu, or location context the page represents. Used for targeting and for the sponsored product feed. |
accountId and siteId together resolve your publisher configuration. If either is wrong, initialization fails with a MissingConfigurationError that includes a link to the matching configuration in the Surfside app.
locationId is checked against the excludedLocations list in your publisher configuration. If the location is excluded, initialization fails with a LocationExcludedError — this is a deliberate configuration outcome, not a fault. Treat it as "no ads for this location" and render your unsponsored experience.
Request context: ip, userAgent, and url
These three fields are the main difference between headless and browser integrations. In a browser, the SDK reads them automatically from window and navigator. On a server there is no browser context, so you forward them from the incoming request.
ip: req.ip,
userAgent: req.headers['user-agent'] ?? '',
url: req.headers.referer ?? `${req.protocol}://${req.hostname}${req.originalUrl}`,
url must be an absolute, parseable URL. It is parsed into the page context the SDK uses for macro expansion in creative and clickthrough templates, and it is sent as the site context on the bid request. A relative path or a malformed string fails initialization with a NotAUrlError.
Use the URL of the page the user is viewing, not the URL of your API endpoint. If your ad service is called from a rendering server, forward the page URL explicitly — req.headers.referer is a reasonable fallback but is not always present.
userAgent drives two things:
- Device detection. The SDK parses make, model, OS, OS version, browser, and CPU architecture out of the string and puts them on the OpenRTB
deviceobject. A missing or generic user-agent means the bidder sees an unknown device, which reduces the pool of bids that can match. - Bot filtering. See Bot filtering below.
ip is the end user's IP address. See Geo resolution in headless for exactly how it is used today and how to control geo targeting from a server.
userId
An identifier for the current end user, used for personalization. It should be stable for a given user across requests — a hashed customer ID or a loyalty ID works well.
userId unlocks the personalized recommenders. Without it:
generateForYoufalls back to top productsgeneratePreviousPurchasesfalls back to top products
Sponsored product carousels and banner/video creatives work the same either way. Omit userId entirely for anonymous traffic rather than passing an empty string or a placeholder.
logging
Set logging: true to have the SDK write its initialization and request activity to the console — configuration loading, geo resolution, service wiring, bid activity, and template evaluation.
const adsRes = await LoadSurfAdsCoreHeadless({
// ...identifiers and request context
logging: true,
});
Output is prefixed with [SURF] and is plain-text (not the coloured browser format), so it is safe to pipe into a log aggregator. It is verbose — use it in development and when diagnosing an integration, not in steady-state production.
Defaults applied for you
Because there is no browser to read from, LoadSurfAdsCoreHeadless fills in a small number of values on your behalf. These are fixed and cannot be set through the headless config:
| Value | Fixed to | Effect |
|---|---|---|
| Screen size | 1920 × 1080 | Reported as the device's screen dimensions on the bid request |
| Mobile flag | false | The device is always described as non-mobile |
| Language | en-US | Reported as the device language on the bid request |
If your integration serves a materially mobile audience, or a non-English one, and you need the bid request to reflect that, supply the device object yourself through overrides.dependencies.device. See Overrides.
Configuration caching
Your publisher configuration and the global category configuration are fetched over the network the first time you initialize, then cached in-process for 5 minutes:
- The publisher configuration cache is keyed on
accountId+siteId, so a multi-tenant server caches each account separately. - The category configuration is global and shared across all accounts in the process.
This means initialization is cheap after the first call, and it means changes you make in the Surfside app can take up to 5 minutes to appear on a long-running server. Restart the process to pick them up immediately.
The caches live in module scope, so they are shared by every LoadSurfAdsCoreHeadless call in the same process and are not shared across processes or instances. If you want different caching behaviour — a shared Redis cache across your fleet, or no caching at all in a test environment — replace the loaders through overrides.services.
Geo resolution in headless
Geo is resolved by calling Surfside's geo enrichment service during initialization, and the result populates device.geo and device.ip on every bid request built from the returned API object.
In a headless integration that call is made from your server, so it resolves your server's network location rather than the end user's. On a single-region deployment, every request will geo-target to your datacenter.
Geo is not required for bidding — it is a targeting signal — so a failed or absent geo lookup does not fail initialization. It does reduce the number of bids that match.
If geo-targeting accuracy matters for your integration, supply the geo context yourself with overrides.dependencies.geo. The SDK will use exactly what you pass and skip the lookup entirely:
import { LoadSurfAdsCoreHeadless } from '@surfside/ads-core';
const adsRes = await LoadSurfAdsCoreHeadless({
accountId: 'your-account-id',
siteId: 'your-site-id',
channelId: 'your-channel-id',
locationId: 'your-location-id',
ip: req.ip,
userAgent: req.headers['user-agent'] ?? '',
url: pageUrl,
overrides: {
dependencies: {
geo: {
ip: req.ip,
country: 'US',
region: 'MA',
city: 'Boston',
zip: '02110',
coords: { latitude: 42.3554, longitude: -71.0605, accuracy: 50 },
utcoffset: -300,
},
},
},
});
Most CDNs and load balancers give you these values on the request already — Cloudflare exposes cf-ipcountry and friends, AWS CloudFront exposes CloudFront-Viewer-Country and CloudFront-Viewer-City, and Vercel exposes x-vercel-ip-* headers. Mapping those onto the geo object is usually a few lines and gives you per-user accuracy without an extra network call.
Talk to your Surfside contact if you want geo resolved server-side by Surfside instead.
Bot filtering
The SDK checks userAgent against a list of known crawlers, scrapers, and monitoring agents — Googlebot, bingbot, Ahrefs, Screaming Frog, security scanners, and a long tail of AI crawlers among them.
When the user-agent matches, the SDK does not send a bid request or call the recommender at all. Instead:
| Call | Behaviour for a matched bot |
|---|---|
requestBids | Returns Err(NoBidsError) without a network call |
getRecommendedProduct | Returns Err(NoRecommendationsError) |
generateTopProducts | Yields nothing |
generateForYou | Yields nothing |
generatePreviousPurchases | Yields nothing |
Initialization itself still succeeds — the filtering happens at request time, not init time. This keeps crawler traffic out of your bid volume and your measurement, and it means SEO crawls of your pages will render without ads.
If you are testing your integration with curl or a synthetic monitor and consistently see no bids, check the user-agent you are sending first.
Overrides
overrides is the escape hatch. It lets you replace the SDK's internal services or hand it dependencies you have already resolved. Most integrations never need it; it exists for local caching, mock services in test environments, and custom data sources.
There are two kinds.
overrides.dependencies
Supply an already-resolved value and the SDK skips the work of producing it.
| Key | Type | Use it to |
|---|---|---|
configuration | ICompiledPublisherConfiguration | Supply a publisher configuration directly, bypassing the network fetch and the compile step entirely |
geo | IGeoContext | Supply geo context from your edge or CDN. See Geo resolution in headless |
device | IDevice | Supply an OpenRTB device object, overriding the defaults for screen size, mobile flag, and language |
handlebars | typeof Handlebars | Supply a preconfigured Handlebars runtime with your own helpers registered |
logger | ILogger | Currently unused by the headless entry point — use the logging flag instead |
overrides.services
Replace a function the SDK would otherwise call. Each entry is an override factory: a function that receives the current config and the SDK's original implementation, and returns a Result containing the replacement.
type OverrideFactory<T> = (config, original: T) => Result<T>;
Wrapping original rather than replacing it outright is usually what you want — that way you inherit the SDK's behaviour and add to it. If your factory returns an error Result, the SDK logs a warning and silently falls back to the original implementation, so a broken override degrades rather than breaks.
| Key | Replaces |
|---|---|
loadConfigurationFile | Fetching the publisher configuration file for an account and site |
loadGlobalCategoryConfiguration | Fetching the global category configuration |
requestBids | Sending the bid request to the Surfside bidder |
startRequest | The OpenRTB 2.6 bid request builder |
mapCategories | Mapping your categories to Surfside canonical categories |
getAllSponsoredProducts | Resolving sponsored products from a native creative ID |
getAllSponsoredBrandProducts | Resolving sponsored brand products from a native creative ID |
getRecommendedProduct | Resolving product detail from a product ID |
generateTopProducts | The top-products recommender |
generateForYou | The personalized recommender |
generatePreviousPurchases | The previous-purchases recommender |
getDynamicZone | Fetching dynamic zone definitions |
buildAllSponsoredProductCardResponses | Building product card responses from a bid response |
generateSponsoredProductCardResponse | Generating a single product card response |
evaluateProductCardTemplate | Handlebars evaluation of the product card template |
evaluateClickthroughTemplate | Handlebars evaluation of the clickthrough URL template |
trackerService | The impression and click tracker service |
createTelemetryCollector | The telemetry collector |
generateCategories | Deprecated. Use mapCategories |
Example: serving product data from your own catalogue
By default the SDK fetches product detail from the Surfside product feed. If you already have a product catalogue in-process, wrap the original and serve from it:
const adsRes = await LoadSurfAdsCoreHeadless({
// ...identifiers and request context
overrides: {
services: {
getRecommendedProduct: (config, original) => Ok(
async (productId: string) => {
const local = await catalogue.find(productId);
return local !== undefined
? Ok(local)
: original(productId); // fall back to the Surfside feed
}
),
},
},
});
Example: sharing the configuration cache across your fleet
The built-in configuration cache is per-process. On a fleet of instances that means each one fetches its own copy. Point the loader at a shared cache instead:
overrides: {
services: {
loadConfigurationFile: (config, original) => Ok(
async (accountId: string, siteId: string) => {
const key = `surfside:config:${accountId}:${siteId}`;
const cached = await redis.get(key);
if (cached !== null) {
return Ok(JSON.parse(cached));
}
const res = await original(accountId, siteId);
if (res.ok) {
await redis.set(key, JSON.stringify(res.value), 'EX', 300);
}
return res;
}
),
},
}
The Result pattern
LoadSurfAdsCoreHeadless returns a Result. Before using the returned value, always check whether the call succeeded:
const adsRes = await LoadSurfAdsCoreHeadless({ /* ... */ });
if (!adsRes.ok) {
console.error(adsRes.error); // handle the error however you want
return;
}
const ads = adsRes.value;
A Result is a discriminated union, so checking .ok narrows the type for you:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
Every async function in the Surfside SDK follows this pattern and returns a Result rather than throwing. Network failures, malformed responses, and configuration problems all arrive as values you can branch on. You do not need to wrap SDK calls in try/catch.
Initialization errors
| Error | Cause | What to do |
|---|---|---|
NotAUrlError | url was not a parseable absolute URL | Fix the URL you're forwarding. Check for relative paths and empty strings |
MissingConfigurationError | No publisher configuration exists for that accountId + siteId | Check the identifiers. The error message includes a direct link to the configuration in the Surfside app |
SurfsideAdsDisabledError | Ads are disabled for this site in your publisher configuration | Expected when ads are intentionally turned off. Render your unsponsored experience |
LocationExcludedError | locationId is in the site's excluded locations list | Expected for excluded locations. Render your unsponsored experience |
SurfsideAdsDisabledError and LocationExcludedError are configuration outcomes rather than faults. Serving a 503 for them will make your monitoring noisy — handle them as "no ads here" and move on. A useful shape:
import {
LoadSurfAdsCoreHeadless,
SurfsideAdsDisabledError,
LocationExcludedError,
} from '@surfside/ads-core';
const adsRes = await LoadSurfAdsCoreHeadless({ /* ... */ });
if (!adsRes.ok) {
const expected =
adsRes.error instanceof SurfsideAdsDisabledError ||
adsRes.error instanceof LocationExcludedError;
if (expected) {
return res.status(204).send(); // no ads for this context
}
logger.error('Surfside init failed', adsRes.error);
return res.status(503).json({ error: adsRes.error.message });
}
A failed geo lookup does not fail initialization — see Geo resolution in headless.
What you get back
The object in adsRes.value is a collection of functions already configured with your account-specific data. You don't pass accountId, siteId, or the other identifiers on every call; they're baked in.
const ads = adsRes.value;
// These are ready to use, no extra config needed
const bids = await ads.requestBids(request);
const banner = ads.buildBannerResponseCore(bid);
This is why you call a function to get a bunch of functions: LoadSurfAdsCoreHeadless does the setup work once — loading configuration, resolving geo context, wiring services together — and returns an object ready to use for the lifetime of that request.
Building and sending bid requests
| Member | Signature | Description |
|---|---|---|
startRequest | (zoneId: string) | Starts an OpenRTB 2.6 bid request builder. See Building Bid Requests |
startNative | () | Starts an empty OpenRTB Native 1.2 request builder |
defaultNative | () | A native request builder pre-filled with the standard Surfside product card assets |
requestBids | (request: IBidRequest) | Sends a bid request and returns the bid response |
Building creatives from a bid response
| Member | Signature | Description |
|---|---|---|
buildBannerResponseCore | (bid: IBid) | Builds a single banner from a bid |
buildVideoResponseCore | (bid: IBid) | Builds a single video (VAST) from a bid |
buildAllBannersCore | (bidResponse: IBidResponse) | Builds every banner in a bid response |
buildAllVideosCore | (bidResponse: IBidResponse) | Builds every video in a bid response |
buildAllResponsesCore | (bidResponse: IBidResponse) | Builds every creative of every type in a bid response |
buildAllSponsoredProductCardResponses | (bidResponse: IBidResponse) | Builds every sponsored product card in a bid response |
generateSponsoredProductCardResponse | (bid: IBid) | Builds a single sponsored product card |
Carousels
Carousels are lazy. Each builder returns a state object with a getProduct() async generator; products are fetched as you consume them, and the carousel re-requests bids when it runs out. Take only as many as you intend to render.
| Member | Signature | Description |
|---|---|---|
buildSponsoredCarousel | (request: IBidRequest) | Sponsored products only, refilling from the bidder as it is consumed |
buildRecommendedCarousel | (recommender: PublicRecommenderType, category?: string) | Recommended products only |
buildHybridCarousel | (recommender: PublicRecommenderType, request: IBidRequest) | Leads with sponsored products, fills with recommendations |
buildSimpleCarousel | (bid: IBid) | A flat, non-refilling carousel built from a single bid you already have |
buildSponsoredCarouselFromInitialBid | (request: IBidRequest, initialBid: IBid) | Sponsored carousel seeded with a bid you already have — used by dynamic zones |
buildHybridCarouselFromInitialBid | (recommender, request, initialBid) | Hybrid carousel seeded with a bid you already have — used by dynamic zones |
buildAllSimpleCarousels | (bidResponse: IBidResponse) | Every simple carousel in a bid response, built at once |
PublicRecommenderType is an object with a single type field: { type: 'top-products' }, { type: 'for-you' }, or { type: 'past-purchases' }.
See Carousels.
Recommendations and product data
| Member | Signature | Description |
|---|---|---|
generateTopProducts | (category?: string) | Async generator of the most popular products on the site |
generateForYou | (category?: string) | Async generator personalized to the userId you initialized with, falling back to top products |
generatePreviousPurchases | (category?: string) | Async generator based on the user's purchase history, falling back to top products |
getAllSponsoredProducts | (nativeId: string, categories?: string[]) | Resolves the products behind a native creative ID |
getRecommendedProduct | (productId: string) | Resolves product detail for a product ID |
The userId you passed at initialization is baked into the personalized recommenders — you do not pass it again per call. All three are async generators, so they are consumed with for await, and all three are subject to bot filtering.
Dynamic zones
| Member | Signature | Description |
|---|---|---|
getDynamicZone | (zoneId: string) | Fetches a dynamic zone definition |
buildDynamicZoneRequest | (zoneId: string, zone: IDynamicZone, categories?: string[]) | Builds the bid request for a dynamic zone |
Context, configuration, and tracking
| Member | Signature | Description |
|---|---|---|
configuration | property | The compiled publisher configuration for this site |
getConfiguration | () | The same configuration, as a Result |
geo | property | The resolved geo context, or undefined |
getGeo | () | The same geo context, as a Result |
device | property | The resolved OpenRTB device object, or undefined |
getDevice | () | The same device object, as a Result |
mapCategories | (categories: string[]) | Maps your categories to Surfside canonical categories |
generateCategories | (categories: string[]) | Deprecated. Alias of mapCategories |
evaluateProductCardTemplate | (product, clickthrough?) | Renders the site's product card template |
evaluateClickthroughTemplate | (product) | Renders the site's clickthrough URL template |
trackers | property | Service that builds impression and click trackers for a product |
handlebars | property | The Handlebars runtime used for template evaluation |
logger | property | The logger in use, if logging was enabled |
configuration, geo, and device are plain properties resolved at initialization; the get* variants return the same data wrapped in a Result and exist so that the same code path works in browser and headless integrations.
Lifecycle
Initialize once per request, not once per process. The returned object captures request-scoped context — the geo, device, user, and page URL for that specific visitor. Holding one across requests will attribute one user's context to another's ads and measurement.
Initializing per request is cheap. The two network calls that initialization makes — publisher configuration and category configuration — are cached in-process for 5 minutes, so in steady state initialization does no network I/O at all and costs roughly a Handlebars compile.
Within a single request, initialize once and reuse the object for every placement on the page. A single bid request can carry every impression opportunity at once, so one initialization typically serves the whole page.
Complete example
An Express handler serving a banner, with the full initialization path:
import express from 'express';
import {
LoadSurfAdsCoreHeadless,
SurfsideAdsDisabledError,
LocationExcludedError,
} from '@surfside/ads-core';
const app = express();
app.get('/ads/banner/:zoneId', async (req, res) => {
const { zoneId } = req.params;
const { accountId, siteId, channelId, locationId } = req.query as Record<string, string>;
const adsRes = await LoadSurfAdsCoreHeadless({
accountId,
siteId,
channelId,
locationId,
userId: req.query.userId as string | undefined,
ip: req.ip ?? '127.0.0.1',
userAgent: req.headers['user-agent'] ?? '',
url: req.headers.referer ?? `${req.protocol}://${req.hostname}${req.originalUrl}`,
logging: process.env.NODE_ENV !== 'production',
});
if (!adsRes.ok) {
const expected =
adsRes.error instanceof SurfsideAdsDisabledError ||
adsRes.error instanceof LocationExcludedError;
return expected
? res.status(204).send()
: res.status(503).json({ error: adsRes.error.message });
}
const ads = adsRes.value;
const bidsRes = await ads.requestBids(
ads.startRequest(zoneId).withBanner(4, 1).build()
);
if (!bidsRes.ok) {
return res.status(204).send(); // no bids, or the request was from a bot
}
const bid = bidsRes.value.seatbid?.[0]?.bid?.[0];
if (bid === undefined) {
return res.status(204).send();
}
const bannerRes = ads.buildBannerResponseCore(bid);
if (!bannerRes.ok) {
return res.status(204).send();
}
res.setHeader('Content-Type', 'text/html');
res.send(bannerRes.value.adm);
});
app.listen(3000);
Next steps
- Building Bid Requests — cover every placement on a page in one request
- Banners — request options and response fields for banner creatives
- Videos — VAST responses
- Carousels — sponsored, recommended, and hybrid product carousels
The React Components workflow gives you ready-to-use components backed by the Surfside SDK. Drop them into your component tree and they request, render, and track ads automatically — no manual bid requests or response parsing required. The package is @surfside/ads-react, and it requires React 17 or later.
There are two layers:
- Provider —
SurfsideProvider2initializes the SDK and makes it available to all components in the tree. You set it up once at the root of the relevant subtree. - Components —
SurfsideBanner,SurfsideVideo,SurfsideCarousel, andSurfsideDynamicZoneeach handle their own fetching, rendering, and tracking lifecycle independently.
Before any Surfside component can request ads, the SDK needs to be initialized. Wrap your component tree with SurfsideProvider2 and pass your account identifiers. The provider handles initialization once and makes the SDK available to all descendant components through context.
import { SurfsideProvider2 } from '@surfside/ads-react';
export const App = () => (
<SurfsideProvider2
default={{
accountId: 'your-account-id',
siteId: 'your-site-id',
channelId: 'your-channel-id',
locationId: 'your-location-id',
}}
>
{/* your app */}
</SurfsideProvider2>
);
Props
| Prop | Type | Required | Description |
|---|---|---|---|
default.accountId | string | Yes | Your Surfside account ID |
default.siteId | string | Yes | The site ID configured in your account |
default.channelId | string | Yes | The channel ID for this placement context |
default.locationId | string | No | The location ID. Can be omitted and set later — see below |
default.userId | string | No | An identifier for the current user, used for personalization |
logger | ILogger | No | A logger instance. Pass InitLogger() from @surfside/ads-core to enable console output |
config.debug | boolean | No | Enables default console logging without a custom logger |
config.mock | boolean | No | Uses mock services instead of live Surfside APIs. Useful in development |
config.simulateNoResponse | number | No | When mocking, the probability (0–1) that the mock bidder returns no bids |
config.simulateTimeout | number | No | When mocking, artificial network latency in milliseconds |
onError | (error: ErrResult) => void | No | Called whenever any Surfside service returns an error |
Setting locationId dynamically
locationId identifies which store, menu, or location context the page represents. If this value isn't known when the provider mounts — for example, if it depends on a route parameter or a user selection — you can omit it from default and set it later using setLocationId from the context.
The SDK will not initialize until a locationId is set.
import { useSurfsideContext } from '@surfside/ads-react';
const LocationSetter = ({ locationId }: { locationId: string }) => {
const { setLocationId } = useSurfsideContext();
useEffect(() => {
setLocationId(locationId);
}, [locationId]);
return null;
};
Place <LocationSetter locationId={currentLocationId} /> anywhere inside SurfsideProvider2.
Development mode
Pass config={{ mock: true }} to use mock services and keep real network calls out of your development environment:
<SurfsideProvider2
default={{
accountId: 'your-account-id',
siteId: 'your-site-id',
channelId: 'your-channel-id',
locationId: 'your-location-id',
}}
config={{ mock: true, debug: true }}
>
{children}
</SurfsideProvider2>
With debug: true, initialization steps and bid activity are logged to the console.
Provider placement
SurfsideProvider2 should wrap the subtree that contains your ad components. You can place it at the app root, or closer to a specific page or layout that needs ads — whatever fits your architecture. Components outside the provider will fail to initialize and log an error.
The React Hooks workflow gives you direct access to ad fetching and state management, letting you own the rendering. Use it when the built-in components don't fit your design or you need to compose ad state with other application state. The package is @surfside/ads-react, and it requires React 17 or later.
There are two layers:
- Provider —
SurfsideProvider2initializes the SDK and makes it available to all hooks in the tree. Set it up once at the root of the relevant subtree. - Hooks —
useSurfsideBanner,useSurfsideVideo,useSurfsideProducts, anduseSurfsideDynamicZoneeach manage their own fetching and state, and return the creative data for you to render.
Before any Surfside hook can request ads, the SDK needs to be initialized. Wrap your component tree with SurfsideProvider2 and pass your account identifiers. The provider handles initialization once and makes the SDK available to all descendant hooks through context.
import { SurfsideProvider2 } from '@surfside/ads-react';
export const App = () => (
<SurfsideProvider2
default={{
accountId: 'your-account-id',
siteId: 'your-site-id',
channelId: 'your-channel-id',
locationId: 'your-location-id',
}}
>
{/* your app */}
</SurfsideProvider2>
);
Props
| Prop | Type | Required | Description |
|---|---|---|---|
default.accountId | string | Yes | Your Surfside account ID |
default.siteId | string | Yes | The site ID configured in your account |
default.channelId | string | Yes | The channel ID for this placement context |
default.locationId | string | No | The location ID. Can be omitted and set later — see below |
default.userId | string | No | An identifier for the current user, used for personalization |
logger | ILogger | No | A logger instance. Pass InitLogger() from @surfside/ads-core to enable console output |
config.debug | boolean | No | Enables default console logging without a custom logger |
config.mock | boolean | No | Uses mock services instead of live Surfside APIs. Useful in development |
config.simulateNoResponse | number | No | When mocking, the probability (0–1) that the mock bidder returns no bids |
config.simulateTimeout | number | No | When mocking, artificial network latency in milliseconds |
onError | (error: ErrResult) => void | No | Called whenever any Surfside service returns an error |
Setting locationId dynamically
locationId identifies which store, menu, or location context the page represents. If this value isn't known when the provider mounts — for example, if it depends on a route parameter or a user selection — you can omit it from default and set it later using setLocationId from the context.
The SDK will not initialize until a locationId is set.
import { useSurfsideContext } from '@surfside/ads-react';
const LocationSetter = ({ locationId }: { locationId: string }) => {
const { setLocationId } = useSurfsideContext();
useEffect(() => {
setLocationId(locationId);
}, [locationId]);
return null;
};
Place <LocationSetter locationId={currentLocationId} /> anywhere inside SurfsideProvider2.
Development mode
Pass config={{ mock: true }} to use mock services and keep real network calls out of your development environment:
<SurfsideProvider2
default={{
accountId: 'your-account-id',
siteId: 'your-site-id',
channelId: 'your-channel-id',
locationId: 'your-location-id',
}}
config={{ mock: true, debug: true }}
>
{children}
</SurfsideProvider2>
With debug: true, initialization steps and bid activity are logged to the console.
Provider placement
SurfsideProvider2 should wrap the subtree that contains your hooks. You can place it at the app root, or closer to a specific page or layout that needs ads — whatever fits your architecture. Hooks called outside the provider will fail to initialize and log an error.
The Web Components workflow requires no build step and no JavaScript initialization call. Add a single <script> tag to your page, and it registers a set of custom elements — <surf-banner>, <surf-video>, <surf-carousel>, and <surf-dynamic-zone> — that you can drop directly into your HTML, the same way you'd use any other component. It's provided by the @surfside/ads package.
<script src="//cdn.surfside.io/ads/latest/r.js"></script>
Place this anywhere on the page — the <head> and just before </body> both work. Once it loads, any matching elements already in the DOM are upgraded automatically; there's nothing else to wire up.
Account attributes
Unlike the React workflows, there's no shared provider to configure once. Each element carries its own account identifiers as HTML attributes:
| Attribute | Required | Description |
|---|---|---|
account-id | Yes | Your Surfside account ID |
site-id | Yes | The site ID configured in your account |
channel-id | Yes | The channel ID for this placement context |
location-id | Yes | The location ID for this placement context (store-id is also accepted) |
zone-id | Yes | The zone ID of the placement (placement-id is also accepted) |
category | No | Comma-separated category targeting hints passed to the bidder |
keywords | No | Comma-separated keyword targeting hints passed to the bidder |
<surf-banner
account-id="your-account-id"
site-id="your-site-id"
channel-id="your-channel-id"
location-id="your-location-id"
zone-id="your-zone-id"
width="4"
height="1"
></surf-banner>
Repeating account-id, site-id, channel-id, and location-id across every element on the page is expected — each component reads its own attributes independently, there's no shared context. See Banners, Videos, Carousels, and Dynamic Zones for the attributes specific to each component.
Attributes are reactive: changing one on an already-connected element (for example, updating zone-id from JavaScript) automatically triggers a new bid request and re-render.
Identifying the current user
There's no user-id attribute to set. The Web Components workflow reads the current user's identifier automatically from Surfside's own first-party cookie rather than something you pass in yourself.