# QuizGen — API docs for AI assistants > docs_version: 2026-08-02 · schema_version: 1 · changelog: added sandbox mode > (create a quiz with no API key), hard rules, worked error example. 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. ## Hard rules - Always send `"schema_version": 1`. - Only use the 8 question types listed below. Never invent types (`dropdown`, `date`, `file`, `rating` do not exist — the closest are `choice`, `short_text`, and `scale`). - Question `id`s and option `value`s are snake_case; `id`s must be unique across the whole quiz, not just within a section. - Sections cannot nest. `show_if` may 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_errors` and 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. ## What you can build Quizzes, surveys, intake forms, lead-qualification forms (with scoring), "which one are you?" quizzes (with outcome buckets), event RSVPs, feedback forms. 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 1. Ask the user what they want to ask people (if not already clear). 2. Build a quiz JSON definition (schema below). 3. `POST /api/v1/quizzes` with the user's API key → you get back a live `url`. 4. Give the user the link. That's it — hosting, autosave, and storage are handled. 5. Later, read responses with `GET /api/v1/quizzes/:id/responses` and answer questions like "how's my quiz doing?" or "summarize the answers." If the user has no API key yet, you have two options: 1. **Sandbox (no key, instant):** POST the quiz with *no* `Authorization` header. It goes live immediately but expires in 24 hours, caps at 10 responses, and cannot be updated or read back. Use it to show the user a working link right now, and tell them: "this is a temporary preview — create a free account at https://quizgen.dev/login and give me an API key to keep it." Sandbox creates are limited to 10/hour per IP (429 `rate_limited`). 2. **Real key:** send the user to https://quizgen.dev/login — the dashboard issues keys (`qg_live_...`). ## Auth Every `/api/v1` request needs: ``` Authorization: Bearer qg_live_... Content-Type: application/json ``` Exception: `POST /quizzes` with the header omitted entirely creates a sandbox quiz (see above). A wrong or revoked key still returns 401 — sandbox only triggers when no `Authorization` header is present at all. ## Endpoints Base URL: `https://quizgen.dev/api/v1` | Method | Path | Purpose | |---|---|---| | 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, response_count). | | 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. | | DELETE | `/quizzes/:id` | Soft-delete. The link stops working. | | GET | `/quizzes/:id/responses` | List responses. `?complete=true` filters to finished ones. Add `?format=csv` (or `Accept: text/csv`) for CSV. | ### Worked example: validation error → fix → success Request with a mistake: ```json 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: ```json { "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: ```json { "id": "qz_1f6b...", "slug": "x7km2p", "url": "https://quizgen.dev/q/x7km2p", "status": "live", "title": "Signup", "definition_version": 1, "response_count": 0 } ``` Error codes: `invalid_quiz` (422), `unauthorized` (401), `not_found` (404), `invalid_json` (400), `rate_limited` (429, sandbox creation only). 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. ## Quiz JSON schema (schema_version 1) Minimal working quiz: ```json { "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`. | | `title` | yes | Shown as the quiz heading and link preview. | | `description` | no | Subtitle under the title. | | `sections` | yes | 1–30 sections; each renders as a page. Single-section quizzes hide section chrome. | | `settings` | no | See below. | | `theme` | no | `{ "accent": "#0d9488", "mode": "light" \| "dark" \| "auto" }`. | | `outcomes` | no | Scoring + result buckets. See below. | `settings` (all optional): ```json { "collect_respondent": "none" | "email_optional" | "email_required", "autosave": true, "show_progress": true, "close_at": "2026-12-31T00:00:00Z", "max_responses": 200, "completion": { "message": "Thanks!", "redirect_url": null } } ``` Sections: `{ "key": "unique_snake_case", "title": "...", "blurb": "...", "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?}`), `follow_up` (string — shows a free-text box after selecting; saved as `__followup`) | | `multi_choice` | pick several | `options` (same shape), `min`, `max` selections | | `scale` | 1–N rating | `min` (0 or 1), `max` (2–10), `min_label`, `max_label` | | `email` | email w/ validation | `placeholder` | | `number` | numeric | `min`, `max`, `unit` | | `statement` | info panel, not a question | `title`, `body` (array of paragraphs). No prompt, collects nothing. | Notes: - Option `value`s are snake_case identifiers; `label` is what people see; `detail` is an optional second line on the card. - There is no file-upload or payment type yet (payments coming to the Business plan). ## Branching (`show_if`) Show a question only when a condition on an earlier answer holds: ```json { "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: ```json "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" } ] } ``` - `points` maps 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's `title`/`body` show on the completion screen; `redirect_url` (if set) redirects instead. - Each response stores `score` and `outcome` (the bucket key) — filter and sort by them when reading responses. ## Reading responses `GET /api/v1/quizzes/:id/responses` returns: ```json { "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, "started_at": "...", "completed_at": "...", "meta": { "referrer": "..." } } ] } ``` - `answers` is a flat `question_id → value` map (`multi_choice` → array of values). - `complete: false` rows are partial (autosaved, respondent hasn't finished) — useful for drop-off analysis; filter them out with `?complete=true` when the user asks about finished answers. - When summarizing for the user, lead with counts (started/finished), then the distribution of key `choice` answers, then notable free-text patterns. ## Conventions worth following - Keep quizzes short: 5–10 questions unless the user asks for more. - Use `choice`/`scale` over free text when the user will want to aggregate. - Use sections to group related questions; use a `statement` to explain anything the respondent needs context for. - Prefer `email_optional` unless the user needs to contact every respondent. - After creating, always give the user the live `url` and mention answers will appear in their dashboard at https://quizgen.dev/dashboard. ## Limits | Plan | Live quizzes | Answers/quiz/mo | Retention | Extras | |---|---|---|---|---| | Sandbox (no key) | 10/hr/IP, 24h lifetime | 10 total | until expiry | create-only; can't update or read responses | | Free | 3 | 100 | 90 days | — | | Pro $15/mo | unlimited | 5,000 | forever | custom theme/logo, email sends | | Business $49/mo | unlimited | 25,000 | forever | custom domain, payments | When a quiz hits its cap, the hosted page shows "no longer accepting responses" and `/api/respond` returns 410 `quiz_full` — existing responses stay readable. If the user is bumping into limits, suggest the next plan up at https://quizgen.dev/#pricing. Slugs and hosting are always on quizgen.dev. ## More for agents & humans - This file: https://quizgen.dev/llms.txt (canonical, also at /docs.md) - Landing page as markdown: https://quizgen.dev/index.md - Pricing as markdown: https://quizgen.dev/pricing.md - Installable agent skill & scaffolder: https://quizgen.dev/agents