GEA - Ad Content
Ad content serving for AI agent monetization
Get ad content (v2, keyless GET)
Keyless, CDN-cacheable GET surface for SSAI ad serving. The response is a pure
function of (pk, u) so it can be cached at the edge.
No authentication — the serve path carries no secret (industry standard: GAM
iu, AdSense ca-pub, Kevel networkId). pk is your public workspace UUID.
Abuse is handled by a per-workspace domain allow-list + IVT/bot detection + WAF,
not a per-request key. Any legacy Authorization: Api-Key header is accepted but
ignored.
Response is raw text/html (no JSON envelope), directly injectable into the
<div smalk-ads> placeholder:
200+ HTML when an ad is booked for the page. The internal booking id is never exposed; the only metadata isLast-Modified(see below).204 No Contenton no-fill (99.9% of pages) — cacheable; leave the<div smalk-ads>element unchanged.Last-Modified= when the served ad HTML last changed. It moves only when the creative actually changes (stable across cache-fills), so a publisher (Arc XP, WordPress, any CDN integration) can tell that the ad now injected differs from the one injected last time and trigger its own cache purge + page-header refresh. Compare it against the value you stored on the previous fetch; if it changed, re-inject and purge.ETag+ conditional304onIf-None-Match.Cache-Control: public, max-age=300, stale-while-revalidate=86400.
Preview: ?preview=<code> returns a placeholder render, no-store.
Differences vs v1 (POST /api/v1/transform/ads/content): GET not POST; pk/u
query params replace the JSON body; placement_id and all per-visitor params
removed; text/html replaces the JSON envelope.
query Parameters
pkPublic workspace UUID (project_key). In the cache key.
uPage URL (URL-encoded) — the only ad selector. Canonically normalized (drop utm_*/gclid) before forming the cache key.
previewRender a placeholder ad instead of serving a real one: no booking is consumed, no impression is recorded, nothing is read from the database. The response is shaped exactly like a served ad, so what the page shows is what a booking would show — only the editorial content is a fixed lorem placeholder.
Current vocabulary — a frame code {generation}{format}{content}, the same value a citation widget derives from its format and content type. Format: m minimal, t teaser, p paragraph, f full. Content: o ad_only, c toc, s summary, d tldr, v video. Accepted codes: 2mo, 2mc, 2ms, 2md, 2mv, 2to, 2tc, 2ts, 2td, 2tv, 2po, 2pc, 2ps, 2pd, 2pv, 2fo, 2fc, 2fs, 2fd, 2fv.
Legacy vocabulary (deprecated) — the frame_color values that predate the codes, still accepted: no, grey, black, clear, gradient, shadow, label, preview, summary_boxed_reveal, summary_editorial_brief, summary_video_transcript, summary_toc, summary_expert_content. They render the chrome they always did; prefer a frame code.
true / 1 turns preview on with no frame. false / 0 / off, or omitting the parameter, serves the real ad. An unreadable value is never an error: it falls back to no frame. A preview in the body wins over the query string.
On this surface the placeholder comes back as raw text/html with Cache-Control: no-store.
Get ad content (v2, keyless GET) › Responses
Booked ad HTML (raw, text/html). Last-Modified is the ad's content-change time — publishers purge their cache + refresh page headers when it changes; ETag enables 304. The internal booking id is never exposed.
Get ad content for injection
Retrieve HTML ad content to inject into your webpage.
This endpoint returns pre-generated ad content that matches active campaigns
for the specified page URL. The content is returned as HTML that can be
directly injected into elements with the smalk-ads attribute.
Authentication: Requires API Key in the Authorization header.
Format: Authorization: Api-Key YOUR_API_KEY
The project_key in the request body is used to identify the workspace.
Request Data Guidelines
The request body should contain only the data we need. Filter sensitive information before sending.
✅ Required Fields
| Field | Description |
|---|---|
project_key | Required. Your workspace API key (UUID format). |
page_url | Required. Full URL of the page requesting ad content. |
user_agent | Required. The visitor's User-Agent string for AI agent detection. |
referer | Required. Referrer URL for traffic source tracking. |
📋 Recommended Fields
| Field | Description |
|---|---|
client_ip | Client's IP address for geographic targeting. |
placement_id | Specific placement ID to target (for multiple placements). |
Server-Side Integration (SSAI):
This is a server-side API. Your backend should:
- Detect
<div smalk-ads></div>elements in your HTML before sending the response - Call this endpoint with your API Key and the current
page_url - Replace the div elements with the returned HTML content
- Send the complete HTML to the client (including AI agents)
This ensures ads are visible to AI agents (ChatGPT, Perplexity, etc.) that don't execute JavaScript.
Returned HTML — tags & attributes to allow-list
If you sanitize/re-parse our HTML before injecting it, allow the following tags and attributes so the content and its sponsored framing render intact. The safest integration is to inject our fragment verbatim (no re-parsing), which preserves everything automatically.
Tags:
| Group | Tags |
|---|---|
| Structure | h2, h3, p, ol, li |
| Inline | strong, a |
| Structured data | script (only type="application/ld+json" — FAQPage + Organization + Service bundle; the key citability signal for AI engines) |
| Comparison table (occasional format) | table, thead, tbody, tr, th, td |
We never emit ul, div, span, section, article, or styling tags
(b/u/i/font) — sanitizers may keep or drop those freely.
Attributes (must be preserved):
| Attribute | On | Purpose |
|---|---|---|
href, style | a | Sponsor link + underline |
type | script | application/ld+json |
data-cf, data-cf-end, data-summary-variant | block elements | Frame styling markers — strip these and the sponsored frame/label/summary widget will NOT render client-side |
⚠️ Allow-lists that drop
data-*by default (HTMLPurifier-style) will remove the framing markers. Explicitly permitdata-cf,data-cf-end, anddata-summary-variant.
Encoding: non-ASCII characters are returned as numeric HTML entities
(e.g. é for é), so the ad renders correctly regardless of your page's
charset (incl. legacy windows-1252). Do not transcode or strip entities.
Preview Mode:
Set preview=<code> in the body, or ?preview=<code> in the query string, to receive a
placeholder ad instead of a served one. The editorial content is a fixed Latin lorem-ipsum
placeholder shaped like a real ad; everything else — the framing markers, the summary or
transcript the rendering displays above the ad — is exactly what a booking would return.
A publisher can therefore drop tracker.js on a real page and eyeball any rendering live,
with no configuration and no booking.
The preview parameter below carries the vocabulary: the current frame codes, which pair a
format axis with a content axis exactly as a citation widget does, and the legacy
frame_color values that predate them. Both lists are generated from the code that parses
them, so neither can advertise a rendering the server refuses.
Preview mode requires the same API key but skips the publisher_ads_enabled check, does not
record impressions, and does not create placements. Body wins over query.
Response: Returns HTML content for each placement, or empty content if no active campaigns match.
query Parameters
previewRender a placeholder ad instead of serving a real one: no booking is consumed, no impression is recorded, nothing is read from the database. The response is shaped exactly like a served ad, so what the page shows is what a booking would show — only the editorial content is a fixed lorem placeholder.
Current vocabulary — a frame code {generation}{format}{content}, the same value a citation widget derives from its format and content type. Format: m minimal, t teaser, p paragraph, f full. Content: o ad_only, c toc, s summary, d tldr, v video. Accepted codes: 2mo, 2mc, 2ms, 2md, 2mv, 2to, 2tc, 2ts, 2td, 2tv, 2po, 2pc, 2ps, 2pd, 2pv, 2fo, 2fc, 2fs, 2fd, 2fv.
Legacy vocabulary (deprecated) — the frame_color values that predate the codes, still accepted: no, grey, black, clear, gradient, shadow, label, preview, summary_boxed_reveal, summary_editorial_brief, summary_video_transcript, summary_toc, summary_expert_content. They render the chrome they always did; prefer a frame code.
true / 1 turns preview on with no frame. false / 0 / off, or omitting the parameter, serves the real ad. An unreadable value is never an error: it falls back to no frame. A preview in the body wins over the query string.
Get ad content for injection › Request Body
project_keyYour workspace API key (UUID format)
page_urlFull URL of the page requesting ad content
user_agentVisitor's User-Agent string (required for AI agent detection)
refererReferrer URL (required for traffic source tracking)
client_ipClient's IP address for geographic targeting
placement_idDEPRECATED. Previously used to target a specific placement; no longer recommended. Targeting is now handled automatically via page_url. Field is still accepted for backwards compatibility but should not be used in new integrations.
previewRender a placeholder ad instead of serving a real one: no booking is consumed, no impression is recorded, nothing is read from the database. The response is shaped exactly like a served ad, so what the page shows is what a booking would show — only the editorial content is a fixed lorem placeholder.
Current vocabulary — a frame code {generation}{format}{content}, the same value a citation widget derives from its format and content type. Format: m minimal, t teaser, p paragraph, f full. Content: o ad_only, c toc, s summary, d tldr, v video. Accepted codes: 2mo, 2mc, 2ms, 2md, 2mv, 2to, 2tc, 2ts, 2td, 2tv, 2po, 2pc, 2ps, 2pd, 2pv, 2fo, 2fc, 2fs, 2fd, 2fv.
Legacy vocabulary (deprecated) — the frame_color values that predate the codes, still accepted: no, grey, black, clear, gradient, shadow, label, preview, summary_boxed_reveal, summary_editorial_brief, summary_video_transcript, summary_toc, summary_expert_content. They render the chrome they always did; prefer a frame code.
true / 1 turns preview on with no frame. false / 0 / off, or omitting the parameter, serves the real ad. An unreadable value is never an error: it falls back to no frame. A preview in the body wins over the query string.
Get ad content for injection › Responses
Ad content retrieved successfully. IMPORTANT: When no ads are available, the API returns {"html": ""}. In this case, server-side implementations must NOT replace the
htmlHTML content to inject into the page. Empty string if no active booking. IMPORTANT: When the API returns {"html": ""}, the
booking_idUUID of the active booking (only present when ad content is available)
Metadata object containing project_id, timestamp, and session_id (only present when no active booking)