ReTarget.gg
Integrate

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.

5 min readReviewed Apr 27, 2026

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.

Public key + website key required

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

  1. 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.

  2. 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.

  3. 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

  1. Page loads widget-decline.js

    The embed POSTs POST /v1/widget-decline/session with your public + website keys as query params. On success it receives a sessionId and a 15-minute expiresAt.

  2. Embed surfaces the session id

    It sets window.__RETARGET_DECLINE_SESSION_ID and dispatches a DOM event retarget:decline-session with detail: { sessionId }. Your client code reads either.

  3. Your client notifies your server

    Send the sessionId to your backend (a cookie-auth'd API route, a signed token, or whatever fits your app).

  4. Embed polls in the background

    Every 2 seconds it calls GET /v1/widget-decline/session/{sessionId}. Status stays pending until you trigger.

  5. Your server calls trigger

    POST /v1/widget-decline/trigger with Authorization: Bearer <integration_secret> and { "sessionId": "..." }. Returns 200 with { "ok": true } (or { "ok": true, "alreadyTriggered": true } if called twice).

  6. 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

HTML
<script
src="https://cdn.retarget.gg/widget-decline.js"
data-pub="YOUR_PUBLIC_KEY"
data-website="YOUR_WEBSITE_KEY"
></script>
Next.js
// 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&apos;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.

Next.js (client)
"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;
}
Plain JS
<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

POST/v1/widget-decline/triggerauth: integration-secret

Send Authorization: Bearer <integration_secret> (or the x-integration-secret header) with { "sessionId": "..." }. On success the API returns 200 with { "ok": true }.

Next.js route
// 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 });
}
Generic TS helper
// 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()}`);
}
}
Python
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

AttributePurpose
data-apiOverride 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-regionSimulate a visitor's location. Only honored on localhost.

End-to-end sanity check

  1. Open the page that loads the embed

    The <script> tag fires and creates a session. DevTools → Network shows POST /v1/widget-decline/session returning 200.

  2. Verify the session id

    In DevTools → Console, window.__RETARGET_DECLINE_SESSION_ID is a string that starts with dcl_.

  3. Trigger from your server

    Via your test harness or an admin-only button, call your /api/retarget/trigger route with that sessionId. The API returns 200 with { "ok": true }.

  4. Overlay appears within ~2 seconds

    The next poll returns status: ready and the embed renders offers. Click one to confirm attribution lands in the dashboard.

Security checklist

  • RETARGET_INTEGRATION_SECRET lives 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.

Need help with setup?

Send us your website stack, target regions, and whether you are installing Geo Popup or Decline Popup.

Trigger Widget | Docs | ReTarget.gg