# Order form template that takes payment

Short answer: this is a complete QuizGen JSON definition for a small-business
order form — customer details, product picks, a pickup or delivery date —
with a native Stripe payment question at the end so the order and the
payment happen in the same step. POST it as-is (with your own products and
prices) and you get a hosted order page at a short link. If you're not on
the Business plan yet, skip to the fallback section below — the rest of the
template still works.

This is what an order form template is for: not a full storefront, but a
single link you can text or post that takes someone from "I want one" to
"paid" for a fixed, simple catalog — pre-orders, custom orders, small runs,
event merch. For the wider case — what a hosted order form does that a
printable one can't, and where it stops — see
https://quizgen.dev/online-order-form.

## The template

A print shop taking custom t-shirt orders, priced by quantity:

```json
{
  "schema_version": 1,
  "kind": "form",
  "title": "Riverside Print Co. — order form",
  "description": "Custom screen-printed shirts. Payment is collected when you submit.",
  "settings": {
    "collect_respondent": "email_required",
    "completion": {
      "message": "Thanks! We'll confirm your order and pickup or delivery time by email."
    }
  },
  "sections": [
    {
      "key": "order",
      "title": "Your order",
      "questions": [
        { "id": "customer_name", "type": "short_text", "prompt": "Your name",
          "required": true },
        { "id": "phone", "type": "phone", "prompt": "Phone number",
          "required": true, "help": "In case we need to reach you about your order." },
        { "id": "style", "type": "choice", "prompt": "Which shirt are you ordering?",
          "required": true,
          "options": [
            { "value": "crew_tee", "label": "Crew neck tee" },
            { "value": "hoodie", "label": "Hoodie" },
            { "value": "tank", "label": "Tank top" },
            { "value": "long_sleeve", "label": "Long sleeve" }
          ] },
        { "id": "fulfillment", "type": "choice", "prompt": "Pickup or delivery?",
          "required": true,
          "options": [
            { "value": "pickup", "label": "Pickup at the shop" },
            { "value": "delivery", "label": "Local delivery" }
          ] },
        { "id": "delivery_address", "type": "short_text", "prompt": "Delivery address",
          "required": true,
          "show_if": { "question": "fulfillment", "op": "eq", "value": "delivery" } },
        { "id": "needed_by", "type": "date", "prompt": "Date you need it by",
          "required": true },
        { "id": "notes", "type": "long_text", "prompt": "Design details, logo placement, colors",
          "help": "Attach nothing here — just describe it. We'll follow up by email if we need a file.",
          "rows": 4 }
      ]
    },
    {
      "key": "payment",
      "title": "Payment",
      "questions": [
        { "id": "package", "type": "payment",
          "prompt": "Choose your quantity and pay now", "required": true,
          "price_options": [
            { "value": "single", "label": "1 shirt", "amount_cents": 2500 },
            { "value": "five_pack", "label": "5-pack", "amount_cents": 11000,
              "detail": "$22 each" },
            { "value": "ten_pack", "label": "10-pack", "amount_cents": 20000,
              "detail": "$20 each" },
            { "value": "twenty_pack", "label": "20-pack (event size)", "amount_cents": 38000,
              "detail": "$19 each" }
          ],
          "button_label": "Pay & confirm order" }
      ]
    }
  ]
}
```

## What each piece is doing

- `kind: "form"` — the order shows on one page (not one question per
  screen), which matches how people expect to fill in an order.
- `collect_respondent: "email_required"` — this is the "customer contact"
  field. You don't need a separate `email` question; the address is
  captured automatically and attached to the response.
- `style` is a `choice` question. Note that every style costs the same here
  — see the rule below about why it has to.
- `fulfillment` is a `choice`, and `delivery_address` only appears when they
  pick delivery, via `show_if`.
- `needed_by` is a `date` question — always prefer it over a text field for
  dates; the answer comes back as `"YYYY-MM-DD"`.
- `package` is the one `payment` question this quiz is allowed to have.
  `price_options` (2–10 of them) is the right choice here instead of a
  single `amount_cents`, because the respondent is picking their quantity
  and paying the exact amount for it — one dropdown, one charge, no
  separate invoice step.

### The one rule to keep: price_options is the only place quantity lives

There is deliberately **no separate "How many shirts?" question** in this
template, and that is the most important thing to copy. A form takes one
`payment` question, and that question charges exactly the `amount_cents` of
the option the customer picked — nothing else on the form can change the
total. So the moment you add a free-entry field that also implies a price,
the two can disagree and the charge always wins:

> A `number` question asking "How many shirts?" alongside a `price_options`
> list is a trap. The customer types **20**, picks the **1 shirt — $25**
> option, and the form accepts it. The order arrives with `paid: true`
> against $25 for twenty shirts, and the only way to catch it is for a human
> to read every row.

