Trigger Widget
Server-triggered overlay for KYC fails, declined deposits, and 'we can't serve you here' flows. Same UI as the geo-blocking widget, different trigger.
Use the Trigger Widget when your app has already decided a user isn't a fit. Think KYC failed, deposit declined, signup rejected, or age check blocked. You want to show alternative sponsored offers instead of an empty "sorry" screen. Same overlay UI as the Geo-blocking widget. Only the trigger differs. Your server arms it with an integration secret that must never ship to the browser.
Security first
The integration secret (its_…) is the only credential that can arm a Trigger Widget session. Leaking it lets anyone trigger overlays on your domain. Store it in env variables or a secrets manager. Never in NEXT_PUBLIC_*, client bundles, or git history.
Before you install
Enable Trigger Widget in the dashboard
Dashboard → Websites → [Your site] → toggle Trigger Widget. This reveals the integration secret (keep it on the server only). Confirm your domain matches the page you'll load the widget on.
Collect three values
From the same screen, copy: public key (
pk_…), website key (web_…), integration secret (its_…). The first two go in HTML; the secret goes in your server's env.Pick where to load the embed
Usually a dedicated page (
/declined,/signup-failed) or a post-decline step in a modal. Must be on the same domain you registered.
How the handshake works
Page loads widget-decline.js
The embed POSTs
POST /v1/widget-decline/sessionwith your public + website keys as query params. On success it receives asessionIdand a 15-minuteexpiresAt.Embed surfaces the session id
It sets
window.__RETARGET_DECLINE_SESSION_IDand dispatches a DOM eventretarget:decline-sessionwithdetail: { sessionId }. Your client code reads either.Your client notifies your server
Send the
sessionIdto your backend (a cookie-auth'd API route, a signed token, or whatever fits your app).Embed polls in the background
Every 2 seconds it calls
GET /v1/widget-decline/session/{sessionId}. Status stayspendinguntil you trigger.Your server calls trigger
POST /v1/widget-decline/triggerwithAuthorization: Bearer <integration_secret>and{ "sessionId": "..." }. Returns 200 with{ "ok": true }(or{ "ok": true, "alreadyTriggered": true }if called twice).Next poll sees ready → overlay renders
Within 2 seconds the embed loads offers and renders the same UI as the geo-blocking widget.
1. Load widget-decline.js on your ineligible-user page
<script
src="https://cdn.retarget.gg/widget-decline.js"
data-pub="YOUR_PUBLIC_KEY"
data-website="YOUR_WEBSITE_KEY"
></script>// app/declined/page.tsx
import Script from "next/script";
export default function DeclinedPage() {
return (
<>
<Script
id="retarget-decline"
src="https://cdn.retarget.gg/widget-decline.js"
strategy="beforeInteractive"
data-pub={process.env.NEXT_PUBLIC_RETARGET_PUB!}
data-website={process.env.NEXT_PUBLIC_RETARGET_WEBSITE!}
/>
<main>
<h1>We can't proceed with this account</h1>
<p>You may see alternative offers while we review.</p>
</main>
</>
);
}Prefer synchronous loading
widget-decline.js looks up its own <script> tag to read data-pub / data-website. Async/defer loading can make this self-lookup flaky. Use a synchronous tag (or Next.js beforeInteractive) on this one embed.
2. Relay the session id to your backend
Your server needs the sessionId (to pair with your user); the integration secret stays on the server.
"use client";
// app/declined/decline-bridge.tsx: include on the decline page
import { useEffect } from "react";
export function DeclineBridge() {
useEffect(() => {
const sendToServer = (sessionId: string) =>
fetch("/api/retarget/decline-arm", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId }),
});
function handler(ev: Event) {
const detail = (ev as CustomEvent<{ sessionId: string }>).detail;
if (detail?.sessionId) void sendToServer(detail.sessionId);
}
window.addEventListener("retarget:decline-session", handler);
const existing = window.__RETARGET_DECLINE_SESSION_ID;
if (typeof existing === "string" && existing) void sendToServer(existing);
return () => window.removeEventListener("retarget:decline-session", handler);
}, []);
return null;
}<script>
(function () {
function sendToServer(sessionId) {
fetch("/api/retarget/decline-arm", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: sessionId })
});
}
window.addEventListener("retarget:decline-session", function (ev) {
var id = ev.detail && ev.detail.sessionId;
if (id) sendToServer(id);
});
var existing = window.__RETARGET_DECLINE_SESSION_ID;
if (existing) sendToServer(existing);
})();
</script>Authenticate the bridge route
/api/retarget/decline-arm should authenticate the caller (session cookie, signed JWT) and pair the sessionId with a user id in your database before letting the server trigger. Otherwise a visitor could arm arbitrary sessions on your domain.
3. Call trigger from your server
Send Authorization: Bearer <integration_secret> (or the x-integration-secret header) with { "sessionId": "..." }. On success the API returns 200 with { "ok": true }.
// app/api/retarget/trigger/route.ts
import { NextResponse } from "next/server";
const API = process.env.RETARGET_API_URL ?? "https://api.retarget.gg";
export async function POST(req: Request) {
const secret = process.env.RETARGET_INTEGRATION_SECRET;
if (!secret) {
return NextResponse.json(
{ error: "Missing RETARGET_INTEGRATION_SECRET" },
{ status: 500 },
);
}
const { sessionId } = (await req.json()) as { sessionId?: string };
if (!sessionId) {
return NextResponse.json({ error: "sessionId required" }, { status: 400 });
}
const res = await fetch(`${API}/v1/widget-decline/trigger`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secret}`,
},
body: JSON.stringify({ sessionId }),
});
if (!res.ok) {
return NextResponse.json({ error: await res.text() }, { status: res.status });
}
const body = await res.json(); // { ok: true }
return NextResponse.json(body, { status: 200 });
}// server-only util: call this from your "decline" code path
const API = process.env.RETARGET_API_URL ?? "https://api.retarget.gg";
export async function triggerDeclineOverlay(sessionId: string) {
const secret = process.env.RETARGET_INTEGRATION_SECRET;
if (!secret) throw new Error("RETARGET_INTEGRATION_SECRET is not set");
const res = await fetch(`${API}/v1/widget-decline/trigger`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secret}`,
},
body: JSON.stringify({ sessionId }),
});
if (!res.ok) {
throw new Error(`ReTarget.gg trigger failed: ${res.status} ${await res.text()}`);
}
}import os
import requests
API = os.environ.get("RETARGET_API_URL", "https://api.retarget.gg")
def trigger_decline_overlay(session_id: str) -> None:
secret = os.environ["RETARGET_INTEGRATION_SECRET"]
r = requests.post(
f"{API}/v1/widget-decline/trigger",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {secret}",
},
json={"sessionId": session_id},
timeout=15,
)
r.raise_for_status()Call triggerDeclineOverlay(sessionId) from the exact code path where your product says "not eligible": immediately after your KYC call fails, after you persist a "declined" row, etc.
Optional attributes
| Attribute | Purpose |
|---|---|
data-api | Override API base URL: use for staging or a self-hosted API. |
data-comments="false" | Mute the script's console.log diagnostics. |
data-dev-country / data-dev-region | Simulate a visitor's location. Only honored on localhost. |
End-to-end sanity check
Open the page that loads the embed
The
<script>tag fires and creates a session. DevTools → Network showsPOST /v1/widget-decline/sessionreturning 200.Verify the session id
In DevTools → Console,
window.__RETARGET_DECLINE_SESSION_IDis a string that starts withdcl_.Trigger from your server
Via your test harness or an admin-only button, call your
/api/retarget/triggerroute with thatsessionId. The API returns200with{ "ok": true }.Overlay appears within ~2 seconds
The next poll returns
status: readyand the embed renders offers. Click one to confirm attribution lands in the dashboard.
Security checklist
RETARGET_INTEGRATION_SECRETlives only in server env vars or a secrets manager.- Your "arm session" route authenticates the caller before accepting a
sessionId. - Log every
sessionId/ trigger pair to your audit log: makes abuse and debugging fast. - Never log the integration secret itself.
Related
API reference: Trigger Widget endpoints
POST /v1/widget-decline/session, GET …/session/{id}, POST …/trigger.
Widget integration reference
Full script attribute list and cross-cutting behavior.
Geo-blocking widget
Same overlay UI, different trigger (automatic on geo mismatch).
Debugging
What to check when the overlay doesn't render after trigger.
Publisher
Geo Popup & Decline Popup overview
The product framing for the install you just read.
Advertiser
CPC, CPA, eCPM in one auction
The demand side of the network — useful context even for publishers.
Essays
Long-form strategy
How others think about geo-blocking, KYC declines, and bid models.
Need help with setup?
Send us your website stack, target regions, and whether you are installing Geo Popup or Decline Popup.