New Popup API spec · demo hub · plain-English guide · client contract implemented in the prototype widget (see src/types.ts and src/index.ts in the project repo)

New Popup — Platform API Specification

Three endpoints turn the prototype into product: serve survey config, accept responses, count events. Classic Popup is untouched; everything here lives in a new /pop-up/v2/ namespace.

v0.1 draft for platform-team estimation · July 2026 · Client side of this contract is already implemented in the prototype widget (21 KB, tested)

Confirmed foundations (Adam, July 2026): (1) The platform can create a survey record on arrival — the anonymous/generic-survey mechanism where sent date = completed date = now(). Endpoint 2 builds on that, not on pre-created non-response records. (2) A new config endpoint can be designed and built — this document is its spec. (3) The existing per-survey page-hit counter (crude response rate) is preserved via endpoint 3.

0. Shared principles

1. Survey config

GET /pop-up/v2/config/{surveyId}.json

Returns the survey's behaviour as JSON. Written by Survey Builder on Save (the evolution of the Distribution tab: trigger, audience, frequency, page fields join today's frequency + delay). The widget fetches it on every page load; this is what makes marketer changes live in minutes with no website deploy.

Response — 200, worked example

{
  "v": 2,
  "surveyId": "f8988988-14da-4124-a027-ff4fcd4ffd12",
  "active": true,
  "mode": "popover",
  "position": "bottom-right",
  "theme": { "primary": "#1a73e8", "radius": 12 },
  "language": "auto",
  "trigger": { "type": "scroll", "percent": 50 },
  "audience": {
    "urls": { "include": ["/checkout/*"], "exclude": [] },
    "devices": ["desktop", "mobile"],
    "sample": 25,
    "attributes": [{ "key": "page_views", "op": "gt", "value": 2 }]
  },
  "frequency": { "policy": "until_submitted", "cooldownDays": 30 },
  "questions": [
    { "id": "nps", "type": "rating", "text": "How likely…?", "scaleMin": 0, "scaleMax": 10,
      "labelLow": "Not at all likely", "labelHigh": "Extremely likely" },
    { "id": "comment", "type": "text", "text": "What is the main reason?" }
  ],
  "thankYou": { "title": "Thank you!", "message": "…" },
  "fields": {
    "order_value": { "source": "dataLayer:ecommerce.value", "dest": "segment_c" },
    "utm_campaign": "query:utm_campaign"
  },
  "submitUrl": "https://survey.eu.customergauge.com/pop-up/v2/responses",
  "eventsUrl": "https://survey.eu.customergauge.com/pop-up/v2/events"
}

2. Response ingestion

POST /pop-up/v2/responses

Accepts a completed (or partial) response and creates the survey record on arrival, using the existing anonymous-survey mechanism: sent date = completed date = now(). No pre-created non-response record exists or is required.

Request — what the widget sends today

{
  "v": 2,
  "surveyId": "f8988988-14da-4124-a027-ff4fcd4ffd12",
  "responseToken": "6f3f2c1e-8a37-4b1e-9d3e-2f6a1c9e4b70",
  "partial": false,
  "userId": "contact-4711",
  "attributes": {
    "page_views": 7, "session_page_views": 3,
    "order_value": 1250, "utm_campaign": "q3_launch",
    "plan": "enterprise", "account_name": "Acme Fabrication BV"
  },
  "answers": { "nps": 9, "comment": "Fast delivery, great support" },
  "url": "https://shop.example.com/checkout/complete",
  "submittedAt": "2026-07-07T14:31:09.144Z"
}

Server behaviour, in order

  1. Validate: known surveyId, answer ids exist in the survey, size ≤ 32 KB, ≤ 100 attribute keys, scalar values ≤ 256 chars.
  2. Idempotency: (surveyId, responseToken) is unique. A repeat is a no-op returning 202 — this makes sendBeacon/fetch retries and double-clicks safe. Tokens are client-generated UUIDs.
  3. Create the record via the anonymous-survey path: sent date = completed date = now(). partial: true creates it flagged partial; a later request with the same responseToken and partial: false completes it (mirrors today's partial-completes setting).
  4. Contact matching — opt-in, not automatic: match/create a contact only when identity fields (email / phone / reference) are present in the mapped fields and the account allows it. Otherwise the response stays anonymous and consumes no contact quota. This single rule fixes the junk-contacts problem.
  5. Tier 1 routing: using the survey's fields config, write attributes with a dest to that CG field/segment; write answers to their mapped CG fields (question→field mapping set in Survey Builder).
  6. Tier 2 storage: persist the remaining attributes object with the response — queryable JSON column per the engineering overview (registry + promotion); a memo-field shim is an acceptable bridge. Drop non-allowlisted PII keys at ingestion.

Status codes

CodeMeaning
202Accepted (including idempotent repeats). Processing may be async; the widget never waits on reporting.
400Malformed / unknown survey / failed validation. Body: {"error": "…"}.
413 / 429Too large / rate-limited (per IP + surveyId; generous — a human answers a survey once).

3. Events (impressions & funnel)

POST /pop-up/v2/events

Fire-and-forget batched counters. No records are created — this replaces "a non-response record per display" with cheap aggregates, while keeping the numbers you rely on.

{
  "v": 2,
  "surveyId": "f8988988-14da-4124-a027-ff4fcd4ffd12",
  "events": { "displayed": 1, "started": 1, "completed": 1 },
  "url": "https://shop.example.com/checkout/complete"
}

Rollout & effort shape

Open items for the platform team: (1) where the per-account PII allowlist lives and its default (suggest: default-deny email/phone capture from page data; identity only via explicit identify/params); (2) whether config Save purges the CDN or we accept the 60s TTL; (3) confirm the anonymous-survey record path exposes partial-complete semantics; (4) naming — /pop-up/v2/ vs a fresh /widget/ namespace.