The same trap catches per-option surcharges. If `style` had a "Hoodie
+$15" label, picking the hoodie would still charge the plain bundle price,
because `price_options` is the entire pricing model — labels are text, not
arithmetic. Either fold the surcharge into the price list (a "5-pack
hoodies" option at its real price) or charge one price across styles, as
this template does.

The rule that follows: **anything that changes what someone owes belongs in
`price_options`, and nowhere else.** Questions outside the payment question
are for things you need in order to fulfil the order — style, colour,
address, deadline, notes — not for things that determine the total. If your
combinations won't fit in ten price options, that is the signal to reach for
a real store rather than to add a quantity box.

## Payment needs the Business plan — here's the fallback

The `payment` question type is live, but it's gated: it only works on the
**Business plan ($49/mo)**, and only after the creator finishes Stripe
onboarding at [quizgen.dev/dashboard/payments](/dashboard/payments). Until
that's done, creating a quiz with a `payment` question returns a 402
`payments_not_ready` error. Money always goes to the creator's own Stripe
account — QuizGen takes a 2% platform fee on top, nothing more.

If you're not on Business yet, the rest of this template still works —
just drop the `payment` section and pick one of two fallbacks:

1. **Collect now, invoice after.** Take the order with everything above,
   then send an [invoice](/blog/invoice-template) with the total and a
   payment link once you've confirmed details (custom sizing, design
   proofs, whatever needs a human look first).
2. **Redirect to an external payment link.** Add
   `"completion": { "redirect_url": "https://buy.stripe.com/YOUR_LINK" }`
   to `settings` and the respondent lands on your existing Stripe Payment
   Link or PayPal.me page right after submitting. You lose the "pick your
   size, pay the right amount" convenience of `price_options`, but the
   order and the payment still happen in one flow.

Either fallback is a completely normal way to run this template — plenty of
small shops will use redirect links even after they're eligible for native
payment, especially if they already have Stripe Payment Links set up.

## Reading orders back

`GET /api/v1/quizzes/:id/responses` returns each order's `answers` (style,
package, fulfillment, notes) plus, for a paid order, `paid: true` and the
Stripe `payment_intent` id — that's what you reconcile against your Stripe
dashboard if a number ever looks off. Filter with `?complete=true` to skip
abandoned carts.

For same-day routing, don't poll — set a
[webhook](/blog/quiz-response-webhooks) on the quiz. Every completed order
POSTs itself to your endpoint the moment it's paid, so you can push it
straight into your fulfillment queue, a Slack channel, or a spreadsheet
instead of checking the dashboard.

## When a real store is the better tool

Be honest with yourself about the size of the catalog before reaching for
this. QuizGen's order form is for a **fixed, simple catalog** — a handful of
products or package sizes, no per-item options exploding into hundreds of
SKUs. It has no inventory tracking, no variant matrix (size × color × style
all at once), and no shipping-rate calculator. If you need any of those —
real inventory counts, a cart with multiple line items, calculated shipping
— use **Shopify** or **Square**; they're built for exactly that and it would
be a mistake to fight this template into doing their job. Reach for this
template instead for pre-orders, custom one-off orders, small production
runs, and event merch — the cases where "one page, one payment, done" is
the whole requirement.

## Let an AI assistant fill it in

Connect [QuizGen's MCP server](/mcp)
and say:

> Read https://quizgen.dev/blog/order-form-template.md and make me an order
> form for my candle shop: 3 scents, one size, $18 each, pickup only,
> needed-by date, and a notes field for gift messages. I'm on the Business
> plan and already connected to Stripe.

The assistant adapts the products and prices, POSTs the definition, and
hands back the live link.

## FAQ

**Can I sell more than one product in the same order?** Change `style` to
`multi_choice` so people can select several items, but keep the `payment`
question's `price_options` as fixed bundles or package sizes — there's no
per-line-item cart math, so price each option as a complete order. If
selecting a second item is supposed to cost more, that difference has to
appear as its own price option; a `multi_choice` selection can't add to the
total on its own.

**How do I let someone order a quantity I didn't list?** You don't — not in
the same submission. Add a "More than 20 / custom quantity" `choice` option
with no price attached and `show_if` a notes field off it, then quote them
separately with an [invoice](/blog/invoice-template). A form charges one
listed amount; anything negotiated needs a second step.

**What happens if someone abandons before paying?** The response is saved
with `complete: false` and no `paid` field. Nothing is charged, and nothing
ships — you can safely ignore incomplete responses when you tally orders.

**Can I cap how many orders I take?** Yes — `"max_responses": 50` in
`settings` closes the form automatically once you hit it, which is exactly
what a limited production run needs.

**Does the order form need to be a quiz?** No — `kind: "form"` renders
everything on one page, which is what people expect from an order form.
`kind` is advisory only; anything you set in `settings` overrides it.
