← Polite Popup / API
Tokens

Drive Polite Popup from your own code

The page does its free work in the browser: it reads one or more pasted popups and their context, names each popup's purpose, format and trigger, finds the six areas the skill checks (timing, value, respect, targeting, mobile, compliance) with the line that shows each, computes the conversion and close rates against the skill's benchmark, and raises flags PU-01..PU-22. Its two paid lanes are available over HTTP. You send one JSON object with task set to "audit" or "design", and the model replies with one JSON object. The easiest way to build the facts string is the page itself: Popkit.buildInput(lane, Popkit.analyze(popups, {context, goal, question}), {auditHandoff}) in popkit.js, which also runs under Node.

Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends Content-Type: application/json and, once you have a token, Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10% publisher markup. A run needs a signed-in (user) token and is metered: the platform holds the estimate's hold_credits while the run is in flight and then charges only what it used (charged_credits). /estimate is free. A plain-text summary is at /llms.txt.

The response envelope

Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.

Error codes

HTTPcodemeaning
400VALIDATION_ERRORThe body is not a JSON object, or a field is the wrong type. Every field of the run input is a string.
401UNAUTHORIZEDNo token, or an expired one: get a new one (step 2).
402INSUFFICIENT_CREDITSThe balance is below the run's minimum. Top up at skillsafe.ai/account/billing/.
403FORBIDDENA guest token tried a metered run: sign in for a personal token.
404NOT_FOUNDUnknown job id.
429RATE_LIMITEDBack off and retry with the same Idempotency-Key.

The task field: two lanes

