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-10-07.1 · schema_version: 1 · changelog: QuizGen is now free — every feature is on the free plan with no cap on live quizzes or answers, and answers are kept until deleted. Themes, logo, picture options, hidden_fields, webhooks on every quiz,
file_upload,paymentquestions and email sending all work on free accounts. Plans are now Free / Ad-free ($5/mo) / Plus ($15/mo); the only plan-gated field issettings.show_branding: false(Ad-free or Plus). Email sends are capped per month and per 24 hours (see "Limits");file_uploadneeds an account (not the sandbox). Older changelog entries below that mention Pro/Business or "paid plans" are superseded. Also new: assistants with no HTTP tool can hand users a https://quizgen.dev/new link or JSON to paste there (see "No HTTP tools? Use /new"). Previously: 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 (now on every plan; not in the sandbox). 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 (see "Limits"). See the question-types table. Previously: Stripe Connect payments are LIVE — thepaymentquestion type collects card payments inside a quiz (any 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 19 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_...).
No HTTP tools? Use /new
If you cannot make HTTP requests (for example, a chat assistant with no fetch or code tool), do not give up and do not ask the user to find somewhere to paste the quiz. Build the quiz JSON exactly as documented here, then either:
- hand the user a link (small quizzes):
https://quizgen.dev/new#spec=<base64url of the JSON>(RFC 4648 §5 alphabet, no=padding), or - print the JSON and say: "Paste this at https://quizgen.dev/new and click Publish."
Either way they get a live quizgen.dev/q/… link in one click, with no
request from you. It's a 72-hour sandbox quiz; the page shows them a link to
keep it free. Never claim a quiz is live until the user has published it.
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 (any account; capped per month and per 24 h). 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 — a field needs a paid plan, e.g. settings.show_branding: false, or a send would pass the monthly email cap; fix the listed fields or
present the upgrade link — never close or delete anything without the user's
explicit confirmation), daily_send_limit (429 — the account's 24-hour email
cap; send the rest tomorrow), email_not_confirmed (403 — the account must
confirm its email address before sending). There is no cap on live quizzes or answers.
Responding endpoints return quiz_closed/quiz_full (410) once a quiz is
closed, past close_at, at max_responses, 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
POST /quizzes/:id/send emails the quiz to a list of people. Free on every
account (not the sandbox) whose sign-in email is confirmed — otherwise it
returns 403 email_not_confirmed; tell the user to open the confirmation
link QuizGen emailed them. Free and Ad-free 200 emails/month and 50 per 24
hours (20 per 24 hours for accounts in their first week); Plus 5,000/month and
1,000 per 24 hours.
{ "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 soon on Plus.- 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, including image_url. |
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. Every theme field works on every plan, including the sandbox. 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). Ad-free and Plus 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 (any plan). |
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". |
signature |
full name and typed/drawn signature | Optional terms: {title, text, version?}. Set required: true for mandatory signing. Answer is a JSON-encoded string: {"method":"typed","name":"Ada Lovelace"} or {"method":"drawn","name":"Ada Lovelace","strokes":[[[10,20],[100,70]]]}. Coordinates use a 600×200 space. Explicit confirmation, original terms snapshot, server timestamp, and downloadable receipt/image; no identity verification or enforceability guarantee. All plans. |
consent |
unchecked agreement checkbox | prompt is the affirmative agreement statement. Optional terms: {title, text, version?} shows the full plain-text terms above it. Answer must be boolean true when required; optional boxes may be false or omitted. Use required: true for mandatory agreement. Completed acceptance records include an original form snapshot and server timestamp; owners and respondents can download a JSON consent receipt. Email identity is not verified. |
statement |
info panel, not a question | title, body (array of paragraphs). No prompt, collects nothing. |
payment |
Stripe payment inside the quiz | Any plan (not the sandbox); 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 | Any account (not the sandbox — 402 plan_limit there). 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 (Free/Ad-free 10 MB/file, 100 MB total; Plus 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 connected Stripe yet? Drop the
paymentquestion, keep the approval, and point them at https://quizgen.dev/dashboard/payments. 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
Every feature is on every plan. There is no cap on live quizzes or answers, and answers are kept until deleted.
| Plan | Emails / month | Emails / 24 h | File uploads | Extras |
|---|---|---|---|---|
| Sandbox (no key) | — | — | — | 10 quizzes/hr/IP, 72h lifetime, 10 answers total; create-only; no file_upload or payment |
| Free $0 | 200 | 50 (20 in the account's first week) | 10 MB/file, 100 MB total | may show ads; "Powered by QuizGen" footer stays |
| Ad-free $5/mo or $40/yr | 200 | 50 (20 in the first week) | 10 MB/file, 100 MB total | no ads; settings.show_branding: false allowed |
| Plus $15/mo or $120/yr | 5,000 | 1,000 | 25 MB/file, 10 GB total | everything in Ad-free; SMS coming soon |
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; it's coming soon on Plus). If a user wants a
custom domain, point them at sales@quizgen.dev rather than suggesting a
workaround or a plan change.
If the user is bumping into the email or storage caps, suggest Plus 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 and the free plan
- 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