@zeroclickai/offers-sdk
v1.4.0
Published
ZeroClick Offers SDK
Keywords
Readme
@zeroclickai/offers-sdk
Official SDK for ZeroClick Offers.
Installation
npm install @zeroclickai/offers-sdkArchitecture
getOffers()- Server-side only (called from your backend, requires client IP forwarding)renderOffer()- Client-side (renders an offer as a themed iframe with auto-height and CTA handling)trackOfferImpressions()- Client-side (tracks impressions for analytics)
Client/Browser ──────────────────────────────────┐──────────────────────┐
│ │ │
│ (1) Request offers │ (3) Track │ (4) Render offer
↓ ↓ impressions ↓
Your Backend ──(2)──> ZeroClick API ZeroClick API SDK.renderOffer()
(SDK.getOffers) /api/v2/offers /api/v2/impressions (iframe + postMessage)Quick Start
Server-Side: Fetching Offers
// backend/api/offers.ts (Express, Next.js API route, etc.)
import { ZeroClick } from '@zeroclickai/offers-sdk';
// Initialize once on server startup
ZeroClick.initialize({ apiKey: 'your-api-key' });
// In your API route handler
app.post('/api/offers', async (req, res) => {
const offers = await ZeroClick.getOffers({
ipAddress: req.ip || req.headers['x-forwarded-for'], // Required
userAgent: req.headers['user-agent'], // Optional
query: req.body.query,
limit: 3,
identity: {
userId: req.user?.id,
userLocale: req.user?.locale,
},
});
res.json(offers);
});Client-Side: Rendering Offers
// frontend/client.ts
import { ZeroClick } from '@zeroclickai/offers-sdk';
// Initialize (no API key needed for rendering or tracking)
ZeroClick.initialize();
// Render an offer into a container with theming and auto-height
const handle = await ZeroClick.renderOffer(offer, '#ad-container', {
width: '100%',
mode: 'dark',
style: {
background: '#1a1a2e',
textColor: '#ffffff',
buttonBackground: '#4a9eff',
},
autoHeight: true,
maxHeight: 300,
});
// Track when offers are displayed to the user
await ZeroClick.trackOfferImpressions([offer.id]);API
ZeroClick.initialize(config?)
Initialize the SDK. Must be called before other methods.
Server-side initialization:
ZeroClick.initialize({
apiKey: 'your-api-key', // Required for getOffers
});Client-side initialization (rendering and tracking):
ZeroClick.initialize(); // No API key neededZeroClick.getOffers(options) 🔒 Server-side only
Fetch ranked offers based on intent signals. Must be called from your backend server (no CORS access from browsers). Requires apiKey to be set during initialization.
const offers = await ZeroClick.getOffers({
ipAddress: string; // Required - Client IP from incoming request
query?: string; // Search query for offers
limit?: number; // Max offers to return (default: 1)
userAgent?: string; // Optional - Client's user agent
origin?: string; // Optional - Client's origin/referer
identity?: { // Optional - User identity (per-request)
userId?: string; // Your user's ID
userEmailSha256?: string; // SHA-256 hash of email (lowercase, trimmed)
userPhoneNumberSha256?: string; // SHA-256 hash of phone (E.164 format)
userLocale?: string; // e.g., 'en-US'
userSessionId?: string; // Session identifier
groupingId?: string; // Grouping ID for analytics segmentation
};
});Example:
app.post('/api/offers', async (req, res) => {
const offers = await ZeroClick.getOffers({
ipAddress: req.ip || req.headers['x-forwarded-for'],
userAgent: req.headers['user-agent'],
query: req.body.query,
limit: 3,
identity: {
userId: req.user?.id,
userLocale: req.user?.locale,
},
});
res.json(offers);
});Returns Promise<Offer[]> — an array of offers ranked by relevance.
ZeroClick.trackOfferImpressions(ids) ✅ Client-side
Track offer impressions for analytics. Does not require an API key. Should be called from the client/browser when offers are displayed to the user.
await ZeroClick.trackOfferImpressions(['offer-123', 'offer-456']);ZeroClick.renderOffer(offer, container, options?) 🖥️ Browser-only
The primary way to display offers in browser environments. Creates a sandboxed iframe, sends offer data via postMessage over a dedicated MessageChannel, and returns a RenderHandle for ongoing control (style updates, cleanup).
const handle = await ZeroClick.renderOffer(offer, '#ad-container', {
width: '100%',
mode: 'dark',
style: {
background: '#1a1a2e',
textColor: '#ffffff',
buttonBackground: '#4a9eff',
},
autoHeight: true,
maxHeight: 300,
onCtaClick: ({ url }) => window.open(url, '_blank'),
onResize: ({ height }) => console.log('New height:', height),
});Key behaviors:
- Returns
nullif the offer has nouifield (not all offers support iframe rendering) - Re-rendering into the same container automatically replaces any existing ZeroClick iframe
- On failure (network error, ad blocker, content-ready timeout), cleans up, calls
onError(if provided), and returnsnull - Supports cancellation via
AbortSignalfor React Strict Mode and component unmounting - Smoothly transitions iframe height when
autoHeightis enabled (transition: height 150ms ease)
RenderHandle
The returned handle provides ongoing control over the rendered iframe:
if (handle) {
// Update styles dynamically (e.g., when user toggles theme)
handle.updateStyle({ background: '#fff', textColor: '#000' });
// Remove the iframe and clean up the message channel
handle.destroy();
}Options
| Option | Type | Default | Description |
| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| mode | 'light' \| 'dark' | — | Theme mode passed to the iframe |
| layout | string | — | Layout variant for presentation (e.g. 'horizontal') |
| style | IframeStyleConfig | — | Custom colors to match your surface theme (see below) |
| width | string | '100%' | CSS width |
| height | string | '250px' | CSS height (overridden when autoHeight is enabled) |
| autoHeight | boolean | false | Dynamically resize iframe to match content height |
| minHeight | number | 50 | Minimum height in px when autoHeight is enabled |
| maxHeight | number | — | Maximum height in px when autoHeight is enabled |
| onResize | (event: ResizeEvent) => void | — | Callback when iframe height changes (requires autoHeight: true) |
| onCtaClick | (event: CtaClickEvent) => void | — | Callback when user clicks the CTA (use in environments where popups are blocked) |
| onError | (error: Error) => void | — | Callback when the iframe fails to load (timeout, network error, content-ready timeout). Not called on abort. |
| signal | AbortSignal | — | Cancel an in-flight render (useful for React Strict Mode, unmounting) |
| contentReadyTimeout | number | 5000 | Wait this many ms for the iframe to confirm it rendered successfully. Set to 0 to disable. See Failure Detection. |
| sandbox | string | 'allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox' | Iframe sandbox attributes |
Auto-Height
When autoHeight is enabled, the iframe reports its content height and the SDK automatically resizes the iframe element. Use minHeight and maxHeight to constrain the range.
const handle = await ZeroClick.renderOffer(offer, '#container', {
autoHeight: true,
minHeight: 80,
maxHeight: 400,
onResize: ({ height }) => {
// Adjust surrounding layout if needed
console.log('Offer height changed to', height);
},
});Style Configuration (IframeStyleConfig)
Pass custom colors to match your host surface theme. If not provided, the iframe renders with a default theme based on mode.
{
background?: string; // e.g., '#1a1a2e'
backgroundHover?: string; // e.g., '#252540'
border?: string; // Full border shorthand (e.g., '1px solid #333', 'none')
borderRadius?: string; // e.g., '12px'
buttonBackground?: string; // e.g., '#4a9eff'
buttonBackgroundHover?: string; // e.g., '#3a8eef'
buttonTextColor?: string; // e.g., '#ffffff'
textColor?: string; // e.g., '#ffffff'
// Any additional string keys are passed through to the iframe as CSS variables
[key: string]: string | undefined;
}Styles can be updated at any time after render via handle.updateStyle() — useful for responding to theme changes without re-rendering the entire offer.
Failure Detection with contentReadyTimeout
The iframe load event fires as soon as the HTML response is received, even if the page fails to actually render the offer (e.g., a script error inside the iframe). By default, renderOffer waits up to 5000 ms for the iframe to confirm it rendered successfully — if the confirmation doesn't arrive in time, the SDK calls onError, removes the iframe, and renderOffer resolves to null, letting you fall back gracefully.
const handle = await ZeroClick.renderOffer(offer, '#ad-container', {
autoHeight: true,
contentReadyTimeout: 3000, // Override the default 5s timeout
onError: (err) => {
console.warn('Offer failed to render:', err.message);
// Show fallback content or hide the container
},
});
if (!handle) {
// Render failed — handle gracefully
}Set contentReadyTimeout: 0 to disable and resolve as soon as the iframe load event fires.
Cancellation with AbortSignal
Use signal to cancel renders that are no longer needed — particularly useful with React Strict Mode (which mounts/unmounts in development) or when a component unmounts before the iframe finishes loading.
const controller = new AbortController();
ZeroClick.renderOffer(offer, '#container', {
signal: controller.signal,
});
// Cancel the render (e.g., on unmount)
controller.abort();Offer Schema
interface Offer {
id: string;
title: string | null;
subtitle: string | null;
content: string | null;
cta: string | null;
clickUrl: string;
rawUrlEncoded: string | null;
imageUrl: string | null;
metadata: Record<string, unknown> | null;
context: string | null;
brand: { name; description; url; iconUrl } | null;
product: { productId; sku; title; description; category; subcategory; image; availability; metadata } | null;
price: { amount; currency; originalPrice; discount; interval } | null;
location: { text; address; city; state; zip; distance; distanceUnit; coordinates; hours } | null;
media: { title; url; description; contentType } | null;
rating: { value; scale; count } | null;
ui?: { type: string; url: string } | null;
}TypeScript
All types are exported for TypeScript users:
import type {
Offer,
ZeroClickConfig,
GetOffersOptions,
Identity,
IframeRenderOptions,
IframeMode,
IframeStyleConfig,
OfferUI,
CtaClickEvent,
ResizeEvent,
RenderHandle,
} from '@zeroclickai/offers-sdk';
// Runtime exports
import { ZeroClick, ZeroClickApiError } from '@zeroclickai/offers-sdk';Common Integration Patterns
React
Use AbortSignal to handle Strict Mode double-mounts and component unmounting:
import { useEffect, useRef } from 'react';
import { ZeroClick, type Offer, type RenderHandle } from '@zeroclickai/offers-sdk';
function OfferBanner({ offer }: { offer: Offer }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const controller = new AbortController();
let handle: RenderHandle | null = null;
ZeroClick.renderOffer(offer, containerRef.current!, {
width: '100%',
autoHeight: true,
maxHeight: 300,
mode: 'light',
signal: controller.signal,
}).then((h) => {
handle = h;
});
return () => {
controller.abort();
handle?.destroy();
};
}, [offer]);
return <div ref={containerRef} />;
}Next.js App Router
// app/api/offers/route.ts
import { ZeroClick } from '@zeroclickai/offers-sdk';
import { NextRequest, NextResponse } from 'next/server';
ZeroClick.initialize({ apiKey: process.env.ZEROCLICK_API_KEY });
export async function POST(req: NextRequest) {
const { query, userId } = await req.json();
const ip = req.headers.get('x-forwarded-for') || req.ip || '127.0.0.1';
const offers = await ZeroClick.getOffers({
ipAddress: ip,
userAgent: req.headers.get('user-agent') || undefined,
query,
limit: 3,
identity: userId ? { userId } : undefined,
});
return NextResponse.json(offers);
}VS Code Extension (Webview)
Use onCtaClick to handle link opening (VS Code webviews block window.open) and CSS variables to match the editor theme:
const handle = await ZeroClick.renderOffer(offer, '#ad-slot', {
width: '100%',
mode: isDarkTheme ? 'dark' : 'light',
style: {
background: 'var(--vscode-sideBar-background)',
textColor: 'var(--vscode-foreground)',
buttonBackground: 'var(--vscode-button-background)',
buttonTextColor: 'var(--vscode-button-foreground)',
},
autoHeight: true,
onCtaClick: ({ url }) => vscode.env.openExternal(vscode.Uri.parse(url)),
});
// Update theme when VS Code theme changes
if (handle) {
window.addEventListener('message', (event) => {
if (event.data.type === 'theme-change') {
handle.updateStyle({
background: event.data.isDark ? '#1e1e1e' : '#ffffff',
textColor: event.data.isDark ? '#cccccc' : '#333333',
});
}
});
}Express
import { ZeroClick } from '@zeroclickai/offers-sdk';
import express from 'express';
const app = express();
app.use(express.json());
ZeroClick.initialize({ apiKey: process.env.ZEROCLICK_API_KEY });
app.post('/api/offers', async (req, res) => {
const offers = await ZeroClick.getOffers({
ipAddress: req.ip || (req.headers['x-forwarded-for'] as string),
userAgent: req.headers['user-agent'],
query: req.body.query,
limit: 3,
identity: {
userId: req.user?.id,
userLocale: req.user?.locale,
},
});
res.json(offers);
});License
MIT
