Intake forms
What a published form looks like over the API, how each question type wants its answer, and what happens after you submit.
An intake form is an ordered list of typed questions that a planner publishes under a public token. Rendering one yourself is two calls: read the form, then post the answers.
Reading a form
GET /public/forms/{token} returns the published definition — heading, intro, successMessage, redirectUrl, and questions:
{
"data": {
"id": "…",
"heading": "Tell us about your day",
"intro": null,
"successMessage": "Thanks — we'll be in touch within two business days.",
"redirectUrl": null,
"questions": [
{ "id": "3f1c…", "label": "Your name", "helpText": null, "type": "short_text", "required": true, "options": [], "autocomplete": "name" },
{ "id": "9a40…", "label": "Email", "helpText": null, "type": "email", "required": true, "options": [], "autocomplete": "email" },
{ "id": "c77e…", "label": "Wedding date", "helpText": "Leave blank if undecided", "type": "date", "required": false, "options": [] }
]
}
}Order in the array is display order. Render label, helpText, and a required marker from each question; the id is what you key the answer by. When a question has autocomplete, put it on the input's autocomplete attribute so the browser can fill in the visitor's own details; off means the question asks about someone else. Fetch the form on each page load rather than caching it — planners edit live forms, and answers are validated against the questions as they are at the moment of submission.
Answer formats
Send one entry per question in answers, keyed by question id. Optional questions may be omitted, null, or blank; required ones must carry a value. Values are trimmed and normalised on the way in, and these shapes are accepted:
type | Send | Notes |
|---|---|---|
short_text, phone | string | Up to 2,000 characters. Line breaks are collapsed. |
long_text | string | Up to 20,000 characters. Line breaks are kept. |
email | string | Must parse as an email address. |
number | number or numeric string | "1,200" and "$1200" are accepted; the value must be zero or more. |
date | "YYYY-MM-DD" string | Date only, no time. |
single_select | string | One of options, matched case-insensitively. |
multi_select | array of strings | Each one of options. Duplicates are dropped. |
yes_no | boolean, or "yes" / "no" | Stored as a boolean. |
Anything that does not fit is a 422 unprocessable with errors["answers.{id}"] set to a sentence you can show beside that question. See Errors.
Submitting
POST /api/v1/public/forms/{token}/submissions
Content-Type: application/json
{
"answers": { "3f1c…": "Emma Stone", "9a40…": "emma@example.com", "c77e…": "2027-06-12" },
"pageUrl": "https://www.example.com/contact"
}pageUrl is optional and is shown to the planner alongside the submission, so they know which page the lead came from. turnstileToken is optional too; pass it if your page renders Cloudflare Turnstile.
A 201 means the submission was stored:
{ "data": { "id": "…", "successMessage": "Thanks — we'll be in touch within two business days.", "redirectUrl": null } }Show successMessage, or if redirectUrl is set, send the visitor there instead. Both come from the form the planner configured, so a design change on their side needs no change on yours.
After the submission
From here the lead is the planner's. The submission appears in their Folia CRM, where it becomes a contact — automatically, or after the planner has looked it over. The API does not report which; the response is the same either way, so your page should treat 201 as done and not poll for anything further. If the planner has webhooks configured, their systems hear about the outcome through those — see Webhooks.