Qaid
ARTICLE

Getting Started with Quests in 10 Minutes

Drop a multi-step intake form onto your site, define the questions in JSON, and start collecting structured responses — using a dental practice as the running example.

Qaid Team

Imagine you run a small dental practice. You have a static marketing site, and you want new patients to be able to book an intake appointment without picking up the phone. You do not need a full booking platform — you just need a short, friendly questionnaire that collects a name, a phone number, a preferred day, and a few details about why they are coming in.

That is the job @qaiddev/quests-embed is built for. It is the questionnaire sibling of @qaiddev/thumbs-embed: instead of capturing thumbs-up / thumbs-down feedback, it walks a visitor through a multi-step form defined entirely in JSON. This guide builds that dental intake form from scratch.

The Shape of a Questionnaire

A questionnaire is a plain JSON object. You give it an id, a title, an optional description, and an array of questions. You can also customize the button labels and the thank-you screen.

Here is a minimal intake form for our dental practice:

{
  "id": "new-patient-intake",
  "title": "New Patient Intake",
  "description": "A few quick questions so we can prepare for your visit.",
  "submitLabel": "Request appointment",
  "nextLabel": "Continue",
  "backLabel": "Back",
  "thankYouTitle": "Thanks — we will be in touch",
  "thankYouMessage": "Our front desk will call you within one business day to confirm your appointment time.",
  "questions": [
    {
      "id": "name",
      "type": "text",
      "label": "Your full name",
      "required": true,
      "minLength": 2,
      "maxLength": 80
    },
    {
      "id": "phone",
      "type": "text",
      "label": "Best phone number to reach you",
      "inputType": "tel",
      "required": true
    },
    {
      "id": "reason",
      "type": "multiple-choice",
      "label": "What brings you in?",
      "required": true,
      "options": [
        { "value": "cleaning", "label": "Routine cleaning" },
        { "value": "consultation", "label": "New patient consultation" },
        { "value": "pain", "label": "I am having pain" },
        { "value": "cosmetic", "label": "Cosmetic question" },
        { "value": "other", "label": "Something else" }
      ]
    }
  ]
}

That is the whole shape. Every other section in this guide is just adding fields or wiring this object into the page.

Installing via Script Tag

The simplest install is a single script tag. Drop it at the end of <body> and pass your endpoint, API key, and a URL to the JSON above.

<script
  src="https://unpkg.com/@qaiddev/quests-embed"
  data-endpoint="https://qaid.dev/api/quest-responses"
  data-api-key="your-api-key-here"
  data-config-url="/forms/new-patient-intake.json"
  data-container="#appointment-form"
></script>

If you would rather keep configuration as a JSON blob next to the loader, use the config-tag form:

<script type="application/json" data-quests-config>
  {
    "endpoint": "https://qaid.dev/api/quest-responses",
    "apiKey": "your-api-key-here",
    "configUrl": "/forms/new-patient-intake.json",
    "container": "#appointment-form"
  }
</script>
<script src="https://unpkg.com/@qaiddev/quests-embed"></script>

Both forms auto-initialize as soon as the script loads.

Installing via npm (SPAs)

If you build your site with React, Vue, Svelte, or another framework, install the package and construct the embed yourself:

npm install @qaiddev/quests-embed
import { QaidQuests } from "@qaiddev/quests-embed";

const embed = new QaidQuests({
  endpoint: "https://qaid.dev/api/quest-responses",
  apiKey: "your-api-key-here",
  configUrl: "/forms/new-patient-intake.json",
  container: "#appointment-form",
});

This is the right path for any app that hydrates client-side. You get a real instance back, which means you can call embed.destroy() on route changes or embed.getAnswers() on demand.

Inline Questionnaire vs configUrl

You have two ways to feed the questionnaire JSON to the embed:

  • Inline — pass the full object as questionnaire. The questionnaire ships with your page bundle. This is great for static forms that rarely change, and avoids a second network round-trip.
  • Remote — pass a URL as configUrl. The embed fetches the JSON at runtime. Use this when you want to update copy, reorder questions, or add options without redeploying your site.

Inline looks like this:

import { QaidQuests } from "@qaiddev/quests-embed";
import questionnaire from "./new-patient-intake.json";

new QaidQuests({
  endpoint: "https://qaid.dev/api/quest-responses",
  apiKey: "your-api-key-here",
  questionnaire,
  container: "#appointment-form",
});

If both questionnaire and configUrl are provided, the inline questionnaire wins.

Without a container option, the embed opens as a centered modal over a backdrop. This is the right choice when the form is a one-off action triggered from a button.

new QaidQuests({
  endpoint: "https://qaid.dev/api/quest-responses",
  apiKey: "your-api-key-here",
  configUrl: "/forms/new-patient-intake.json",
});

With a container selector, the embed renders inline inside that element. This is the right choice for a dedicated “Book an appointment” page where the form is the main content.

<section id="appointment-form" style="max-width: 520px; margin: 0 auto;"></section>
new QaidQuests({
  endpoint: "https://qaid.dev/api/quest-responses",
  apiKey: "your-api-key-here",
  configUrl: "/forms/new-patient-intake.json",
  container: "#appointment-form",
});

You can also tune the modal with modalWidth, backdropOpacity, and zIndex.

Question Types in Context

The dental intake naturally exercises four of the five built-in question types. Each question is just an object in the questions array.

Text — name, phone, email

The text type covers any single-line or multi-line free text. Use inputType to switch the native input mode for short fields.