taskwhat the model does
"audit"The popup audit: an inventory entry per popup (purpose, format, trigger, a one-line read); the six areas (timing, value, respect, targeting, mobile, compliance) each good, weak, bad or unknown with a finding, quotes and a fix; the numbers (the page's conversion rate, benchmark and below, within, above or unknown); the issues as finding, impact and fix with the page's flag ids; 3 to 6 fixes; a verdict keep, fix_first or rebuild.
"design"The popups: the goal; 1 to 3 popups each with format (never full screen), trigger and its detail (never page load), audience, pages (checkout excluded), frequency (capped, with a wait after a dismissal), copy (headline, subhead, button, decline), fields, design and the phone version; conflict rules; 2 to 5 compliance and accessibility items; 3 to 6 metrics; 2 to 4 tests.

Input fields (all strings)

fieldwhencontent
taskrequiredaudit or design
popupsrequiredThe popups, verbatim. Past 6,000 characters the page cuts the middle on line boundaries and leaves a [polite-popup: ...] marker.
factsrequiredA JSON string: popups (name, purpose, format, trigger, triggers, delay_s, scroll_pct, fields, copy), goal_basis, areas (timing, value, respect, targeting, mobile, compliance: found, evidence, note), found, rules, counts, metrics, benchmark, bench_verdict, flags, notes. It is Popkit.factsOf(A).
contextoptionalThe site, the traffic and the popup's numbers. Cut past 4,000 characters.
goaloptionalThe main popup's purpose when the user chose it: email_capture, lead_magnet, discount, announcement, feature_promo or feedback.
questionoptionalWhat the user wants to know.
audit_handoffdesign onlyA JSON string of an audit's verdict, bad[] (areas), issues[{flag, finding, fix}] and fixes[], for the same input.
retry_noteoptionalOnly on a reformat retry: what was wrong with the previous reply.

The reply contracts

Both lanes share an envelope: task, headline, questions[], summary.

task: "audit"

inventory[{name, purpose, format, trigger, read}] (one per popup, in order), areas[{area: timing|value|respect|targeting|mobile|compliance, status: good|weak|bad|unknown, finding, evidence[], fix}] (exactly six, in order), numbers{conversion_rate, benchmark, verdict: below|within|above|unknown, read}, issues[{flag, kind: timing|value|respect|targeting|mobile|compliance|performance|measurement, severity: high|medium|low, finding, impact, evidence[], fix}], fixes[] (3 to 6), verdict (keep|fix_first|rebuild).

task: "design"

source (audit handoff|from the input), goal{purpose, why}, popups[{name, purpose, format: center_modal|slide_in|bar|inline, trigger{kind: time|scroll|exit|click|pages|behavior, detail}, audience, pages, frequency, copy{headline, subhead, cta, decline}, fields[], design, mobile}] (1 to 3), conflict_rules[], compliance[] (2 to 5), measurement[{metric, definition}] (3 to 6), tests[{test, hypothesis, metric}] (2 to 4).

Worked examples

These are the page's own bundled examples (invented sites and numbers), with the input as app.js builds it and the saved model reply the page replays for free.

Audit: a plant shop's full-screen discount on page load, every page, checkout included

\n

Input (popups and facts shortened here):

{
 "task": "audit",
 "popups": "Popup 1: Welcome discount\nFormat: a full-screen overlay with a photo of a monstera.\nTrigger: shows on page load, on every page view, sitewide including the cart and checkout.\nHeadline: \"Welcome to Mossbrindle!\"\nBody: \"Get 10% off your first order.\"\nForm fields: first name, email, phone number and birthday.\nA pre-checked box under the form: \"Text me plant care tips and offers.\"\nCTA: \"Submit\"\nDecline link: \"No thanks, ... (693 characters in all)",
 "facts": "(a JSON string; decoded: 2 popup(s) (Popup 1: Welcome discount: discount, full_screen, page_load; Popup 2: Exit offer: discount, center_modal, exit), 6 of 6 areas stated, conversion 1.27% against 2-5% (below), flags PU-01, PU-04, PU-07, PU-11, PU-13, PU-19, PU-08, PU-09, PU-10, PU-14, PU-17, PU-21, PU-16, PU-18, PU-20)",
 "context": "Mossbrindle - an online shop for houseplants and pots (B2C e-commerce). Average order is $46.\nAbout 72% of our traffic is mobile, and on phones the welcome popup is the same full-screen overlay.\nLast 30 days: 48,000 popup impressions, 610 signups from the welcome popup.\nWe get emails from customers who say the popups are annoying.\n",
 "question": "Why is our popup converting so badly when it's on every page?"
}
\n

Reply:

\n
{
 "task": "audit",
 "inventory": [
  {
   "name": "Popup 1: Welcome discount",
   "purpose": "discount",
   "format": "full_screen",
   "trigger": "page_load",
   "read": "A full-screen first-order discount that fires on page load on every page view, including cart and checkout, and asks for four fields behind a faint, delayed close."
  },
  {
   "name": "Popup 2: Exit offer",
   "purpose": "discount",
   "format": "unclear",
   "trigger": "exit",
   "read": "An exit-intent repeat of the same first-order discount; the paste does not state its format, so the page's center_modal reading is a guess."
  }
 ],
 "areas": [
  {
   "area": "timing",
   "status": "bad",
   "finding": "The Welcome discount interrupts visitors on page load before they have seen a single plant.",
   "evidence": [
    "Trigger: shows on page load, on every page view"
   ],
   "fix": "Fire it after a scroll depth of 25-50% or a time delay of 30-60 seconds, never on page load."
  },
  {
   "area": "value",
   "status": "bad",
   "finding": "The headline states no benefit, the button is generic, the form asks for four fields and the discount has no deadline.",
   "evidence": [
    "Welcome to Mossbrindle!",
    "Form fields: first name, email, phone number and birthday.",
    "CTA: \"Submit\""
   ],
   "fix": "Lead with the offer in the headline, ask for email only, use a first-person button like \"Claim my [discount]\" and add a [code expiry]."
  },
  {
   "area": "respect",
   "status": "bad",
   "finding": "The popup shows on every page view with no cap, the close is faint and delayed, the decline link guilt-trips, two popups have no rule between them and customers complain.",
   "evidence": [
    "The close X is light grey and only appears after 5 seconds.",
    "No thanks, I prefer paying full pr
... (shortened here; the page shows it whole)
\n

Design: the same popups, carrying the audit

\n

Input (with the audit handed over):

{
 "task": "design",
 "popups": "Popup 1: Welcome discount\nFormat: a full-screen overlay with a photo of a monstera.\nTrigger: shows on page load, on every page view, sitewide including the cart and checkout.\nHeadline: \"Welcome to Mossbrindle!\"\nBody: \"Get 10% off your first order.\"\nForm fields: first name, email, phone number and birthday.\nA pre-checked box under the form: \"Text me plant care tips and offers.\"\nCTA: \"Submit\"\nDecline link: \"No thanks, ... (693 characters in all)",
 "facts": "(a JSON string; decoded: 2 popup(s) (Popup 1: Welcome discount: discount, full_screen, page_load; Popup 2: Exit offer: discount, center_modal, exit), 6 of 6 areas stated, conversion 1.27% against 2-5% (below), flags PU-01, PU-04, PU-07, PU-11, PU-13, PU-19, PU-08, PU-09, PU-10, PU-14, PU-17, PU-21, PU-16, PU-18, PU-20)",
 "context": "Mossbrindle - an online shop for houseplants and pots (B2C e-commerce). Average order is $46.\nAbout 72% of our traffic is mobile, and on phones the welcome popup is the same full-screen overlay.\nLast 30 days: 48,000 popup impressions, 610 signups from the welcome popup.\nWe get emails from customers who say the popups are annoying.\n",
 "question": "Why is our popup converting so badly when it's on every page?",
 "audit_handoff": "{\"verdict\":\"rebuild\",\"bad\":[\"timing\",\"value\",\"respect\",\"targeting\",\"mobile\",\"compliance\"],\"issues\":[{\"flag\":\"PU-04\",\"finding\":\"The Welcome discount shows on every page view with no frequency cap.\",\"fix\":\"Cap it at once per session and wait 7-30 days after a dismissal.\"},{\"flag\":\"PU-01\",\"finding\":\"The Welcome discount f..."
}
\n

Reply:

\n
{
 "task": "design",
 "source": "audit handoff",
 "goal": {
  "purpose": "discount",
  "why": "The main popup offers 10% off a first order to new Mossbrindle shoppers, so the design keeps that offer and fixes how and when it is shown."
 },
 "popups": [
  {
   "name": "Welcome discount",
   "purpose": "discount",
   "format": "slide_in",
   "trigger": {
    "kind": "scroll",
    "detail": "Slides in from the bottom corner once a first-time visitor scrolls 50% of a product or collection page, or after 30-60 seconds on the site, whichever comes first."
   },
   "audience": "New visitors only; never existing subscribers, past customers, anyone who already claimed the code or anyone who dismissed it recently.",
   "pages": "Product, collection, home and blog pages; never the cart, checkout, payment or account pages.",
   "frequency": "At most once per session, and after a dismissal or a signup it stays away for 30 days.",
   "copy": {
    "headline": "Get 10% off your first plant order",
    "subhead": "One-time code, valid until [code expiry]. No spam, unsubscribe any time.",
    "cta": "Claim my 10% off",
    "decline": "No thanks"
   },
   "fields": [
    "email"
   ],
   "design": "A compact card about 400-600px wide on desktop with a small monstera photo beside the form. A clear, high-contrast close X at the top right from the first moment, plus click outside and Esc to close.",
   "mobile": "A bottom slide-up covering only the lower part of the screen, with a large tappable close and a full-width button; never a full-screen overlay."
  },
  {
   "name": "Exit offer",
   "purpose": "email_capture",
   "format": "center_modal",
   "trigger": {
    "kind": "exit",
    "detail": "On desktop, when the mouse moves toward the browser tabs, only for visitors who were not shown
... (shortened here; the page shows it whole)
\n

Audit: a B2B blog slide-in with good manners and a weak pitch

\n

Input:

{
 "task": "audit",
 "popups": "Blog newsletter slide-in (live on every blog post)\n\nFormat: a slide-in from the bottom-right corner.\nTrigger: when the reader scrolls past 50% of the article.\nHeadline: \"Subscribe to our newsletter\"\nBody: \"Get new posts in your inbox.\"\nFields: name, work email and company.\nButton: \"Submit\"\nClose: an X in the top corner; clicking outside closes it too.\nFrequency: once per session; after someone closes it, it won't sho... (645 characters in all)",
 "facts": "(a JSON string; decoded: 1 popup(s) (The popup: email_capture, slide_in, scroll), 6 of 6 areas stated, conversion 1.6% against 2-5% (below), flags PU-15, PU-17, PU-21, PU-16)",
 "context": "Quillstack - documentation software for engineering teams (B2B SaaS). The blog gets most of our organic traffic.\nLast quarter the slide-in had 21,400 impressions and 342 submissions.\n"
}
\n

Reply:

\n
{
 "task": "audit",
 "inventory": [
  {
   "name": "The popup",
   "purpose": "email_capture",
   "format": "slide_in",
   "trigger": "scroll",
   "read": "The blog newsletter slide-in fires at 50% scroll on every blog post and asks for name, work email and company."
  }
 ],
 "areas": [
  {
   "area": "timing",
   "status": "good",
   "finding": "A 50% scroll depth sits inside the skill's 25-50% range and waits for a reader who is engaged.",
   "evidence": [
    "Trigger: when the reader scrolls past 50% of the article."
   ],
   "fix": ""
  },
  {
   "area": "value",
   "status": "weak",
   "finding": "The headline asks for a subscription without saying what the reader gets, the form asks for three fields and the button says Submit.",
   "evidence": [
    "Subscribe to our newsletter",
    "Fields: name, work email and company.",
    "Button: \"Submit\""
   ],
   "fix": "Lead with the benefit, ask for work email only and change the button to a first-person action such as \"Send me new posts\"."
  },
  {
   "area": "respect",
   "status": "weak",
   "finding": "The close, the session cap and the 14-day wait are right, but there is no polite \"No thanks\" decline link.",
   "evidence": [
    "Close: an X in the top corner; clicking outside closes it too.",
    "Frequency: once per session; after someone closes it, it won't show again for 14 days."
   ],
   "fix": "Add a polite \"No thanks\" link under the button and make sure Esc also closes it."
  },
  {
   "area": "targeting",
   "status": "good",
   "finding": "It runs only on blog posts and known subscribers are excluded.",
   "evidence": [
    "live on every blog post",
    "Existing subscribers don't see it (we exclude known subscribers by cookie)."
   ],
   "fix": ""
  },
  {
   "area": "mobile",
   "status": "goo
... (shortened here; the page shows it whole)
\n

Design: a candle shop's first popups, from a rough idea

\n

Input:

{
 "task": "design",
 "popups": "We have no popup yet and want to start collecting emails on the Tallow & Wick site. Rough idea:\n\n- An email popup offering free shipping on the first order\n- Maybe a second one for people leaving the product pages\n- Our newsletter goes out every two weeks with new scents and restock alerts\n",
 "facts": "(a JSON string; decoded: 1 popup(s) (The popup: discount, center_modal, unclear), 3 of 6 areas stated, conversion unknown against 2-5% (unknown), flags PU-03, PU-05, PU-18, PU-20, PU-22)",
 "context": "Tallow & Wick - hand-poured soy candles sold online (B2C e-commerce). Most orders are between $30 and $60.\nTraffic is mostly from Instagram and Pinterest; about 65% of visits are on mobile.\nNew scents sell out within days, and restock alerts are what our customers ask for most.\n",
 "goal": "discount"
}
\n

Reply:

\n
{
 "task": "design",
 "source": "from the input",
 "goal": {
  "purpose": "discount",
  "why": "You chose a discount, and free shipping on the first order is the offer you want to lead with to start collecting emails."
 },
 "popups": [
  {
   "name": "First-order free shipping",
   "purpose": "discount",
   "format": "center_modal",
   "trigger": {
    "kind": "scroll",
    "detail": "Appears once a first-time visitor has scrolled 50% of a product or collection page."
   },
   "audience": "New visitors only; never people already on the list, past customers, or anyone who dismissed it recently.",
   "pages": "Product and collection pages and the blog; never the cart, checkout, payment or account pages.",
   "frequency": "At most once per session; after a dismissal, wait 30 days before showing it again.",
   "copy": {
    "headline": "Free shipping on your first Tallow & Wick order",
    "subhead": "Plus new scents and restock alerts every two weeks. Code is single use and expires [code expiry]. No spam, unsubscribe any time.",
    "cta": "Claim my free shipping",
    "decline": "No thanks"
   },
   "fields": [
    "email"
   ],
   "design": "A 400-600px wide modal with a photo of a lit candle on one side and the form on the other; a large close X top right, click outside and Esc also close it.",
   "mobile": "A bottom slide-up covering the lower part of the screen with a large close X and full-width button, never full screen."
  },
  {
   "name": "Restock alert",
   "purpose": "email_capture",
   "format": "slide_in",
   "trigger": {
    "kind": "exit",
    "detail": "On desktop, when the cursor heads for the browser bar on a product page, only for visitors who have not seen the free shipping popup this session."
   },
   "audience": "Product page visitors who have not s
... (shortened here; the page shows it whole)
\n

The page re-checks the reply; the API does not

The page holds every reply to account (recon.js): for an audit, one inventory entry per popup the page read and the goal you chose; six areas in order, none called good under a high or medium flag and every high flag's area called bad, nothing claimed for an area the paste never states without a quote; the conversion rate, benchmark and verdict equal to the page's arithmetic; every high and medium flag carried and a high one never downgraded; rebuild when two high flags stand or three areas are bad, and no "keep" while a high flag stands. For a design, never full screen and never on page load, a 30-60 second delay or a 25-50% scroll, a phone alternative for exit intent, checkout excluded, a frequency cap, a benefit headline, a specific button, a polite decline, at most two fields, conflict rules for two or more popups, consent and privacy for an email form, conversion rate measured, tests with metrics, and the handed-off audit answered. In both, quotes are checked word for word and no count, rate, discount or window may be invented beyond the skill's constants. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same input into the page.

\n

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope.

\n
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://polite-popup.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
\n

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

\n
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"polite-popup"}' | jq -r '.data.token')
\n

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

\n
api GET /me | jq '.data | {subject_type, credits}' 
\n

Step 4 — POST /estimate

Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.

\n
# input.json: the run input object - {"task": "audit", "popups": "...", "context": "...", "facts": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 
\n

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses polite-popup:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.

\n
KEY="polite-popup:audit:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}' 
\n

Step 6 — POST /run-stream (design)

The same run as Server-Sent Events. design.json is a design input, with audit_handoff when it follows an audit.

\n
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: polite-popup:design:$(shasum -a 256 design.json | cut -c1-16):a1" --data @design.json
\n