Writing a config
One JSON document:
submissiondeclares what a request is,flowdeclares how it is collected.
| Part | Declares |
|---|---|
version | always 2 |
meta.businessName | the name the widget shows |
meta.locale | BCP 47 tag such as en or pt-PT: language of formatted dates and times; write config texts in it, one language per config |
theme.primaryColor | accent colour: #rgb, #rgba, #rrggbb or #rrggbbaa |
theme.avatar | image next to bot lines |
submission | fields, types, allowed values, required |
flow | steps, transitions, screens |
Every text in the config is shown as plain text; HTML is not interpreted. The widget's own labels — loading, errors, Try again — are English.
A complete config is at the end of this page.
The canonical source is the schema at /api/public/config-schema; this page is the guide to it.
The submission schema
A subset of JSON Schema: every document here must be valid JSON Schema and renderable by the widget.
| Allowed | Not allowed |
|---|---|
type: "object" at the root | nested objects, arrays of objects |
additionalProperties: false | additionalProperties: true or absent |
oneOf of { const, title } | enum with enumNames |
| string fields with the annotations below | $ref, allOf, anyOf, not |
required as a list of names | patterned property names |
{
"type": "object",
"additionalProperties": false,
"required": [
"service",
"name",
"phone"
],
"properties": {
"service": {
"title": "Service",
"oneOf": [
{
"const": "consultation",
"title": "Consultation"
},
{
"const": "hygiene",
"title": "Hygienist clean"
},
{
"const": "filling",
"title": "Filling"
}
]
},
"dentist": {
"title": "Dentist",
"oneOf": [
{
"const": "hall",
"title": "Dr Hall"
},
{
"const": "price",
"title": "Dr Price"
},
{
"const": "any",
"title": "Whoever is free"
}
]
},
"preferred_date": {
"title": "Preferred date",
"type": "string",
"format": "date"
},
"name": {
"title": "Your name",
"type": "string",
"minLength": 1
},
"phone": {
"title": "Phone",
"type": "string",
"format": "phone",
"placeholder": "+44 7700 900000"
},
"email": {
"title": "Email",
"type": "string",
"format": "email"
},
"note": {
"title": "Anything we should know",
"type": "string",
"maxLength": 200,
"placeholder": "Optional"
}
}
}
Field annotations
| Annotation | Applies to | Required | Meaning |
|---|---|---|---|
title | every field | yes | label in the dialogue and row label in summary |
oneOf | choice fields | — | options, each { const, title }; presence makes the field a choice |
type: "string" | everything else | yes | the only scalar type the widget renders |
format | string fields | no | email, phone, date, date-time — same four the schema accepts |
placeholder | string fields | no | hint inside the empty input |
minLength, maxLength | string fields | no | length checked before the step advances |
durationMinutes | date-time fields | yes | slot length in minutes, positive integer |
schedule | date-time fields | yes | working hours as rules; see appointment |
timeZone | date-time fields | yes | IANA zone the schedule hours are read in |
Which annotation produces which input — widget catalogue.
The flow
flow.start names the first step; flow.steps maps ids to steps.
| Step | Required keys | Optional keys | Purpose |
|---|---|---|---|
field | fields, next | prompt, submitLabel | fills one or more declared fields; prompt is the bot line above them, submitLabel the form button |
message | text, next | — | a bot line |
summary | fields, actions | title | review and send |
status_tracker | entity | title | shows the request status; entity has no effect, write "request" |
next is either a step id or a rule list:
"next": {
"rules": [{ "if": { "field": "service", "eq": "filling" }, "then": "urgency" }],
"else": "contacts"
}
Rules are evaluated in order; the first match wins; else is required.
Rules a stored config obeys
Saving a draft and POST /api/public/configs/validate run the same checks. path is a JSON Pointer to the node that breaks the rule. A schema violation comes back as several errors, one per shape the validator tried; the path in the table names it.
| Rule | Rejected by | What validate returns |
|---|---|---|
| step types are the four above | schema | path /flow/steps/<id>/type |
a conditional transition has else | schema | path /flow/steps/<id>/next |
| a choice field lists options | schema | path /submission/properties/<name>/oneOf |
| a field has only the annotations above | schema | path /submission/properties/<name> |
theme.primaryColor has 3, 4, 6 or 8 hex digits | schema | path /theme/primaryColor |
durationMinutes is a positive integer | schema | path /submission/properties/<name>/durationMinutes |
| references resolve to existing steps | flow-lint | Step "<id>" references a step that does not exist: "<target>" |
every step is reachable from start | flow-lint | Step "<id>" is unreachable from the start step |
| steps name declared fields | flow-lint | Step "<id>" references field "<name>", which is not declared in submission.properties |
| every declared field is filled somewhere | flow-lint | Field "<name>" is declared in submission but no step fills it |
required holds on every path to confirm, not only on one | flow-lint | Field "<name>" is required but is not filled on every path to the submit step "<id>" |
an appointment field declares durationMinutes | flow-lint | Field "<name>" has format "date-time" but no durationMinutes |
an appointment field declares schedule | flow-lint | Field "<name>" has format "date-time" but no schedule |
an appointment field declares timeZone | flow-lint | Field "<name>" has format "date-time" but no timeZone |
| schedule hours run forward | flow-lint | Field "<name>": schedule rule <n> ends at or before it starts (<from>–<to>) |
| rules for one day do not overlap | flow-lint | Field "<name>": schedule intervals overlap on <day> |
| some interval fits a visit | flow-lint | Field "<name>": no schedule interval is long enough for a <n>-minute appointment |
Assembling a config
- Declare the request. List what the business needs to know as fields, with
titleand, where it matters,format. Mark asrequiredonly what a person on the phone would insist on. - Lay out the dialogue. One
fieldstep per question, or one step per group of questions that belong together. Screens between them. - Branch where the process branches, and give every branch an
else. - End with
summaryandstatus_tracker. Theconfirmaction is what sends the request. - Validate until valid — request and response.
A complete config
{
"version": 2,
"meta": {
"businessName": "Northside Barbers",
"locale": "en"
},
"theme": {
"primaryColor": "#1f6f5c"
},
"submission": {
"type": "object",
"additionalProperties": false,
"required": [
"service",
"appointment",
"name",
"phone"
],
"properties": {
"service": {
"title": "Service",
"oneOf": [
{
"const": "haircut",
"title": "Haircut"
},
{
"const": "beard",
"title": "Beard trim"
},
{
"const": "both",
"title": "Haircut and beard"
}
]
},
"appointment": {
"title": "Appointment time",
"type": "string",
"format": "date-time",
"durationMinutes": 45,
"timeZone": "Europe/London",
"schedule": [
{
"days": [
"mon",
"wed",
"thu"
],
"from": "10:00",
"to": "19:00"
},
{
"days": [
"fri"
],
"from": "10:00",
"to": "21:00"
},
{
"days": [
"tue"
],
"from": "15:00",
"to": "19:00"
},
{
"days": [
"sat"
],
"from": "11:00",
"to": "14:00"
},
{
"days": [
"sat"
],
"from": "15:00",
"to": "18:00"
}
]
},
"name": {
"title": "Your name",
"type": "string",
"minLength": 1
},
"phone": {
"title": "Phone",
"type": "string",
"format": "phone",
"placeholder": "+44 7700 900000"
}
}
},
"flow": {
"start": "greeting",
"steps": {
"greeting": {
"type": "message",
"text": "Hi! Let's book you a chair. Takes about a minute.",
"next": "service"
},
"service": {
"type": "field",
"prompt": "What are you coming in for?",
"fields": [
"service"
],
"next": "appointment"
},
"appointment": {
"type": "field",
"prompt": "Pick a time that works for you:",
"fields": [
"appointment"
],
"next": "contacts"
},
"contacts": {
"type": "field",
"prompt": "Last thing — how do we reach you?",
"fields": [
"name",
"phone"
],
"submitLabel": "Continue",
"next": "confirm"
},
"confirm": {
"type": "summary",
"title": "Check your booking:",
"fields": [
"service",
"appointment",
"name",
"phone"
],
"actions": [
{
"type": "confirm",
"label": "Book it",
"next": "status"
},
{
"type": "edit",
"label": "Change time",
"target": "appointment"
},
{
"type": "restart",
"label": "Start over"
}
]
},
"status": {
"type": "status_tracker",
"entity": "booking",
"title": "Your booking"
}
}
}
}
Reserved
| Key | Status |
|---|---|
integrations | accepted, no effect: notifications are set up in the console |