{
  "id": "name",
  "type": "text",
  "label": "Your full name",
  "required": true,
  "minLength": 2,
  "maxLength": 80
},
{
  "id": "phone",
  "type": "text",
  "label": "Best phone number to reach you",
  "inputType": "tel",
  "required": true
},
{
  "id": "email",
  "type": "text",
  "label": "Email for appointment confirmation",
  "inputType": "email",
  "placeholder": "you@example.com"
}

Multi-line text — notes for the dentist

For longer answers, set multiline: true. The input becomes a textarea.

{
  "id": "notes",
  "type": "text",
  "label": "Anything we should know before your visit?",
  "description": "Allergies, anxieties, recent dental work — all helpful.",
  "multiline": true,
  "maxLength": 500
}

Date — preferred appointment day

The date type renders a native date picker. Use min and max to bound the selectable range. Here we forbid same-day requests by setting min to tomorrow.

{
  "id": "preferredDay",
  "type": "date",
  "label": "Preferred appointment day",
  "required": true,
  "min": "2026-04-27"
}

Multiple choice (single) — visit reason

Single-select multiple choice is the default. The answer is stored as the chosen value string.

{
  "id": "reason",
  "type": "multiple-choice",
  "label": "What brings you in?",
  "required": true,
  "options": [
    { "value": "cleaning", "label": "Routine cleaning" },
    { "value": "consultation", "label": "New patient consultation" },
    { "value": "pain", "label": "I am having pain" },
    { "value": "cosmetic", "label": "Cosmetic question" },
    { "value": "other", "label": "Something else" }
  ]
}

Multiple choice (multi) — insurance providers

Set multiple: true to allow several selections. The answer becomes an array of values.

{
  "id": "insurance",
  "type": "multiple-choice",
  "label": "Which insurance providers do you carry?",
  "description": "Select all that apply.",
  "multiple": true,
  "options": [
    { "value": "delta", "label": "Delta Dental" },
    { "value": "cigna", "label": "Cigna" },
    { "value": "metlife", "label": "MetLife" },
    { "value": "aetna", "label": "Aetna" },
    { "value": "none", "label": "None / paying out of pocket" }
  ]
}

A currency type also exists for collecting monetary values with a locale-aware formatter — useful for quote requests, but not a fit for this scenario.

Required Fields & Validation

Every question accepts required: true, which blocks the user from advancing without an answer. Each question type adds its own validation knobs:

  • Text: minLength (only enforced when required) and maxLength.
  • Currency: min and max for the numeric value.
  • Date: min and max as ISO date strings.

A common pattern: require the basics, leave the nuance optional.

{
  "id": "preferredDay",
  "type": "date",
  "label": "Preferred appointment day",
  "required": true,
  "min": "2026-04-27",
  "max": "2026-07-31"
}

The required flag is also what gates the visibility evaluator — questions hidden by visibleIf do not block submission, even when marked required.

Customizing the Submit & Thank-You Screen

The defaults are fine, but a small front-desk form deserves friendly copy. Override the submit button label and the final-screen text on the Questionnaire itself:

{
  "id": "new-patient-intake",
  "title": "New Patient Intake",
  "submitLabel": "Request appointment",
  "nextLabel": "Continue",
  "backLabel": "Back",
  "thankYouTitle": "Thanks — we will be in touch",
  "thankYouMessage": "Our front desk will call you within one business day to confirm your appointment time.",
  "questions": [ /* ... */ ]
}

The submitLabel only shows on the final step; intermediate steps use nextLabel.

Reading Answers from JS

The embed handles persistence on its own. As soon as the form is mounted, it POSTs to your endpoint to create a response record. As the visitor answers each question, the embed PATCHes to endpoint/{id} with the per-question value (debounced for text fields via saveDebounceMs). When the form is submitted, it POSTs to endpoint/{id}/submit with the full answers map.

That means you usually do not need to read answers in the browser at all — the server already has them. But if you are embedding inside a larger flow (a booking wizard, a checkout, an editor preview), you can grab a snapshot at any time:

const embed = new QaidQuests({
  endpoint: "https://qaid.dev/api/quest-responses",
  apiKey: "your-api-key-here",
  configUrl: "/forms/new-patient-intake.json",
  container: "#appointment-form",
});

// Later, e.g. in a "Save draft" button handler
const answers = embed.getAnswers();
console.log(answers.name, answers.reason, answers.insurance);

getAnswers() returns a read-only snapshot keyed by question id. Values are strings, numbers, string arrays, or null.

The embed also auto-generates a visitorId and stores it client-side so a single visitor’s session can be reconciled across page loads. There are no userId or userEmail init options — identity is handled separately, typically by passing the visitor’s contact info as answers.

What’s Next

Once you have the basics working, two follow-up guides are worth your time:

  • Branching with visibleIf — show or hide questions based on prior answers. For our dental intake, you would use this to ask follow-up pain questions only when reason is pain.
  • Theming and Inline Embeds — color tokens, custom CSS, and layout tweaks that match your site’s brand.

Summary

  • @qaiddev/quests-embed renders multi-step questionnaires defined as JSON, with answers auto-persisted to your endpoint.
  • Install via script tag for static sites, or via npm and new QaidQuests({ ... }) for SPAs.
  • Provide the form inline as questionnaire, or remotely via configUrl when you want to update copy without redeploying.
  • Without container, the embed opens as a modal; with container, it renders inline inside that element.
  • Five question types cover the common ground: text (with inputType and multiline), currency, range, date, and multiple-choice (with optional multiple: true).
  • Validate with required, minLength/maxLength, currency min/max, and date min/max.
  • Use embed.getAnswers() for client-side reads and embed.destroy() to clean up on route changes — though most apps just rely on the server-side persistence the embed handles for you.
Back to all articles