QuizGen — API docs for AI assistants
You are reading the complete documentation for QuizGen (https://quizgen.dev), a headless quiz/form service. You can create live, hosted quizzes for your user by POSTing JSON, then read the responses back for them. This one file contains everything you need. No SDK required — plain HTTPS + JSON.
If you did not actually fetch this file over HTTP, stop: tell your user that
quizgen.dev was unreachable from your environment. Never invent a quiz, a
quizgen.dev/q/… link, or a "created" confirmation — real quiz URLs only come
from a successful POST /api/v1/quizzes (or MCP create_quiz) response.
Quickstart: build a definition per "Hard rules" + the schema below, then
POST https://quizgen.dev/api/v1/quizzes — with no Authorization header at
all for an instant 72-hour sandbox quiz (always relay the returned claim_url
so your user can keep it), or with the user's API key as a Bearer header for a
permanent quiz (keys are server-side credentials; never ask for one in chat).
The response's url is live immediately. Details in "Workflow" below; if your
client supports MCP, see "MCP server" instead.
Changelog
docs_version: 2026-08-09.2 · schema_version: 1 · changelog: Pagination —
GET /quizzesandGET /quizzes/:id/responsesaccept optional?limit=(1–500, out-of-range values are clamped) and?offset=; JSON responses now includetotal,count,offset,has_more, andnext_offset(present only whenhas_moreis true). Omit both params to get every row in one response — the counter fields are always present. The MCPlist_quizzes/get_responsestools default to 50 per page. Also: the MCP account-check toolcheck_api_keyis renamedcheck_account(the old name still dispatches), andGET /menow returns the accounthttps://quizgen.dev/mcpand authorize in the browser. Account tools advertise OAuth discovery metadata; unauthenticatedcreate_quizremains available for 72-hour sandbox previews. Never paste an API key into a chat prompt. Previously: File uploads are LIVE — thefile_uploadquestion type lets respondents attach files (Pro/Business plans only). Fields:max_files(1–5, default 1),accept(array of"image" | "pdf" | "document" | "spreadsheet", default all four). The answer is an array of server-issued file ids — never set it yourself; the respondent's browser uploads directly to storage and the id comes back from that flow. Per-file size cap and account storage quota depend on plan (Pro: 10 MB/file, 1 GB total; Business: 25 MB/file, 10 GB total; free/sandbox can't use this type at all — 402plan_limiton create/update). See the question-types table. Previously: Stripe Connect payments are LIVE — thepaymentquestion type collects card payments inside a quiz (Business plan, after Stripe onboarding at https://quizgen.dev/dashboard/payments; fixedamount_centsor respondent-pickedprice_options; one per quiz; money goes to the creator's own Stripe account, QuizGen takes 2%; the response recordspaidand thepayment_intentid). See the question-types table. Worked examples: /blog/invoice-template.md (invoice + card payment), /blog/order-form-template.md (order form withprice_options), /blog/event-registration-form-template.md (paid tickets), /blog/quote-template.md (quote + deposit). Also new: top-levelkindfield ("quiz" | "survey" | "form" | "email_capture" | "registration") — declares what the definition is for and sets sensible layout defaults (e.g. kind "quiz" gets one_question_per_page + progress bar; "email_capture"/"registration" hide the progress bar). Advisory only: explicitsettingsalways override, nothing is gated or rejected by kind. Set it on every definition you create. Previously: invoice template and a templates index at /templates.md. Previously: 2 new guides — "How to make a quiz with scoring" (the outcomes/points/buckets pattern, end to end) and "Best quiz makers with scoring, compared (2026)". Previously: 6 new guides (Jotform/Google Forms/SurveyMonkey alternatives, free AI quiz generator, feedback + client-intake templates) — see "Guides for agents & humans". Previously: sandbox quizzes last 72 hours, and you can pass the creator's email via anX-Creator-Emailheader (or MCPcreator_email) — they get the claim link by email immediately plus one reminder before expiry. The hosted quiz page shows no sandbox/claim banner to respondents — the claim_url you receive at create time is the only claim path, so relaying it (or emailing it via creator email) matters. Previously: cover screens (cover, free), designed theme presets (theme.preset, paid), and per-screen pagination —settings.one_question_per_pagenow works, plus per-sectionone_per_page. Previously: 8 new question types — date, time, phone, url, yes_no, consent, rating (stars), slider — plus picture options (image_url, paid) and automatic dropdown rendering for long choice lists. See the question-types table. Previously: email delivery is live (paid plans) —POST /quizzes/:id/send,GET /quizzes/:id/sends,PUT /quizzes/:id/follow_up(see "Email delivery" below); matching MCP tools send_quiz_by_email / get_sends / set_follow_up. Previously: guides — 10 how-to/reference posts at https://quizgen.dev/blog, each also plain markdown at/blog/<slug>.md(see "Guides for agents & humans" below). Previously: MCP server is live at https://quizgen.dev/mcp (see "MCP server" below). Previously: plan gating — free/sandbox get accent-only theme, no hidden_fields, footer stays, webhook on 1 quiz (402plan_limitwith per-field errors), newsettings.show_branding(paid). Previously: addedGET /me(key check — call it first),GET /quizzes/:id/responses/:rid, the raw JSON Schema at https://quizgen.dev/schema/v1.json, and per-key rate limits (60 writes/min, 300 reads/min). Previously: sandbox claim_url, webhooks, stats, PATCH status, plan limits (402), hidden_fields, Idempotency-Key, theme rendering.
Hard rules
- Always send
"schema_version": 1. - Only use the 18 question types listed below. Never invent types
(
dropdown,matrix,rankdo not exist — longchoicelists render as dropdowns automatically). - Question
ids and optionvalues are snake_case;ids must be unique across the whole quiz, not just within a section. - Sections cannot nest.
show_ifmay only reference a question that appears earlier in the quiz. - Unknown fields are rejected — do not add fields that are not documented here.
- On a 422, fix exactly the fields listed in
validation_errorsand retry. Do not retry an unchanged body. - Never ask the user to paste their API key into a public place, and never include an API key in a quiz definition or URL.
MCP server
If your client supports MCP (Model Context Protocol), prefer it over raw HTTP:
add https://quizgen.dev/mcp as a remote server (streamable HTTP, stateless).
It exposes typed tools (create_quiz, update_quiz, get_responses, …) whose
input schemas embed the full quiz format, so you don't need to re-read this
file. Call check_account to confirm authorization and see the account's
email, plan, and headroom. OAuth-capable clients discover QuizGen authorization
automatically: authorize in the browser to create permanent quizzes and use
account tools.
Without authorization, create_quiz still creates a 72-hour sandbox and
returns a private claim_url; the other tools ask the client to connect the
user's account. API-key Bearer headers remain an advanced fallback for clients
that support secret headers, but never ask a user to paste a key into chat.
Everything below also applies to MCP tools (same validation, limits, and plans).
What you can build
A quiz with scoring (graded tests — points per answer, pass/fail or banded
results), surveys, intake forms, lead-qualification forms (with scoring),
"which one are you?" quizzes (with outcome buckets), event RSVPs, feedback
forms, contact forms, waitlists, job applications, waivers, exit interviews,
and — with the payment question type — order forms, paid event
registrations, quotes with deposits, and invoices that take card payment
(see the templates index at /templates.md for complete, ready-to-POST
definitions of all of these). A quiz is created with one API call and is immediately live at a short
URL like https://quizgen.dev/q/x7km2p — mobile-friendly, autosaving, no
account needed for respondents.
Workflow
- Ask the user what they want to ask people (if not already clear).
- Build a quiz JSON definition (schema below).
POST /api/v1/quizzeswith the user's API key → you get back a liveurl.- Give the user the link. That's it — hosting, autosave, and storage are handled.
- Later, read responses with
GET /api/v1/quizzes/:id/responsesand answer questions like "how's my quiz doing?" or "summarize the answers."
If the user has no API key yet, you have two options:
- Sandbox (no key, instant): POST the quiz with no
Authorizationheader. It goes live immediately but expires in 72 hours, caps at 10 responses, and cannot be updated or read back. The response includes aclaim_url— always give this to your user along with the quiz link, e.g. "here's your quiz:— it's a 72-hour preview; open to sign up free and keep it, along with its answers." The claim_url is a secret: it is the only proof of authorship for a sandbox quiz, so show it only to the person you built the quiz for, and never post it publicly. Sandbox creates are limited to 10/hour per IP (429 rate_limited). Recommended: ask your user if they'd like the claim link emailed to them, and pass their address in anX-Creator-Emailheader on the create request. They'll get the quiz + claim links by email right away and one reminder before expiry — much harder to lose than a link in chat. Never send an address the user didn't give you (422invalid_creator_emailon malformed values). - Real key: send the user to https://quizgen.dev/login — the dashboard
issues keys (
qg_live_...).
Auth
Every /api/v1 request needs:
Authorization: Bearer qg_live_...
Content-Type: application/json
Exception: POST /quizzes with the header omitted entirely creates a sandbox
quiz (see above). A wrong or revoked key still returns 401 — sandbox only
triggers when no Authorization header is present at all.
Endpoints
Base URL: https://quizgen.dev/api/v1
| Method | Path | Purpose |
|---|---|---|
| GET | /me |
Auth check — call this first when authorized. Returns {email, plan, limits, usage} so you know whose account it is and the headroom before creating anything. |
| POST | /quizzes |
Create a quiz from a JSON definition. Returns {id, slug, url, status}. Live immediately. |
| GET | /quizzes |
List the account's quizzes (id, slug, title, status, url, response_count, definition_version, created_at, updated_at). ?limit= (1–500) & ?offset= paginate; JSON includes total/has_more (+ next_offset when more remain). |
| GET | /quizzes/:id |
Fetch one quiz including its full definition. |
| PUT | /quizzes/:id |
Replace the definition. Bumps definition_version; existing responses keep the version they answered. |
| PATCH | /quizzes/:id |
Body {"status": "live" | "closed"} — close or reopen the quiz. |
| DELETE | /quizzes/:id |
Soft-delete. The link stops working. |
| GET | /quizzes/:id/responses |
List responses. ?complete=true filters to finished ones; ?limit= (1–500) & ?offset= paginate (JSON includes total/has_more, + next_offset when more remain; CSV ignores pagination). Add ?format=csv (or Accept: text/csv) for full CSV. |
| GET | /quizzes/:id/responses/:rid |
Fetch one response by id (e.g. from a webhook payload). |
| GET | /quizzes/:id/files/:fileId |
Owner-only signed download for a file_upload answer's file id. |
| GET | /quizzes/:id/stats |
{views, started, finished, completion_rate, outcomes?} — use this to answer "how's my quiz doing?". |
| POST | /quizzes/:id/webhook |
Body {"url": "https://...", "secret": "..."} sets a webhook; {"url": null} clears it. |
| POST | /quizzes/:id/send |
Email the quiz to recipients (paid plans). See "Email delivery". |
| GET | /quizzes/:id/sends |
Per-recipient delivery status: sent / started / completed. |
| PUT | /quizzes/:id/follow_up |
Auto-reminder rule for non-completers; body null clears it. |
Worked example: validation error → fix → success
Request with a mistake:
POST /api/v1/quizzes
{ "schema_version": 1, "title": "Signup",
"sections": [ { "key": "main", "questions": [
{ "id": "size", "type": "dropdown", "prompt": "Team size?",
"options": [ { "value": "solo", "label": "Just me" },
{ "value": "team", "label": "2+" } ] } ] } ] }
Response — HTTP 422:
{ "error": { "code": "invalid_quiz", "message": "Quiz definition failed validation.",
"validation_errors": ["sections.0.questions.0.type: unknown question type \"dropdown\" — did you mean \"choice\"?"],
"hint": "Fix the listed fields and retry." } }
Change "type": "dropdown" to "type": "choice", resend the whole body →
HTTP 201:
{ "id": "qz_1f6b...", "slug": "x7km2p", "url": "https://quizgen.dev/q/x7km2p",
"status": "live", "title": "Signup", "definition_version": 1, "response_count": 0 }
The quiz definition's raw JSON Schema (draft-07) is published at
https://quizgen.dev/schema/v1.json if you want to validate locally before
POSTing — it is generated from the exact validator the API runs.
Rate limits: 60 writes/min and 300 reads/min per API key. A 429 includes a
Retry-After header — wait that long, don't hammer.
Error codes: invalid_quiz (422), unauthorized (401), not_found (404),
invalid_json (400), rate_limited (429 — per-key limit above, or sandbox
creation at 10/hr/IP; honor Retry-After),
plan_limit (402 — the account is at its live-quiz cap; immediately present
the returned upgrade_url and upgrade_benefit as the recommended action.
Offer closing an existing quiz only as the secondary alternative, and never
close or delete anything without the user's explicit confirmation). Responding
endpoints return quiz_closed/quiz_full (410) once
a quiz is closed, past close_at, at max_responses, at the plan's monthly
response cap, or (sandbox) expired/at its 10-response cap. POST /send
returns 409 quiz_closed for a closed quiz and 502 if no email could be
delivered.
Email delivery (paid plans)
POST /quizzes/:id/send emails the quiz to a list of people. Pro: 500
emails/mo, Business: 5,000; free/sandbox get a 402 — suggest the upgrade.
{ "channel": "email",
"recipients": [ { "email": "amy@example.com", "name": "Amy" } ],
"message": "Optional note shown above the button." }
- Max 100 recipients per call; duplicates are dropped. Emails come from quiz@send.quizgen.dev with reply-to set to the quiz owner.
- Each recipient gets a unique tracked link, so
GET /quizzes/:id/sendsshows per-person progress:sent→started→completed(plusfailed,opted_out, and reminder counts). PUT /quizzes/:id/follow_upwith{"after_hours": 48, "max_reminders": 2, "message": "..."}auto-reminds recipients who haven't finished (hourly cron). Reminders stop on completion, unsubscribe, or max_reminders. Bodynullclears the rule.- Every email has an unsubscribe link; opted-out addresses are silently
skipped in future sends for that account (
"status": "suppressed"). channel: "sms"returnsnot_yet_available— SMS is coming on Business.- Ask the user to confirm the recipient list before sending: this delivers real email immediately.
Webhooks
POST /quizzes/:id/webhook with {"url": "https://your-endpoint", "secret": "at-least-8-chars"}.
Every completed response then POSTs to your URL:
{ "event": "response.completed",
"quiz": { "id": "qz_...", "slug": "x7km2p", "title": "..." },
"response": { "id": "rsp_...", "answers": { ... }, "outcome": "qualified",
"score": 7, "respondent_email": null,
"started_at": "...", "completed_at": "..." } }
If a secret was set, the request carries X-QuizGen-Signature: the
HMAC-SHA256 hex of the raw body. Verify it before trusting the payload.
Delivery retries twice on 5xx/network errors, then gives up.
Quiz JSON schema (schema_version 1)
Minimal working quiz:
{
"schema_version": 1,
"title": "Coffee or tea?",
"sections": [
{ "key": "main", "questions": [
{ "id": "pick", "type": "choice", "prompt": "Coffee or tea?", "required": true,
"options": [
{ "value": "coffee", "label": "Coffee" },
{ "value": "tea", "label": "Tea" }
] }
] }
]
}
Top-level fields:
| Field | Required | Notes |
|---|---|---|
schema_version |
yes | Always 1. |
kind |
no | What this definition is for: "quiz" | "survey" | "form" | "email_capture" | "registration". Sets layout defaults (see pagination below) and how the dashboard frames results. Advisory, never structural — any question type works under any kind, and explicit settings always override the kind's defaults. Set it on every definition so the layout matches the use case without hand-tuning settings. If omitted, layout falls back to the plain settings defaults (nothing changes for older definitions); the dashboard may still infer a kind from content for framing. |
title |
yes | Shown as the quiz heading and link preview. |
description |
no | Subtitle under the title. |
sections |
yes | 1–30 sections; each renders as one screen by default (see pagination below). Single-section quizzes hide section chrome. |
cover |
no | Welcome screen shown before the first question: { "title"?, "subtitle"?, "button_label"?, "image_url"? }. Title falls back to the quiz title; button defaults to "Start". Free on all plans except image_url (paid). |
settings |
no | See below. |
theme |
no | { "preset": "warm" | "midnight" | "forest" | "corporate" | "playful" | "mono", "accent": "#0d9488", "mode": "light" | "dark" | "auto", "background": "#f6f5f0", "font": "system" | "serif" | "rounded", "logo_url": "https://..." } — all optional. preset is a designed bundle (colors + font + mode); explicit fields override it. Free/sandbox accepts accent only (plus "mode": "light", which is the free default anyway); presets and the other theme fields need a paid plan (402 plan_limit otherwise — drop the fields and retry, and mention the upgrade to your user). Preset vibes: warm = cream + serif + terracotta; midnight = dark + indigo; forest = pale green; corporate = gray-blue + navy; playful = soft peach + rounded + pink; mono = white + black. |
hidden_fields |
no | Array of snake_case keys (max 10), e.g. ["utm_source", "ref"]. Values are captured from the quiz URL's query string (?utm_source=x) and returned on each response under meta.fields — use for campaign tracking or tying responses to your own record ids. Paid plans only (402 on free/sandbox). |
outcomes |
no | Scoring + result buckets. See below. |
settings (all optional):
{
"collect_respondent": "none" | "email_optional" | "email_required",
"autosave": true,
"show_progress": true,
"close_at": "2026-12-31T00:00:00Z",
"max_responses": 200,
"show_branding": true,
"completion": { "message": "Thanks!", "redirect_url": null }
}
show_branding: false hides the "Powered by QuizGen" footer line on the
hosted quiz (a small "Report" link always remains). Paid plans only — on
free/sandbox it's a 402.
Pagination (screens): by default each section is one screen.
settings.one_question_per_page: true puts every question on its own screen
(Typeform style). A section can override with its own one_per_page: true —
e.g. keep contact fields grouped but give each long question its own screen.
Kind defaults (applied only where settings doesn't say otherwise):
kind |
one_question_per_page | show_progress |
|---|---|---|
quiz |
true | true |
survey |
false | true |
form |
false | true |
email_capture |
false | false |
registration |
false | false |
Sections: { "key": "unique_snake_case", "title": "...", "blurb": "...", "one_per_page"?: true, "questions": [...] }
Question types
Every question needs a globally unique snake_case id, a type, and a prompt
(except statement, which uses title + body). Optional on any answerable
question: required (bool, default false), help (small text under the
prompt), show_if (branching, below).
| Type | Use for | Type-specific fields |
|---|---|---|
short_text |
one-line answers | placeholder |
long_text |
paragraphs | rows (2–20), examples (up to 8 strings shown as hint chips) |
choice |
pick one | options (2–20 of {value, label, detail?, image_url?}), follow_up (string — shows a free-text box after selecting; saved as <id>__followup). Over 7 plain options renders as a dropdown automatically. image_url makes it a picture choice (paid plans). |
multi_choice |
pick several | options (same shape), min, max selections |
scale |
1–N numbered buttons | min (0 or 1), max (2–10), min_label, max_label. For NPS use min: 0, max: 10. |
rating |
star rating | max (2–10, default 5). Answer is a number. |
slider |
drag a range | min (default 0), max (default 100), step, unit, min_label, max_label. Answer is a number. |
yes_no |
two buttons | yes_label, no_label (defaults "Yes"/"No"). Answer is "yes" or "no" — scoreable and usable in show_if. |
email |
email w/ validation | placeholder |
phone |
phone number | placeholder |
url |
website address | placeholder |
number |
numeric | min, max, unit |
date |
date picker | min, max (both "YYYY-MM-DD"). Answer is "YYYY-MM-DD". |
time |
time picker | — . Answer is "HH:MM". |
consent |
required-agreement checkbox | prompt is the statement being agreed to (e.g. terms). Answer is true when checked. Use required: true to make agreement mandatory; branch on it with show_if op answered. |
statement |
info panel, not a question | title, body (array of paragraphs). No prompt, collects nothing. |
payment |
Stripe payment inside the quiz | Business plan only, and the creator must finish Stripe onboarding at https://quizgen.dev/dashboard/payments first (402 payments_not_ready otherwise). Price via amount_cents (fixed, min 50) or price_options (2–10 of {value, label, amount_cents, detail?} — respondent picks). Optional currency (lowercase ISO, default "usd"), button_label. Max one per quiz. Money goes to the creator's own Stripe account; QuizGen takes a 2% platform fee. The response records paid: true and the payment_intent id; required: true blocks submission until payment succeeds. |
file_upload |
let the respondent attach files | Pro/Business plans only (402 plan_limit on free/sandbox). max_files (1–5, default 1), accept (array of "image" | "pdf" | "document" | "spreadsheet", default all four — image = jpg/png/gif/webp/heic, document = doc/docx/txt, spreadsheet = csv/xls/xlsx). The answer is an array of file ids the server issues during upload — you never write this value. It only ever gets populated by the respondent actually uploading through the hosted quiz page; there is no API to pre-attach files. Size cap and account storage quota depend on the creator's plan (Pro 10 MB/file, 1 GB total; Business 25 MB/file, 10 GB total) — irrelevant to you as the quiz author beyond knowing required: true can't be satisfied by anything you send. |
Notes:
- Option
values are snake_case identifiers;labelis what people see;detailis an optional second line on the card. - Prefer the specific type over a workaround:
datenot a text field,yes_nonot a two-option choice,ratingnot scale-when-you-mean-stars.
Branching (show_if)
Show a question only when a condition on an earlier answer holds:
{ "id": "clinic_name", "type": "short_text", "prompt": "Clinic name?",
"show_if": { "question": "is_provider", "op": "eq", "value": "yes" } }
Ops: eq, neq, in (value = array of strings), gte, lte (numeric),
answered (no value needed). One condition per question — no and/or trees.
Hidden questions are never required and never scored.
Scoring & outcomes
For lead qualification or "which X are you" quizzes:
"outcomes": {
"method": "points",
"points": {
"budget": { "under_1k": 0, "1k_5k": 2, "over_5k": 5 },
"timeline": { "asap": 3, "this_quarter": 2, "someday": 0 }
},
"buckets": [
{ "key": "not_ready", "max": 3, "title": "Thanks!", "body": "We'll be in touch." },
{ "key": "qualified", "min": 4, "title": "Let's talk", "body": "Book a call below.",
"redirect_url": "https://example.com/book" }
]
}
pointsmaps question ids → option value → points. Multi-choice sums each selected value.- Buckets match on total score (
min/max, at least one required). The matched bucket'stitle/bodyshow on the completion screen;redirect_url(if set) redirects instead. - Each response stores
scoreandoutcome(the bucket key) — filter and sort by them when reading responses.
Money forms: invoices, estimates, quotes, order forms
Anything with a price on it has a fixed shape. Which one you build depends on who already knows the number.
You know it — invoice, quote, order form. State the number → let them confirm it → record the approval → take the payment.
{ "schema_version": 1, "kind": "form",
"title": "Invoice 2026-041 — Marlow Studio",
"settings": { "collect_respondent": "email_required",
"completion": { "message": "Paid — thank you. A receipt is on its way." } },
"sections": [ { "key": "invoice", "title": "Invoice", "questions": [
{ "id": "line_items", "type": "statement",
"title": "Invoice 2026-041 · due September 19, 2026",
"body": ["Brand identity — logo, type, and color: $2,800",
"Packaging templates, three SKUs: $1,150",
"Total due: $3,950"] },
{ "id": "amounts_ok", "type": "yes_no", "required": true,
"prompt": "Do these charges match what we agreed?" },
{ "id": "correction", "type": "long_text", "rows": 3,
"prompt": "What looks off? I'll send a corrected invoice.",
"show_if": { "question": "amounts_ok", "op": "eq", "value": "no" } },
{ "id": "approval", "type": "consent", "required": true,
"prompt": "I approve this invoice for $3,950.",
"show_if": { "question": "amounts_ok", "op": "eq", "value": "yes" } },
{ "id": "pay", "type": "payment", "required": true,
"prompt": "Pay this invoice", "amount_cents": 395000,
"button_label": "Pay $3,950",
"show_if": { "question": "amounts_ok", "op": "eq", "value": "yes" } }
] } ] }
- The total in the
statementandamount_centsmust agree to the cent ($3,950 →395000; max 1,000,000 = $10,000 per question). Never make the respondent add up line items. - Always give "these numbers are wrong" somewhere to go — the correction question is what turns a dispute into a message instead of a chargeback.
collect_respondent: "email_required"so the payer is identifiable, and acompletion.messagethat says what happens next.- Don't paginate it: no
one_question_per_pageon an invoice. - No Business plan or no connected Stripe? Drop the
paymentquestion, keep the approval, and say what upgrading buys. An itemized invoice with a timestampedconsentis still a paper trail worth sending.
They don't know it — an estimate. Ask 4–6 scored scope questions and
return a price with outcomes buckets whose titles are ranges:
{ "settings": { "collect_respondent": "email_required",
"one_question_per_page": true, "show_progress": true },
"outcomes": { "method": "points",
"points": { "job_type": { "interior": 2, "exterior": 4, "both": 6 },
"home_size": { "small": 1, "medium": 3, "large": 5 },
"condition": { "good": 0, "worn": 2, "rough": 4 },
"extras": { "trim": 1, "ceilings": 1, "cabinets": 3, "deck": 2 } },
"buckets": [
{ "key": "small_job", "max": 7, "title": "Estimated range: $1,200 – $2,400",
"body": "Two to three days with a crew of two… Book a walkthrough at https://cal.com/your-crew/walkthrough for a fixed quote." },
{ "key": "mid_job", "min": 8, "max": 14, "title": "Estimated range: $2,400 – $5,000",
"body": "Four to six days on site… Book a walkthrough and we'll turn this into a fixed quote." },
{ "key": "large_job", "min": 15, "title": "Estimated range: $5,000 – $9,000+",
"body": "A week or more with a crew of three… We'll bring a written quote back within two business days." }
] } }
- Score only what actually moves the price — size, condition, scope, add-ons.
- Ranges, never a single number, and say what the range excludes.
- Add up the lowest and highest reachable totals by hand and confirm both land in a bucket (above: 3 to 22, covered with no gap).
- Every bucket ends in the same next step — book the walkthrough, get the fixed quote.
Complete, ready-to-POST versions of the invoice, quote, and order form are at /templates.md; both shapes are live at https://quizgen.dev/examples.
Reading responses
GET /api/v1/quizzes/:id/responses returns:
{ "responses": [ {
"id": "rsp_...", "quiz_id": "qz_...", "definition_version": 1,
"answers": { "pick": "coffee", "budget": "1k_5k", "budget__followup": "maybe more" },
"outcome": "qualified", "score": 5,
"respondent_email": null, "complete": true,
"paid": false, "payment_intent": null,
"started_at": "...", "completed_at": "...",
"meta": { "referrer": "..." }
} ],
"total": 1, "count": 1, "offset": 0, "has_more": false }
Page through large result sets with ?limit= (1–500) and ?offset=; when
has_more is true the payload also carries next_offset.
answersis a flatquestion_id → valuemap (multi_choice→ array of values).complete: falserows are partial (autosaved, respondent hasn't finished) — useful for drop-off analysis; filter them out with?complete=truewhen the user asks about finished answers.- When summarizing for the user, lead with counts (started/finished), then the
distribution of key
choiceanswers, then notable free-text patterns.
Conventions worth following
These are the defaults that separate a quiz people finish from one they abandon. Follow them unless the user asks for something else.
- Keep quizzes short: 5–7 questions. Completion drops below 50% past 6, and a
form finishing in under a minute completes 15% more often. Count what one
respondent sees —
show_ifbranching doesn't count against you. - Every question must change a decision someone will actually make. If nobody will act on the answer, cut it.
- Open with an easy one-tap
choice/scaleabout the respondent; put email, budget, and demographics last (they're the top abandonment drivers when asked early). one_question_per_page: truefor personality/lead-magnet/assessment quizzes; grouped sections for intake and application forms people want to power through with data already in hand.- Write the 3–5
outcomesbuckets before the questions, then write questions that separate them. Give each bucket a ~200-wordbodyending in a next step, and make sure every reachable score lands in a bucket. - Set a
cover: outcome-shaped title under 15 words, subtitle under 25, and abutton_labelthat states the promise rather than "Start". Keepshow_progress: truefor anything over 3 questions. - Use
choice/scaleover free text when the user will want to aggregate; at most 1 open question per 3. - One idea per question, no leading questions, positive framing (positively framed forms complete at ~55% vs ~38%).
- Use sections to group related questions; use a
statementto explain anything the respondent needs context for — especially before a sensitive block, where saying why you're asking is a completion lever. - Prefer
email_optionalunless the user needs to contact every respondent, or the outcome is the trade for the email. - After creating, always give the user the live
urland mention answers will appear in their dashboard at https://quizgen.dev/dashboard.
Limits
| Plan | Live quizzes | Answers/quiz/mo | Retention | Extras |
|---|---|---|---|---|
| Sandbox (no key) | 10/hr/IP, 72h lifetime | 10 total | until expiry | create-only; can't update or read responses |
| Free | 3 | 100 | 90 days | webhook on 1 quiz; accent-only theme; footer stays |
| Pro $15/mo | unlimited | 5,000 | forever | full theme + logo, hidden_fields, no footer, webhooks on all quizzes, file_upload (10 MB/file, 1 GB total), 500 email sends/mo |
| Business $49/mo | unlimited | 25,000 | forever | everything in Pro + payment questions, file_upload (25 MB/file, 10 GB total), 5,000 email sends/mo |
Not built yet — do not tell a user these are available on any plan, or that
upgrading unlocks them: custom domains, and SMS sending (channel: "sms"
returns 400 not_yet_available). If a user wants one, point them at
sales@quizgen.dev rather than suggesting a workaround or a plan change.
When a quiz hits its cap, the hosted page shows "no longer accepting
responses" and /api/respond returns 410 quiz_full — existing responses
stay readable. If the user is bumping into limits, suggest the next plan up
at https://quizgen.dev/#pricing. Slugs and hosting are always on quizgen.dev.
Guides for agents & humans
Task-specific write-ups, each plain markdown — read one directly instead of re-deriving the pattern from this file. Index: https://quizgen.dev/blog.md
- https://quizgen.dev/blog/claude-mcp-servers-for-quizzes-and-forms.md — adding the MCP server, its tools, sandbox vs. permanent auth
- https://quizgen.dev/blog/build-a-quiz-with-claude.md — full one-sentence-to-live-link workflow
- https://quizgen.dev/blog/build-a-form-with-chatgpt.md — the Actions path and the manual-curl path
- https://quizgen.dev/blog/ai-form-builder-guide.md — draft-assist AI vs. prompt-to-live-link AI
- https://quizgen.dev/blog/typeform-alternative-for-ai-agents.md — capability comparison, not a sales page
- https://quizgen.dev/blog/how-to-make-a-quiz-with-scoring.md — quiz with scoring end to end: points, buckets, reading scores back
- https://quizgen.dev/blog/best-quiz-makers-with-scoring.md — honest comparison: Google Forms, Typeform, ScoreApp, Jotform, QuizGen
- https://quizgen.dev/blog/lead-qualification-form-that-scores-itself.md —
outcomesworked example - https://quizgen.dev/blog/personality-quiz-with-outcomes.md — scoring engine applied to personas
- https://quizgen.dev/blog/event-rsvp-form-with-response-cap.md —
max_responses+close_at+show_if - https://quizgen.dev/blog/headless-form-builder-explained.md — when headless beats a visual editor
- https://quizgen.dev/blog/quiz-response-webhooks.md — setup, payload shape, signature verification
- https://quizgen.dev/blog/free-ai-quiz-generator.md — what free actually includes: sandbox, free plan, paid gates
- https://quizgen.dev/blog/ai-quiz-maker.md — what makes an AI quiz maker good; sandbox and plans explained
- https://quizgen.dev/blog/jotform-alternative.md — capability comparison; when to pick each
- https://quizgen.dev/blog/google-forms-alternative.md — for when you've outgrown Forms: scoring, webhooks, API
- https://quizgen.dev/blog/surveymonkey-alternative.md — one-off surveys without a research-platform seat
- https://quizgen.dev/blog/customer-feedback-form-template.md — complete JSON template, POST it as-is
- https://quizgen.dev/blog/client-intake-form-template.md — complete JSON template with
show_ifbranching - https://quizgen.dev/blog/invoice-template.md — invoice with line items, recorded approval, card payment
- https://quizgen.dev/blog/order-form-template.md — order form with
price_optionscard payment - https://quizgen.dev/blog/quote-template.md — quote with one-click approval, expiry, optional deposit
- https://quizgen.dev/blog/event-registration-form-template.md — registration with ticket tiers, capacity cap
- https://quizgen.dev/blog/contact-form-template.md — minimal contact form + webhook routing
- https://quizgen.dev/blog/waitlist-signup-form.md — the canonical
kind: "email_capture"example - https://quizgen.dev/blog/job-application-form-template.md — application + resume
file_upload+ points-based screening buckets - https://quizgen.dev/blog/liability-waiver-form-template.md — waiver text, minor/guardian branching, recorded agreement
- https://quizgen.dev/blog/customer-satisfaction-survey-template.md — CSAT + NPS 0–10 scale, detractor follow-up
- https://quizgen.dev/blog/exit-interview-questions.md — exit interview question bank + the form
More for agents & humans
- This file: https://quizgen.dev/llms.txt (canonical, also at /docs.md; HTML mirror at /llms)
- Landing page as markdown: https://quizgen.dev/index.md (JSON: /index.json)
- Pricing as markdown: https://quizgen.dev/pricing.md (JSON: /pricing.json)
- Guides index as markdown: https://quizgen.dev/blog.md (JSON: /blog.json)
- Templates index (copy-paste prompts + JSON templates): https://quizgen.dev/templates.md (JSON: /templates.json)
- This file as JSON: https://quizgen.dev/llms.json
- MCP connect page: https://quizgen.dev/mcp
- Installable agent skill & scaffolder: https://quizgen.dev/agents