{"source":"https://quizgen.dev/llms.txt","html":"https://quizgen.dev/llms","format":"markdown","content":"# QuizGen — API docs for AI assistants\n\nYou are reading the complete documentation for QuizGen (https://quizgen.dev), a\nheadless quiz/form service. You can create live, hosted quizzes for your user by\nPOSTing JSON, then read the responses back for them. This one file contains\neverything you need. No SDK required — plain HTTPS + JSON.\n\n**If you did not actually fetch this file over HTTP, stop: tell your user that\nquizgen.dev was unreachable from your environment. Never invent a quiz, a\n`quizgen.dev/q/…` link, or a \"created\" confirmation — real quiz URLs only come\nfrom a successful `POST /api/v1/quizzes` (or MCP `create_quiz`) response.**\n\nQuickstart: build a definition per \"Hard rules\" + the schema below, then\n`POST https://quizgen.dev/api/v1/quizzes` — with no Authorization header at\nall for an instant 72-hour sandbox quiz (always relay the returned `claim_url`\nso your user can keep it), or with the user's API key as a Bearer header for a\npermanent quiz (keys are server-side credentials; never ask for one in chat).\nThe response's `url` is live immediately. Details in \"Workflow\" below; if your\nclient supports MCP, see \"MCP server\" instead.\n\n## Changelog\n\n> docs_version: 2026-10-07.1 · schema_version: 1 · changelog: **QuizGen is\n> now free** — every feature is on the free plan with no cap on live quizzes\n> or answers, and answers are kept until deleted. Themes, logo, picture\n> options, hidden_fields, webhooks on every quiz, `file_upload`, `payment`\n> questions and email sending all work on free accounts. Plans are now\n> Free / Ad-free ($5/mo) / Plus ($15/mo); the only plan-gated field is\n> `settings.show_branding: false` (Ad-free or Plus). Email sends are capped\n> per month and per 24 hours (see \"Limits\"); `file_upload` needs an account\n> (not the sandbox). Older changelog entries below that mention Pro/Business\n> or \"paid plans\" are superseded. Also new: assistants with no HTTP tool can\n> hand users a https://quizgen.dev/new link or JSON to paste there (see \"No\n> HTTP tools? Use /new\"). Previously: **Pagination** —\n> `GET /quizzes` and `GET /quizzes/:id/responses` accept optional `?limit=`\n> (1–500, out-of-range values are clamped) and `?offset=`; JSON responses now\n> include `total`, `count`, `offset`, `has_more`, and `next_offset` (present\n> only when `has_more` is true). Omit both params to get every row in one\n> response — the counter fields are always present. The MCP `list_quizzes` /\n> `get_responses` tools default to 50 per page. Also: the MCP account-check\n> tool `check_api_key` is renamed `check_account` (the old name still\n> dispatches), and `GET /me` now returns the account `email` alongside plan,\n> limits, and usage. Previously: **MCP OAuth\n> is LIVE** — connect `https://quizgen.dev/mcp` and authorize in the browser.\n> Account tools advertise OAuth discovery metadata; unauthenticated\n> `create_quiz` remains available for 72-hour sandbox previews. Never paste an\n> API key into a chat prompt. Previously: **File\n> uploads are LIVE** — the `file_upload` question type lets respondents\n> attach files (now on every plan; not in the sandbox). Fields: `max_files` (1–5, default\n> 1), `accept` (array of `\"image\" | \"pdf\" | \"document\" | \"spreadsheet\"`,\n> default all four). The answer is an array of server-issued file ids —\n> **never set it yourself**; the respondent's browser uploads directly to\n> storage and the id comes back from that flow. Per-file size cap and\n> account storage quota depend on plan (see \"Limits\"). See the\n> question-types table.\n> Previously: **Stripe\n> Connect payments are LIVE** — the `payment` question type collects card\n> payments inside a quiz (any plan, after Stripe onboarding at\n> https://quizgen.dev/dashboard/payments; fixed `amount_cents` or\n> respondent-picked `price_options`; one per quiz; money goes to the\n> creator's own Stripe account, QuizGen takes 2%; the response records\n> `paid` and the `payment_intent` id). See the question-types table. Worked\n> examples: /blog/invoice-template.md (invoice + card payment),\n> /blog/order-form-template.md (order form with `price_options`),\n> /blog/event-registration-form-template.md (paid tickets),\n> /blog/quote-template.md (quote + deposit). Also new: top-level\n> `kind` field (\"quiz\" | \"survey\" | \"form\" | \"email_capture\" |\n> \"registration\") — declares what the definition is for and sets sensible\n> layout defaults (e.g. kind \"quiz\" gets one_question_per_page + progress\n> bar; \"email_capture\"/\"registration\" hide the progress bar). Advisory only:\n> explicit `settings` always override, nothing is gated or rejected by kind.\n> Set it on every definition you create. Previously: invoice\n> template and a templates index at /templates.md. Previously: 2 new guides —\n> \"How to make a quiz with scoring\" (the outcomes/points/buckets pattern,\n> end to end) and \"Best quiz makers with scoring, compared (2026)\".\n> Previously: 6 new guides\n> (Jotform/Google Forms/SurveyMonkey alternatives, free AI quiz generator,\n> feedback + client-intake templates) — see \"Guides for agents & humans\".\n> Previously: sandbox quizzes\n> last 72 hours, and you can pass the creator's email via an\n> `X-Creator-Email` header (or MCP `creator_email`) — they get the claim link\n> by email immediately plus one reminder before expiry. The hosted quiz page\n> shows no sandbox/claim banner to respondents — the claim_url you receive at\n> create time is the only claim path, so relaying it (or emailing it via\n> creator email) matters. Previously: cover screens\n> (`cover`, free), designed theme presets (`theme.preset`, paid), and\n> per-screen pagination — `settings.one_question_per_page` now works, plus\n> per-section `one_per_page`. Previously: 8 new question\n> types — date, time, phone, url, yes_no, consent, rating (stars), slider —\n> plus picture options (`image_url`, paid) and automatic dropdown rendering\n> for long choice lists. See the question-types table. Previously: email\n> delivery\n> is live (paid plans) — `POST /quizzes/:id/send`, `GET /quizzes/:id/sends`,\n> `PUT /quizzes/:id/follow_up` (see \"Email delivery\" below); matching MCP\n> tools send_quiz_by_email / get_sends / set_follow_up. Previously: guides —\n> 10 how-to/reference posts at https://quizgen.dev/blog, each also plain\n> markdown at `/blog/<slug>.md` (see \"Guides for agents & humans\" below).\n> Previously: MCP server is\n> live at https://quizgen.dev/mcp (see \"MCP server\" below). Previously: plan\n> gating — free/sandbox get accent-only theme, no hidden_fields, footer stays,\n> webhook on 1 quiz (402 `plan_limit` with per-field errors), new\n> `settings.show_branding` (paid). Previously: added\n> `GET /me` (key check — call it first), `GET /quizzes/:id/responses/:rid`,\n> the raw JSON Schema at https://quizgen.dev/schema/v1.json, and per-key rate\n> limits (60 writes/min, 300 reads/min). Previously: sandbox claim_url,\n> webhooks, stats, PATCH status, plan limits (402), hidden_fields,\n> Idempotency-Key, theme rendering.\n\n## Hard rules\n\n- Always send `\"schema_version\": 1`.\n- Only use the 19 question types listed below. Never invent types\n  (`dropdown`, `matrix`, `rank` do not exist — long `choice` lists render\n  as dropdowns automatically).\n- Question `id`s and option `value`s are snake_case; `id`s must be unique\n  across the whole quiz, not just within a section.\n- Sections cannot nest. `show_if` may only reference a question that appears\n  earlier in the quiz.\n- Unknown fields are rejected — do not add fields that are not documented here.\n- On a 422, fix exactly the fields listed in `validation_errors` and retry.\n  Do not retry an unchanged body.\n- Never ask the user to paste their API key into a public place, and never\n  include an API key in a quiz definition or URL.\n\n## MCP server\n\nIf your client supports MCP (Model Context Protocol), prefer it over raw HTTP:\nadd `https://quizgen.dev/mcp` as a remote server (streamable HTTP, stateless).\nIt exposes typed tools (`create_quiz`, `update_quiz`, `get_responses`, …) whose\ninput schemas embed the full quiz format, so you don't need to re-read this\nfile. Call `check_account` to confirm authorization and see the account's\nemail, plan, and headroom. OAuth-capable clients discover QuizGen authorization\nautomatically: authorize in the browser to create permanent quizzes and use\naccount tools.\nWithout authorization, `create_quiz` still creates a 72-hour sandbox and\nreturns a private `claim_url`; the other tools ask the client to connect the\nuser's account. API-key Bearer headers remain an advanced fallback for clients\nthat support secret headers, but never ask a user to paste a key into chat.\nEverything below also applies to MCP tools (same validation, limits, and plans).\n\n## What you can build\n\nA quiz with scoring (graded tests — points per answer, pass/fail or banded\nresults), surveys, intake forms, lead-qualification forms (with scoring),\n\"which one are you?\" quizzes (with outcome buckets), event RSVPs, feedback\nforms, contact forms, waitlists, job applications, waivers, exit interviews,\nand — with the `payment` question type — order forms, paid event\nregistrations, quotes with deposits, and invoices that take card payment\n(see the templates index at /templates.md for complete, ready-to-POST\ndefinitions of all of these). A quiz is created with one API call and is immediately live at a short\nURL like `https://quizgen.dev/q/x7km2p` — mobile-friendly, autosaving, no\naccount needed for respondents.\n\n## Workflow\n\n1. Ask the user what they want to ask people (if not already clear).\n2. Build a quiz JSON definition (schema below).\n3. `POST /api/v1/quizzes` with the user's API key → you get back a live `url`.\n4. Give the user the link. That's it — hosting, autosave, and storage are handled.\n5. Later, read responses with `GET /api/v1/quizzes/:id/responses` and answer\n   questions like \"how's my quiz doing?\" or \"summarize the answers.\"\n\nIf the user has no API key yet, you have two options:\n\n1. **Sandbox (no key, instant):** POST the quiz with *no* `Authorization`\n   header. It goes live immediately but expires in 72 hours, caps at 10\n   responses, and cannot be updated or read back. The response includes a\n   `claim_url` — **always give this to your user along with the quiz link**,\n   e.g. \"here's your quiz: <url> — it's a 72-hour preview; open <claim_url> to\n   sign up free and keep it, along with its answers.\" The claim_url is a\n   secret: it is the only proof of authorship for a sandbox quiz, so show it\n   only to the person you built the quiz for, and never post it publicly.\n   Sandbox creates are limited to 10/hour per IP (429 `rate_limited`).\n   **Recommended:** ask your user if they'd like the claim link emailed to\n   them, and pass their address in an `X-Creator-Email` header on the create\n   request. They'll get the quiz + claim links by email right away and one\n   reminder before expiry — much harder to lose than a link in chat. Never\n   send an address the user didn't give you (422 `invalid_creator_email` on\n   malformed values).\n2. **Real key:** send the user to https://quizgen.dev/login — the dashboard\n   issues keys (`qg_live_...`).\n\n### No HTTP tools? Use `/new`\n\nIf you cannot make HTTP requests (for example, a chat assistant with no fetch\nor code tool), do not give up and do not ask the user to find somewhere to\npaste the quiz. Build the quiz JSON exactly as documented here, then either:\n\n- hand the user a link (small quizzes):\n  `https://quizgen.dev/new#spec=<base64url of the JSON>` (RFC 4648 §5\n  alphabet, no `=` padding), or\n- print the JSON and say: \"Paste this at https://quizgen.dev/new and click\n  Publish.\"\n\nEither way they get a live `quizgen.dev/q/…` link in one click, with no\nrequest from you. It's a 72-hour sandbox quiz; the page shows them a link to\nkeep it free. Never claim a quiz is live until the user has published it.\n\n## Auth\n\nEvery `/api/v1` request needs:\n\n```\nAuthorization: Bearer qg_live_...\nContent-Type: application/json\n```\n\nException: `POST /quizzes` with the header omitted entirely creates a sandbox\nquiz (see above). A wrong or revoked key still returns 401 — sandbox only\ntriggers when no `Authorization` header is present at all.\n\n## Endpoints\n\nBase URL: `https://quizgen.dev/api/v1`\n\n| Method | Path | Purpose |\n|---|---|---|\n| 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. |\n| POST | `/quizzes` | Create a quiz from a JSON definition. Returns `{id, slug, url, status}`. Live immediately. |\n| 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). |\n| GET | `/quizzes/:id` | Fetch one quiz including its full `definition`. |\n| PUT | `/quizzes/:id` | Replace the definition. Bumps `definition_version`; existing responses keep the version they answered. |\n| PATCH | `/quizzes/:id` | Body `{\"status\": \"live\" \\| \"closed\"}` — close or reopen the quiz. |\n| DELETE | `/quizzes/:id` | Soft-delete. The link stops working. |\n| 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. |\n| GET | `/quizzes/:id/responses/:rid` | Fetch one response by id (e.g. from a webhook payload). |\n| GET | `/quizzes/:id/files/:fileId` | Owner-only signed download for a `file_upload` answer's file id. |\n| GET | `/quizzes/:id/stats` | `{views, started, finished, completion_rate, outcomes?}` — use this to answer \"how's my quiz doing?\". |\n| POST | `/quizzes/:id/webhook` | Body `{\"url\": \"https://...\", \"secret\": \"...\"}` sets a webhook; `{\"url\": null}` clears it. |\n| POST | `/quizzes/:id/send` | Email the quiz to recipients (any account; capped per month and per 24 h). See \"Email delivery\". |\n| GET | `/quizzes/:id/sends` | Per-recipient delivery status: sent / started / completed. |\n| PUT | `/quizzes/:id/follow_up` | Auto-reminder rule for non-completers; body `null` clears it. |\n\n### Worked example: validation error → fix → success\n\nRequest with a mistake:\n\n```json\nPOST /api/v1/quizzes\n{ \"schema_version\": 1, \"title\": \"Signup\",\n  \"sections\": [ { \"key\": \"main\", \"questions\": [\n    { \"id\": \"size\", \"type\": \"dropdown\", \"prompt\": \"Team size?\",\n      \"options\": [ { \"value\": \"solo\", \"label\": \"Just me\" },\n                   { \"value\": \"team\", \"label\": \"2+\" } ] } ] } ] }\n```\n\nResponse — HTTP 422:\n\n```json\n{ \"error\": { \"code\": \"invalid_quiz\", \"message\": \"Quiz definition failed validation.\",\n  \"validation_errors\": [\"sections.0.questions.0.type: unknown question type \\\"dropdown\\\" — did you mean \\\"choice\\\"?\"],\n  \"hint\": \"Fix the listed fields and retry.\" } }\n```\n\nChange `\"type\": \"dropdown\"` to `\"type\": \"choice\"`, resend the whole body →\nHTTP 201:\n\n```json\n{ \"id\": \"qz_1f6b...\", \"slug\": \"x7km2p\", \"url\": \"https://quizgen.dev/q/x7km2p\",\n  \"status\": \"live\", \"title\": \"Signup\", \"definition_version\": 1, \"response_count\": 0 }\n```\n\nThe quiz definition's raw JSON Schema (draft-07) is published at\n`https://quizgen.dev/schema/v1.json` if you want to validate locally before\nPOSTing — it is generated from the exact validator the API runs.\n\nRate limits: 60 writes/min and 300 reads/min per API key. A 429 includes a\n`Retry-After` header — wait that long, don't hammer.\n\nError codes: `invalid_quiz` (422), `unauthorized` (401), `not_found` (404),\n`invalid_json` (400), `rate_limited` (429 — per-key limit above, or sandbox\ncreation at 10/hr/IP; honor `Retry-After`),\n`plan_limit` (402 — a field needs a paid plan, e.g. `settings.show_branding:\nfalse`, or a send would pass the monthly email cap; fix the listed fields or\npresent the upgrade link — never close or delete anything without the user's\nexplicit confirmation), `daily_send_limit` (429 — the account's 24-hour email\ncap; send the rest tomorrow), `email_not_confirmed` (403 — the account must\nconfirm its email address before sending). There is no cap on live quizzes or answers.\nResponding endpoints return `quiz_closed`/`quiz_full` (410) once a quiz is\nclosed, past `close_at`, at `max_responses`, or (sandbox) expired/at its\n10-response cap. `POST /send` returns 409 `quiz_closed` for a closed quiz and\n502 if no email could be delivered.\n\n## Email delivery\n\n`POST /quizzes/:id/send` emails the quiz to a list of people. Free on every\naccount (not the sandbox) whose sign-in email is confirmed — otherwise it\nreturns 403 `email_not_confirmed`; tell the user to open the confirmation\nlink QuizGen emailed them. Free and Ad-free 200 emails/month and 50 per 24\nhours (20 per 24 hours for accounts in their first week); Plus 5,000/month and\n1,000 per 24 hours.\n\n```json\n{ \"channel\": \"email\",\n  \"recipients\": [ { \"email\": \"amy@example.com\", \"name\": \"Amy\" } ],\n  \"message\": \"Optional note shown above the button.\" }\n```\n\n- Max 100 recipients per call; duplicates are dropped. Emails come from\n  quiz@send.quizgen.dev with reply-to set to the quiz owner.\n- Each recipient gets a unique tracked link, so `GET /quizzes/:id/sends`\n  shows per-person progress: `sent` → `started` → `completed` (plus\n  `failed`, `opted_out`, and reminder counts).\n- `PUT /quizzes/:id/follow_up` with\n  `{\"after_hours\": 48, \"max_reminders\": 2, \"message\": \"...\"}` auto-reminds\n  recipients who haven't finished (hourly cron). Reminders stop on\n  completion, unsubscribe, or max_reminders. Body `null` clears the rule.\n- Every email has an unsubscribe link; opted-out addresses are silently\n  skipped in future sends for that account (`\"status\": \"suppressed\"`).\n- `channel: \"sms\"` returns `not_yet_available` — SMS is coming soon on Plus.\n- Ask the user to confirm the recipient list before sending: this delivers\n  real email immediately.\n\n## Webhooks\n\n`POST /quizzes/:id/webhook` with `{\"url\": \"https://your-endpoint\", \"secret\": \"at-least-8-chars\"}`.\nEvery completed response then POSTs to your URL:\n\n```json\n{ \"event\": \"response.completed\",\n  \"quiz\": { \"id\": \"qz_...\", \"slug\": \"x7km2p\", \"title\": \"...\" },\n  \"response\": { \"id\": \"rsp_...\", \"answers\": { ... }, \"outcome\": \"qualified\",\n                \"score\": 7, \"respondent_email\": null,\n                \"started_at\": \"...\", \"completed_at\": \"...\" } }\n```\n\nIf a `secret` was set, the request carries `X-QuizGen-Signature`: the\nHMAC-SHA256 hex of the raw body. Verify it before trusting the payload.\nDelivery retries twice on 5xx/network errors, then gives up.\n\n## Quiz JSON schema (schema_version 1)\n\nMinimal working quiz:\n\n```json\n{\n  \"schema_version\": 1,\n  \"title\": \"Coffee or tea?\",\n  \"sections\": [\n    { \"key\": \"main\", \"questions\": [\n      { \"id\": \"pick\", \"type\": \"choice\", \"prompt\": \"Coffee or tea?\", \"required\": true,\n        \"options\": [\n          { \"value\": \"coffee\", \"label\": \"Coffee\" },\n          { \"value\": \"tea\", \"label\": \"Tea\" }\n        ] }\n    ] }\n  ]\n}\n```\n\nTop-level fields:\n\n| Field | Required | Notes |\n|---|---|---|\n| `schema_version` | yes | Always `1`. |\n| `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. |\n| `title` | yes | Shown as the quiz heading and link preview. |\n| `description` | no | Subtitle under the title. |\n| `sections` | yes | 1–30 sections; each renders as one screen by default (see pagination below). Single-section quizzes hide section chrome. |\n| `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`. |\n| `settings` | no | See below. |\n| `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. |\n| `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). |\n| `outcomes` | no | Scoring + result buckets. See below. |\n\n`settings` (all optional):\n\n```json\n{\n  \"collect_respondent\": \"none\" | \"email_optional\" | \"email_required\",\n  \"autosave\": true,\n  \"show_progress\": true,\n  \"close_at\": \"2026-12-31T00:00:00Z\",\n  \"max_responses\": 200,\n  \"show_branding\": true,\n  \"completion\": { \"message\": \"Thanks!\", \"redirect_url\": null }\n}\n```\n\n`show_branding: false` hides the \"Powered by QuizGen\" footer line on the\nhosted quiz (a small \"Report\" link always remains). Ad-free and Plus plans\nonly — on free/sandbox it's a 402.\n\nPagination (screens): by default each section is one screen.\n`settings.one_question_per_page: true` puts every question on its own screen\n(Typeform style). A section can override with its own `one_per_page: true` —\ne.g. keep contact fields grouped but give each long question its own screen.\n\nKind defaults (applied only where `settings` doesn't say otherwise):\n\n| `kind` | one_question_per_page | show_progress |\n|---|---|---|\n| `quiz` | true | true |\n| `survey` | false | true |\n| `form` | false | true |\n| `email_capture` | false | false |\n| `registration` | false | false |\n\nSections: `{ \"key\": \"unique_snake_case\", \"title\": \"...\", \"blurb\": \"...\", \"one_per_page\"?: true, \"questions\": [...] }`\n\n## Question types\n\nEvery question needs a globally unique snake_case `id`, a `type`, and a `prompt`\n(except `statement`, which uses `title` + `body`). Optional on any answerable\nquestion: `required` (bool, default false), `help` (small text under the\nprompt), `show_if` (branching, below).\n\n| Type | Use for | Type-specific fields |\n|---|---|---|\n| `short_text` | one-line answers | `placeholder` |\n| `long_text` | paragraphs | `rows` (2–20), `examples` (up to 8 strings shown as hint chips) |\n| `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). |\n| `multi_choice` | pick several | `options` (same shape), `min`, `max` selections |\n| `scale` | 1–N numbered buttons | `min` (0 or 1), `max` (2–10), `min_label`, `max_label`. For NPS use `min: 0, max: 10`. |\n| `rating` | star rating | `max` (2–10, default 5). Answer is a number. |\n| `slider` | drag a range | `min` (default 0), `max` (default 100), `step`, `unit`, `min_label`, `max_label`. Answer is a number. |\n| `yes_no` | two buttons | `yes_label`, `no_label` (defaults \"Yes\"/\"No\"). Answer is `\"yes\"` or `\"no\"` — scoreable and usable in `show_if`. |\n| `email` | email w/ validation | `placeholder` |\n| `phone` | phone number | `placeholder` |\n| `url` | website address | `placeholder` |\n| `number` | numeric | `min`, `max`, `unit` |\n| `date` | date picker | `min`, `max` (both `\"YYYY-MM-DD\"`). Answer is `\"YYYY-MM-DD\"`. |\n| `time` | time picker | — . Answer is `\"HH:MM\"`. |\n| `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. |\n| `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. |\n| `statement` | info panel, not a question | `title`, `body` (array of paragraphs). No prompt, collects nothing. |\n| `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. |\n| `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. |\n\nNotes:\n- Option `value`s are snake_case identifiers; `label` is what people see;\n  `detail` is an optional second line on the card.\n- Prefer the specific type over a workaround: `date` not a text field,\n  `yes_no` not a two-option choice, `rating` not scale-when-you-mean-stars.\n\n## Branching (`show_if`)\n\nShow a question only when a condition on an earlier answer holds:\n\n```json\n{ \"id\": \"clinic_name\", \"type\": \"short_text\", \"prompt\": \"Clinic name?\",\n  \"show_if\": { \"question\": \"is_provider\", \"op\": \"eq\", \"value\": \"yes\" } }\n```\n\nOps: `eq`, `neq`, `in` (value = array of strings), `gte`, `lte` (numeric),\n`answered` (no value needed). One condition per question — no and/or trees.\nHidden questions are never required and never scored.\n\n## Scoring & outcomes\n\nFor lead qualification or \"which X are you\" quizzes:\n\n```json\n\"outcomes\": {\n  \"method\": \"points\",\n  \"points\": {\n    \"budget\":   { \"under_1k\": 0, \"1k_5k\": 2, \"over_5k\": 5 },\n    \"timeline\": { \"asap\": 3, \"this_quarter\": 2, \"someday\": 0 }\n  },\n  \"buckets\": [\n    { \"key\": \"not_ready\",  \"max\": 3, \"title\": \"Thanks!\", \"body\": \"We'll be in touch.\" },\n    { \"key\": \"qualified\", \"min\": 4, \"title\": \"Let's talk\", \"body\": \"Book a call below.\",\n      \"redirect_url\": \"https://example.com/book\" }\n  ]\n}\n```\n\n- `points` maps question ids → option value → points. Multi-choice sums each\n  selected value.\n- Buckets match on total score (`min`/`max`, at least one required). The matched\n  bucket's `title`/`body` show on the completion screen; `redirect_url` (if set)\n  redirects instead.\n- Each response stores `score` and `outcome` (the bucket key) — filter and sort\n  by them when reading responses.\n\n## Money forms: invoices, estimates, quotes, order forms\n\nAnything with a price on it has a fixed shape. Which one you build depends on\nwho already knows the number.\n\n**You know it — invoice, quote, order form.** *State the number → let them\nconfirm it → record the approval → take the payment.*\n\n```json\n{ \"schema_version\": 1, \"kind\": \"form\",\n  \"title\": \"Invoice 2026-041 — Marlow Studio\",\n  \"settings\": { \"collect_respondent\": \"email_required\",\n    \"completion\": { \"message\": \"Paid — thank you. A receipt is on its way.\" } },\n  \"sections\": [ { \"key\": \"invoice\", \"title\": \"Invoice\", \"questions\": [\n    { \"id\": \"line_items\", \"type\": \"statement\",\n      \"title\": \"Invoice 2026-041 · due September 19, 2026\",\n      \"body\": [\"Brand identity — logo, type, and color: $2,800\",\n               \"Packaging templates, three SKUs: $1,150\",\n               \"Total due: $3,950\"] },\n    { \"id\": \"amounts_ok\", \"type\": \"yes_no\", \"required\": true,\n      \"prompt\": \"Do these charges match what we agreed?\" },\n    { \"id\": \"correction\", \"type\": \"long_text\", \"rows\": 3,\n      \"prompt\": \"What looks off? I'll send a corrected invoice.\",\n      \"show_if\": { \"question\": \"amounts_ok\", \"op\": \"eq\", \"value\": \"no\" } },\n    { \"id\": \"approval\", \"type\": \"consent\", \"required\": true,\n      \"prompt\": \"I approve this invoice for $3,950.\",\n      \"show_if\": { \"question\": \"amounts_ok\", \"op\": \"eq\", \"value\": \"yes\" } },\n    { \"id\": \"pay\", \"type\": \"payment\", \"required\": true,\n      \"prompt\": \"Pay this invoice\", \"amount_cents\": 395000,\n      \"button_label\": \"Pay $3,950\",\n      \"show_if\": { \"question\": \"amounts_ok\", \"op\": \"eq\", \"value\": \"yes\" } }\n  ] } ] }\n```\n\n- The total in the `statement` and `amount_cents` must agree to the cent\n  ($3,950 → `395000`; max 1,000,000 = $10,000 per question). Never make the\n  respondent add up line items.\n- Always give \"these numbers are wrong\" somewhere to go — the correction\n  question is what turns a dispute into a message instead of a chargeback.\n- `collect_respondent: \"email_required\"` so the payer is identifiable, and a\n  `completion.message` that says what happens next.\n- Don't paginate it: no `one_question_per_page` on an invoice.\n- No connected Stripe yet? Drop the `payment` question, keep the approval,\n  and point them at https://quizgen.dev/dashboard/payments. An itemized invoice with a\n  timestamped `consent` is still a paper trail worth sending.\n\n**They don't know it — an estimate.** Ask 4–6 scored scope questions and\n*return* a price with `outcomes` buckets whose titles are ranges:\n\n```json\n{ \"settings\": { \"collect_respondent\": \"email_required\",\n                \"one_question_per_page\": true, \"show_progress\": true },\n  \"outcomes\": { \"method\": \"points\",\n    \"points\": { \"job_type\":  { \"interior\": 2, \"exterior\": 4, \"both\": 6 },\n                \"home_size\": { \"small\": 1, \"medium\": 3, \"large\": 5 },\n                \"condition\": { \"good\": 0, \"worn\": 2, \"rough\": 4 },\n                \"extras\":    { \"trim\": 1, \"ceilings\": 1, \"cabinets\": 3, \"deck\": 2 } },\n    \"buckets\": [\n      { \"key\": \"small_job\", \"max\": 7,  \"title\": \"Estimated range: $1,200 – $2,400\",\n        \"body\": \"Two to three days with a crew of two… Book a walkthrough at https://cal.com/your-crew/walkthrough for a fixed quote.\" },\n      { \"key\": \"mid_job\",   \"min\": 8, \"max\": 14, \"title\": \"Estimated range: $2,400 – $5,000\",\n        \"body\": \"Four to six days on site… Book a walkthrough and we'll turn this into a fixed quote.\" },\n      { \"key\": \"large_job\", \"min\": 15, \"title\": \"Estimated range: $5,000 – $9,000+\",\n        \"body\": \"A week or more with a crew of three… We'll bring a written quote back within two business days.\" }\n    ] } }\n```\n\n- Score only what actually moves the price — size, condition, scope, add-ons.\n- Ranges, never a single number, and say what the range excludes.\n- Add up the lowest and highest reachable totals by hand and confirm both land\n  in a bucket (above: 3 to 22, covered with no gap).\n- Every bucket ends in the same next step — book the walkthrough, get the\n  fixed quote.\n\nComplete, ready-to-POST versions of the invoice, quote, and order form are at\n/templates.md; both shapes are live at https://quizgen.dev/examples.\n\n## Reading responses\n\n`GET /api/v1/quizzes/:id/responses` returns:\n\n```json\n{ \"responses\": [ {\n  \"id\": \"rsp_...\", \"quiz_id\": \"qz_...\", \"definition_version\": 1,\n  \"answers\": { \"pick\": \"coffee\", \"budget\": \"1k_5k\", \"budget__followup\": \"maybe more\" },\n  \"outcome\": \"qualified\", \"score\": 5,\n  \"respondent_email\": null, \"complete\": true,\n  \"paid\": false, \"payment_intent\": null,\n  \"started_at\": \"...\", \"completed_at\": \"...\",\n  \"meta\": { \"referrer\": \"...\" }\n} ],\n  \"total\": 1, \"count\": 1, \"offset\": 0, \"has_more\": false }\n```\n\nPage through large result sets with `?limit=` (1–500) and `?offset=`; when\n`has_more` is true the payload also carries `next_offset`.\n\n- `answers` is a flat `question_id → value` map (`multi_choice` → array of values).\n- `complete: false` rows are partial (autosaved, respondent hasn't finished) —\n  useful for drop-off analysis; filter them out with `?complete=true` when the\n  user asks about finished answers.\n- When summarizing for the user, lead with counts (started/finished), then the\n  distribution of key `choice` answers, then notable free-text patterns.\n\n## Conventions worth following\n\nThese are the defaults that separate a quiz people finish from one they\nabandon. Follow them unless the user asks for something else.\n\n- Keep quizzes short: 5–7 questions. Completion drops below 50% past 6, and a\n  form finishing in under a minute completes 15% more often. Count what one\n  respondent sees — `show_if` branching doesn't count against you.\n- Every question must change a decision someone will actually make. If nobody\n  will act on the answer, cut it.\n- Open with an easy one-tap `choice`/`scale` about the respondent; put email,\n  budget, and demographics last (they're the top abandonment drivers when\n  asked early).\n- `one_question_per_page: true` for personality/lead-magnet/assessment\n  quizzes; grouped sections for intake and application forms people want to\n  power through with data already in hand.\n- Write the 3–5 `outcomes` buckets before the questions, then write questions\n  that separate them. Give each bucket a ~200-word `body` ending in a next\n  step, and make sure every reachable score lands in a bucket.\n- Set a `cover`: outcome-shaped title under 15 words, subtitle under 25, and a\n  `button_label` that states the promise rather than \"Start\". Keep\n  `show_progress: true` for anything over 3 questions.\n- Use `choice`/`scale` over free text when the user will want to aggregate;\n  at most 1 open question per 3.\n- One idea per question, no leading questions, positive framing (positively\n  framed forms complete at ~55% vs ~38%).\n- Use sections to group related questions; use a `statement` to explain\n  anything the respondent needs context for — especially before a sensitive\n  block, where saying why you're asking is a completion lever.\n- Prefer `email_optional` unless the user needs to contact every respondent,\n  or the outcome is the trade for the email.\n- After creating, always give the user the live `url` and mention answers will\n  appear in their dashboard at https://quizgen.dev/dashboard.\n\n## Limits\n\nEvery feature is on every plan. There is no cap on live quizzes or answers,\nand answers are kept until deleted.\n\n| Plan | Emails / month | Emails / 24 h | File uploads | Extras |\n|---|---|---|---|---|\n| Sandbox (no key) | — | — | — | 10 quizzes/hr/IP, 72h lifetime, 10 answers total; create-only; no `file_upload` or `payment` |\n| 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 |\n| 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 |\n| Plus $15/mo or $120/yr | 5,000 | 1,000 | 25 MB/file, 10 GB total | everything in Ad-free; SMS coming soon |\n\nNot built yet — do **not** tell a user these are available on any plan, or that\nupgrading unlocks them: custom domains, and SMS sending (`channel: \"sms\"`\nreturns 400 `not_yet_available`; it's coming soon on Plus). If a user wants a\ncustom domain, point them at sales@quizgen.dev rather than suggesting a\nworkaround or a plan change.\n\nIf the user is bumping into the email or storage caps, suggest Plus at\nhttps://quizgen.dev/pricing. Slugs and hosting are always on quizgen.dev.\n\n## Guides for agents & humans\n\nTask-specific write-ups, each plain markdown — read one directly instead of\nre-deriving the pattern from this file. Index: https://quizgen.dev/blog.md\n\n- https://quizgen.dev/blog/claude-mcp-servers-for-quizzes-and-forms.md — adding the MCP server, its tools, sandbox vs. permanent auth\n- https://quizgen.dev/blog/build-a-quiz-with-claude.md — full one-sentence-to-live-link workflow\n- https://quizgen.dev/blog/build-a-form-with-chatgpt.md — the Actions path and the manual-curl path\n- https://quizgen.dev/blog/ai-form-builder-guide.md — draft-assist AI vs. prompt-to-live-link AI\n- https://quizgen.dev/blog/typeform-alternative-for-ai-agents.md — capability comparison, not a sales page\n- https://quizgen.dev/blog/how-to-make-a-quiz-with-scoring.md — quiz with scoring end to end: points, buckets, reading scores back\n- https://quizgen.dev/blog/best-quiz-makers-with-scoring.md — honest comparison: Google Forms, Typeform, ScoreApp, Jotform, QuizGen\n- https://quizgen.dev/blog/lead-qualification-form-that-scores-itself.md — `outcomes` worked example\n- https://quizgen.dev/blog/personality-quiz-with-outcomes.md — scoring engine applied to personas\n- https://quizgen.dev/blog/event-rsvp-form-with-response-cap.md — `max_responses` + `close_at` + `show_if`\n- https://quizgen.dev/blog/headless-form-builder-explained.md — when headless beats a visual editor\n- https://quizgen.dev/blog/quiz-response-webhooks.md — setup, payload shape, signature verification\n- https://quizgen.dev/blog/free-ai-quiz-generator.md — what free actually includes: sandbox and the free plan\n- https://quizgen.dev/blog/ai-quiz-maker.md — what makes an AI quiz maker good; sandbox and plans explained\n- https://quizgen.dev/blog/jotform-alternative.md — capability comparison; when to pick each\n- https://quizgen.dev/blog/google-forms-alternative.md — for when you've outgrown Forms: scoring, webhooks, API\n- https://quizgen.dev/blog/surveymonkey-alternative.md — one-off surveys without a research-platform seat\n- https://quizgen.dev/blog/customer-feedback-form-template.md — complete JSON template, POST it as-is\n- https://quizgen.dev/blog/client-intake-form-template.md — complete JSON template with `show_if` branching\n- https://quizgen.dev/blog/invoice-template.md — invoice with line items, recorded approval, card payment\n- https://quizgen.dev/blog/order-form-template.md — order form with `price_options` card payment\n- https://quizgen.dev/blog/quote-template.md — quote with one-click approval, expiry, optional deposit\n- https://quizgen.dev/blog/event-registration-form-template.md — registration with ticket tiers, capacity cap\n- https://quizgen.dev/blog/contact-form-template.md — minimal contact form + webhook routing\n- https://quizgen.dev/blog/waitlist-signup-form.md — the canonical `kind: \"email_capture\"` example\n- https://quizgen.dev/blog/job-application-form-template.md — application + resume `file_upload` + points-based screening buckets\n- https://quizgen.dev/blog/liability-waiver-form-template.md — waiver text, minor/guardian branching, recorded agreement\n- https://quizgen.dev/blog/customer-satisfaction-survey-template.md — CSAT + NPS 0–10 scale, detractor follow-up\n- https://quizgen.dev/blog/exit-interview-questions.md — exit interview question bank + the form\n\n## More for agents & humans\n\n- This file: https://quizgen.dev/llms.txt (canonical, also at /docs.md; HTML mirror at /llms)\n- Landing page as markdown: https://quizgen.dev/index.md (JSON: /index.json)\n- Pricing as markdown: https://quizgen.dev/pricing.md (JSON: /pricing.json)\n- Guides index as markdown: https://quizgen.dev/blog.md (JSON: /blog.json)\n- Templates index (copy-paste prompts + JSON templates): https://quizgen.dev/templates.md (JSON: /templates.json)\n- This file as JSON: https://quizgen.dev/llms.json\n- MCP connect page: https://quizgen.dev/mcp\n- Installable agent skill & scaffolder: https://quizgen.dev/agents\n"}