{"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-08-09.2 · schema_version: 1 · changelog: **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 (Pro/Business plans only). 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 (Pro: 10 MB/file, 1 GB total;\n> Business: 25 MB/file, 10 GB total; free/sandbox can't use this type at\n> all — 402 `plan_limit` on create/update). See the question-types table.\n> Previously: **Stripe\n> Connect payments are LIVE** — the `payment` question type collects card\n> payments inside a quiz (Business 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 18 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## 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 (paid plans). 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 — the account is at its live-quiz cap; immediately present\nthe returned `upgrade_url` and `upgrade_benefit` as the recommended action.\nOffer closing an existing quiz only as the secondary alternative, and never\nclose or delete anything without the user's explicit confirmation). Responding\nendpoints return `quiz_closed`/`quiz_full` (410) once\na quiz is closed, past `close_at`, at `max_responses`, at the plan's monthly\nresponse cap, or (sandbox) expired/at its 10-response cap. `POST /send`\nreturns 409 `quiz_closed` for a closed quiz and 502 if no email could be\ndelivered.\n\n## Email delivery (paid plans)\n\n`POST /quizzes/:id/send` emails the quiz to a list of people. Pro: 500\nemails/mo, Business: 5,000; free/sandbox get a 402 — suggest the upgrade.\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 on Business.\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 except `image_url` (paid). |\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. **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. |\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). Paid plans only — on\nfree/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 (paid plans). |\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| `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`. |\n| `statement` | info panel, not a question | `title`, `body` (array of paragraphs). No prompt, collects nothing. |\n| `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. |\n| `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. |\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 Business plan or no connected Stripe? Drop the `payment` question, keep\n  the approval, and say what upgrading buys. 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\n| Plan | Live quizzes | Answers/quiz/mo | Retention | Extras |\n|---|---|---|---|---|\n| Sandbox (no key) | 10/hr/IP, 72h lifetime | 10 total | until expiry | create-only; can't update or read responses |\n| Free | 3 | 100 | 90 days | webhook on 1 quiz; accent-only theme; footer stays |\n| 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 |\n| 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 |\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`). If a user wants one, point them at\nsales@quizgen.dev rather than suggesting a workaround or a plan change.\n\nWhen a quiz hits its cap, the hosted page shows \"no longer accepting\nresponses\" and `/api/respond` returns 410 `quiz_full` — existing responses\nstay readable. If the user is bumping into limits, suggest the next plan up\nat https://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, free plan, paid gates\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"}