Quiz lead-gen widget for SaaS landing pages#
“Subscribe to our newsletter” is the lowest-converting CTA in B2B SaaS — single-digit click rates, drop-offs in the form. The interactive quiz pattern (HubSpot, ConvertKit, BuzzFeed-for-B2B) gets 5-10× higher email opt-in because the user has already invested 60 seconds answering questions; surrendering an email to see results is a tiny next step.
This guide builds a drop-in <script>+<div> widget your customers can paste on their landing page. Five trivia questions on a topic they care about, score reveal gated by email, score + personalized results delivered to inbox. About 120 lines of vanilla JS, zero framework dependency.
What you’ll build#
A self-contained widget that:
- Pastes onto any landing page with one
<script>tag + one<div>container - 5 questions on a configurable topic (
data-categoryattribute → QuizBase category) - Score tracking + question-by-question feedback
- Email gate at question 5 — “Where should we send your detailed results?”
- POSTs
{email, score, questionIds, answers}to your CRM webhook (Mailchimp, ConvertKit, Beehiiv, custom endpoint) - Personalized recap email can replay the exact quiz (using stored
questionIds) for “here’s where you struggled” follow-up - Per-question attribution rendered on every card
The mechanic — stable IDs enable personalized follow-up#
When the user finishes the quiz, your CRM webhook receives { email, score: 4, questionIds: ['01HXY...', '01HYZ...'], answers: ['B', 'A', ...] }. Your follow-up email can:
- Fetch each question by ID via
GET /api/v1/questions/{id}— exact same question text, correct answer, attribution - Compare
answers[i]tocorrectAnswerfor each — render a “you answered A but the correct was C, here’s why” personalized recap - Embed a “retry the same quiz” link that uses the same
questionIdsso the user sees the same questions
Without stable IDs you’d have to send the full question text in the webhook payload (huge, hard to template). With IDs your webhook payload is small, and your follow-up renders fresh from QuizBase. See /docs/api/questions-by-id for the full pattern catalogue.
Stack#
- Vanilla JS — no React, no build step, drops into any HTML page
- QuizBase publishable key (
qb_pk_*) — the key is in the widget so any customer who installs your widget uses your key. For SaaS where each customer should bring their own key, exposedata-api-key="..."as an attribute. - CRM webhook URL — Mailchimp, ConvertKit, Beehiiv, or your own endpoint accepting JSON POST
Step 1 — Set up the widget container HTML#
Your customer pastes this onto their landing page (e.g. inside their newsletter signup section):
<div
id="qb-widget"
data-api-key="qb_pk_..."
data-category="marketing"
data-difficulty="medium"
data-webhook="https://api.yourservice.com/leads"
></div>
<script src="https://cdn.yourservice.com/quizbase-widget.js" async></script> You host quizbase-widget.js on your CDN. Customers paste two lines and the widget renders inline.
Step 2 — Build the widget script#
// quizbase-widget.js (your CDN serves this)
(function () {
const el = document.getElementById('qb-widget');
if (!el) return;
const KEY = el.dataset.apiKey;
const CATEGORY = el.dataset.category || 'general-knowledge';
const DIFFICULTY = el.dataset.difficulty || 'medium';
const WEBHOOK = el.dataset.webhook;
let questions = [];
let index = 0;
let answers = [];
async function loadQuestions() {
const url = `https://quizbase.runriva.com/api/v1/questions/random?category=${CATEGORY}&difficulty=${DIFFICULTY}&amount=5`;
const r = await fetch(url, { headers: { 'X-API-Key': KEY } });
const json = await r.json();
questions = json.data;
render();
}
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function render() {
if (index === questions.length) return renderEmailGate();
const q = questions[index];
const choices = shuffle([q.correctAnswer, ...q.incorrectAnswers]);
el.innerHTML = `<h3>Q${index + 1}/${questions.length}: ${q.text}</h3>
${choices
.map(
(c, i) =>
`<button data-choice="${encodeURIComponent(c)}">${'ABCD'[i]}. ${c}</button>`
)
.join('<br>')}
<small>Source: ${q.attribution.author} (${q.attribution.license})</small>`;
el.querySelectorAll('button').forEach((b) =>
b.addEventListener('click', () => {
const choice = decodeURIComponent(b.dataset.choice);
answers.push(choice);
index++;
render();
})
);
}
loadQuestions();
})(); The <small>Source: ... line on every question card is required — QuizBase content ships under CC BY-SA / CC BY / MIT and surfacing source is a license requirement. See /data.
Step 3 — Email gate at question 5#
The renderEmailGate() function shows after the last answer. Computing the score is one loop:
function renderEmailGate() {
const score = answers.filter((a, i) => a === questions[i].correctAnswer).length;
el.innerHTML = `<h3>You scored ${score} / ${questions.length}!</h3>
<p>Enter your email to see which ones you got wrong + a personalized study plan:</p>
<input type="email" id="qb-email" placeholder="you@example.com" required />
<button id="qb-submit">Show my results</button>`;
el.querySelector('#qb-submit').addEventListener('click', async () => {
const email = el.querySelector('#qb-email').value;
if (!email.match(/^[^@]+@[^@]+.[^@]+$/)) {
alert('Please enter a valid email');
return;
}
await fetch(WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
score,
total: questions.length,
questionIds: questions.map((q) => q.id),
answers
})
});
renderResults(score);
});
} Step 4 — Render personalized results#
After the webhook fires, show the user what they got wrong inline (and confirm the email is queued):
function renderResults(score) {
const wrong = questions
.map((q, i) => ({ q, given: answers[i], correct: q.correctAnswer }))
.filter((r) => r.given !== r.correct);
el.innerHTML = `<h3>Score: ${score} / ${questions.length}</h3>
<p>We sent the full breakdown to your inbox. Quick preview:</p>
<ul>${wrong
.slice(0, 2)
.map(
(r) =>
`<li><b>${r.q.text}</b><br>You: ${r.given} · Correct: ${r.correct}<br><small>Source: ${r.q.attribution.author} (${r.q.attribution.license})</small></li>`
)
.join('')}</ul>
<p>Check your email for the full study plan.</p>`;
} Step 5 — Configure your CRM webhook#
The webhook receives JSON with { email, score, total, questionIds, answers }. What you do with it depends on your CRM:
- ConvertKit —
POST /v3/forms/{id}/subscribewithemail+ custom fields forscoreandquiz_id - Mailchimp —
PUT /3.0/lists/{id}/members/{md5_lower(email)}with merge fields - Beehiiv —
POST /v2/publications/{id}/subscriptionswithemailand tags array
Your follow-up email template fetches each question by ID, renders inline, and sends. With Mailchimp Email Designer you can use merge tags for dynamic content; with Postmark/Resend a templating engine like Handlebars works.
Pitfalls#
- The API key is visible in the embed — any customer running your widget exposes your
qb_pk_*to their landing-page visitors. Free tier (500 req/day) caps at 100 quiz completions/day across all installed sites. For scale, upgrade or pass-through customer-supplied keys viadata-api-key. - CORS — QuizBase REST has CORS open for
GET. Your webhook needs CORS configured to accept POST from anywhere (or restrict by customer domain via Origin header). - Email validation in the widget is regex-only — production should also verify deliverability via DeBounce or similar after the webhook. Bounce rate matters for your sender reputation.
- Free tier shared across all customers — for B2B SaaS distributing this widget, customer-supplied API keys (one per install) is the right model.
What next#
- A/B test the email gate placement — gate at Q3 (early) vs Q5 (end) — see which converts better for your audience
- Add a personality-quiz variant — for “what type of [your category] are you?” framing where the right answer is the personality archetype not a correct answer. Use
?tags=to filter for archetype-specific questions. - Multi-language — pass
data-langso the widget renders in the user’s language. See languages and translations. - Pair with daily email digest — sign-ups from the quiz auto-enroll in a daily trivia drip series.
Ready to ship? Grab a free publishable key, copy the quizbase-widget.js from Step 2, host on your CDN, send your customers the two-line embed. Bug? Each question has a stable id — wire POST /api/v1/report into your CRM email “report this question” link.