@zeroclickai/offers-sdk
v1.2.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), cleans up and returns
nullsilently - Supports cancellation via
AbortSignalfor React Strict Mode and component unmounting
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 |
| style | IframeStyleConfig | — | Custom colors to match your surface theme (see below) |
| width | string | '300px' | 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) |
| signal | AbortSignal | — | Cancel an in-flight render (useful for React Strict Mode, unmounting) |
| 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'
borderColor?: string; // e.g., '#333333'
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'
}Styles can be updated at any time after render via handle.updateStyle() — useful for responding to theme changes without re-rendering the entire offer.
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();ZeroClick.getIframeUrl(offer)
Returns the iframe source URL from the offer's UI configuration, or null if the offer doesn't have iframe rendering configured.
const url = ZeroClick.getIframeUrl(offer);ZeroClick.getIframeTag(offer, options?)
Generate a static HTML <iframe> tag string for server-side rendering or contexts where JavaScript is not available (e.g., CMS output, static sites). For interactive rendering with theme support, auto-height, and CTA click handling, use renderOffer() instead. Returns null if the offer has no iframe URL.
const html = ZeroClick.getIframeTag(offer, { width: '100%', height: '120px' });
if (html) {
container.innerHTML = html;
}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,
IframeStyleConfig,
CtaClickEvent,
ResizeEvent,
RenderHandle,
} 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
