# Mind the Front
> An embeddable chat widget that collects service requests on your own site.
```html
```
`init` options:
| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `publishableKey` | string | — | loads the published config for that business |
| `container` | Element | — | mounts inline in the element instead of a floating button; call `init` after the element exists, `null` gives the floating button |
| `apiUrl` | string | origin of `embed.js` | overrides the API address |
The publishable key identifies a business and authorises nothing; it is public by design. A key without a published config mounts nothing and logs a warning to the console.
---
# Widget catalogue
> Every input the widget renders and the declaration that produces it.
An input is derived from the field declaration in `submission.properties`. A step lists field names; it never names an input kind.
| Kind | Declaration | Renders as | Checked before advancing |
| --- | --- | --- | --- |
| [Choice](/docs/widgets#choice) | `oneOf` of `const` + `title` | buttons, or `select` in a form | value is one of `oneOf` |
| [Text](/docs/widgets#text) | `type: "string"` | `input type="text"` | `minLength`, `maxLength` |
| [Phone](/docs/widgets#phone) | `format: "phone"` | `input type="tel"` | 7–15 digits |
| [Email](/docs/widgets#email) | `format: "email"` | `input type="email"` | `local@domain.tld` |
| [Date](/docs/widgets#date) | `format: "date"` | `input type="date"` | `YYYY-MM-DD` |
| [Appointment](/docs/widgets#appointment) | `format: "date-time"` + `schedule` | grid of slots by day | slot came from the grid |
Applies to every kind:
| In the declaration | Effect |
| --- | --- |
| name in `submission.required` | asterisk on the label; step will not advance while empty |
| `placeholder` | hint inside the empty input |
| `title` | the label, and the row label in `summary` |
| `theme.primaryColor` | accent of buttons, launcher and selected slot |
| `meta.locale` | language of formatted dates and times |
## Choice
```json
{
"title": "Service",
"oneOf": [
{
"const": "haircut",
"title": "Haircut"
},
{
"const": "beard",
"title": "Beard trim"
},
{
"const": "both",
"title": "Haircut and beard"
}
]
}
```
One declaration, two renderings, selected by the step:
| Step | Rendering |
| --- | --- |
| `"fields": ["service"]` | a row of buttons, one per option; the click advances the step |
| `"fields": ["property_type", "rooms", "address"]` | `select` inside a form with a submit button |
`submitLabel` sets the button text of the form rendering; default is `Send`.
## Text
```json
{
"title": "Anything we should know",
"type": "string",
"maxLength": 200,
"placeholder": "Optional"
}
```
Single line. `maxLength` is enforced as the visitor types; `minLength` on advance.
## Phone
`format: "phone"`: 7 to 15 digits after stripping spaces, `+`, `-`, `(`, `)`. Anything else is rejected.
```json
{
"title": "Phone",
"type": "string",
"format": "phone",
"placeholder": "+44 7700 900000"
}
```
## Email
`format: "email"`: one `@`, a dot in the domain, no spaces.
```json
{
"title": "Email",
"type": "string",
"format": "email"
}
```
## Date
`format: "date"`: a calendar date without time or zone, `YYYY-MM-DD`.
```json
{
"title": "Preferred date",
"type": "string",
"format": "date"
}
```
Choose between `date` and `appointment` by what the business does with it: `date` states a preference the owner confirms later, `appointment` takes a slot.
## Appointment
```json
{
"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"
}
]
}
```
`format: "date-time"` with three required [annotations](/docs/config#field-annotations): `durationMinutes`, `schedule`, `timeZone`. Slots are `durationMinutes` long and cover the next 14 days.
`schedule` rules:
| Rule | Result |
| --- | --- |
| `{ "days": ["mon","wed"], "from": "10:00", "to": "19:00" }` | same hours on both days |
| two rules for `sat`, `11:00–14:00` and `15:00–18:00` | a break between them |
| a day absent from every rule | closed that day |
| two overlapping rules for one day | rejected by flow-lint |
| `from` later than `to` | rejected by flow-lint |
| remaining window shorter than `durationMinutes` | no slot offered in it |
`from` and `to` are wall-clock time in `timeZone`; an offset such as `10:00+01:00` is rejected.
Picking a slot does not hold it: free time is the schedule minus the owner's calendar at the moment of asking, and the owner confirms the request.
## Screens
Screens produce no values.
| Screen | Declaration | Behaviour |
| --- | --- | --- |
| `message` | `text`, `next` | a bot line |
| `summary` | `title`, `fields`, `actions` | rows of collected values, then the actions below |
| `status_tracker` | `entity`, `title` | the request status: `new`, `confirmed` or `rejected` |
`summary` actions:
| Action | Effect |
| --- | --- |
| `confirm` | sends the request and goes to `next`; every `required` field must be filled by now |
| `edit` | returns to the step in `target`, keeping every entered value |
| `restart` | clears the dialogue and returns to `flow.start` |
```json
{
"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"
}
]
}
```
Next: [assembling these into a config](/docs/config).
---
# Writing a config
> One JSON document: `submission` declares what a request is, `flow` declares 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](/docs/config#a-complete-config).
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 |
```json
{
"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](/docs/widgets#appointment) |
| `timeZone` | `date-time` fields | yes | IANA zone the schedule hours are read in |
Which annotation produces which input — [widget catalogue](/docs/widgets).
## 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:
```json
"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//type` |
| a conditional transition has `else` | schema | path `/flow/steps//next` |
| a choice field lists options | schema | path `/submission/properties//oneOf` |
| a field has only the annotations above | schema | path `/submission/properties/` |
| `theme.primaryColor` has 3, 4, 6 or 8 hex digits | schema | path `/theme/primaryColor` |
| `durationMinutes` is a positive integer | schema | path `/submission/properties//durationMinutes` |
| references resolve to existing steps | flow-lint | `Step "" references a step that does not exist: ""` |
| every step is reachable from `start` | flow-lint | `Step "" is unreachable from the start step` |
| steps name declared fields | flow-lint | `Step "" references field "", which is not declared in submission.properties` |
| every declared field is filled somewhere | flow-lint | `Field "" is declared in submission but no step fills it` |
| `required` holds on every path to `confirm`, not only on one | flow-lint | `Field "" is required but is not filled on every path to the submit step ""` |
| an appointment field declares `durationMinutes` | flow-lint | `Field "" has format "date-time" but no durationMinutes` |
| an appointment field declares `schedule` | flow-lint | `Field "" has format "date-time" but no schedule` |
| an appointment field declares `timeZone` | flow-lint | `Field "" has format "date-time" but no timeZone` |
| schedule hours run forward | flow-lint | `Field "": schedule rule ends at or before it starts (–)` |
| rules for one day do not overlap | flow-lint | `Field "": schedule intervals overlap on ` |
| some interval fits a visit | flow-lint | `Field "": no schedule interval is long enough for a -minute appointment` |
## Assembling a config
1. **Declare the request.** List what the business needs to know as fields, with `title` and, where it matters, `format`. Mark as `required` only what a person on the phone would insist on.
2. **Lay out the dialogue.** One `field` step per question, or one step per group of questions that belong together. Screens between them.
3. **Branch where the process branches**, and give every branch an `else`.
4. **End with `summary` and `status_tracker`.** The `confirm` action is what sends the request.
5. **Validate** until valid — [request and response](/docs/ai-agents#1-3-build-and-validate-without-an-account).
## A complete config
```json
{
"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 |
---
# HTTP API
> Public endpoints the widget calls, owner endpoints your tooling calls. Full contract: [`/openapi.json`](/openapi.json).
| Contour | Prefix | Host | Auth |
| --- | --- | --- | --- |
| Public | `/api/public/*` | apex | none; CORS open |
| Owner | `/api/*` | console | session cookie, or `X-Owner-Token` |
Owner operations are scoped to the business behind the credential. Another owner's resource returns `404`.
## Credentials
| Credential | Obtained from | Scope | Revocation |
| --- | --- | --- | --- |
| `publishableKey` | console | identifies a business; authorises nothing | — |
| `X-Owner-Token` | console, on request | every owner operation for one business | issue a new one; the previous stops working at once |
| `X-Visitor-Token` | `POST /api/public/conversations` | one visitor's request and its status | — |
Issuing a new owner token leaves the publishable key and the published config unchanged.
## Rate limits
| Scope | Limit | Applies to |
| --- | --- | --- |
| public reads, per IP | 60/min | config, status, slots, showcase |
| public writes, per IP | 10/min | conversations, requests, validation |
| generation, per business | 10/hour | `POST /configs/generate` |
Over the limit: `429` with `{"status":429,"detail":"Too many requests"}`.
## Errors
| Status | Meaning |
| --- | --- |
| `400` | a required header, parameter or field is missing or fails its constraints |
| `401` | no session and no owner token |
| `402` | the business has reached its plan limit: the widget does not show, the request is not taken, or the plan's generations are used up — `GET /api/businesses/{id}/plan` says which |
| `404` | absent, or not yours — the two are not distinguished |
| `409` | a conflict: the email is taken, the chosen time was taken meanwhile, the config is already published |
| `413` | body over 64 KB |
| `422` | validation rejected the document or the values; the body names the rule |
| `429` | rate limit |
| `503` | an optional integration is not configured (generation without an API key, calendar without OAuth credentials, password reset without email) |
Body: `application/problem+json` with `status` and `detail`; `errors` on `422`, `field` on `409`.
## Caching
`GET /api/public/configs/{key}` is ETagged and publicly cacheable; publishing invalidates it. `GET /api/public/availability` is `no-store`. `GET /api/public/showcase` is `no-cache`.
## Public endpoints
| Method | Path | What it does | Auth |
| --- | --- | --- | --- |
| `GET` | `/api/public/configs/{publishableKey}` | Fetch the published config for a business | none |
| `POST` | `/api/public/conversations` | Start a visitor conversation | none |
| `POST` | `/api/public/submissions` | Submit a request | none |
| `GET` | `/api/public/submissions/latest` | Read the visitor's latest request | X-Visitor-Token |
| `GET` | `/api/public/availability` | List free slots for an appointment field | none |
| `GET` | `/api/public/showcase` | List the examples shown on the site | none |
| `POST` | `/api/public/configs/validate` | Validate a config without storing it | none |
| `GET` | `/api/public/config-schema` | Fetch the canonical config JSON Schema | none |
#### `GET /api/public/configs/{publishableKey}`
What the widget loads on mount. ETagged and publicly cacheable; publishing a new version invalidates it at once.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `publishableKey` | path | yes | Public key of the business, shown in the console. |
| `If-None-Match` | header | no | Conditional request; a matching ETag returns 304. |
Response: A process config. Its schema is served at `GET /api/public/config-schema`.
| Status | Meaning |
| --- | --- |
| `200` | The published config document. |
| `304` | Not modified. |
| `402` | The widget is unavailable: the business has reached its plan limit (`GET /api/businesses/{businessId}/plan` says which). The widget does not mount. |
| `404` | No such key, or the business has never published. |
| `429` | Per-IP read limit exceeded: 60 requests per minute by default. |
```bash
curl https://mindthefront.com/api/public/configs/pk_live_8f21
```
#### `POST /api/public/conversations`
Issues the visitor token that ties a dialogue to a business.
| Body | Type | Required |
| --- | --- | --- |
| `publishableKey` | string | yes |
| Response | Type | Required |
| --- | --- | --- |
| `visitorToken` | string | yes |
| Status | Meaning |
| --- | --- |
| `201` | `{ "visitorToken": "..." }` |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `404` | Unknown publishable key. |
| `429` | Per-IP write limit exceeded: 10 requests per minute by default. |
#### `POST /api/public/submissions`
Values are validated against the schema of the published config.
| Body | Type | Required |
| --- | --- | --- |
| `visitorToken` | string | yes |
| `fields` | object | yes |
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `status` | one of `new`, `confirmed`, `rejected` | yes |
| `fields` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| `erasedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `201` | The stored request with its status. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `402` | The business has reached its plan limit, including the allowance for conversations started before it; the request is not stored. |
| `404` | Unknown visitor token. |
| `409` | The chosen time has just been taken; `field` names the appointment field. |
| `422` | Values do not satisfy the published schema. |
| `429` | Per-IP write limit exceeded: 10 requests per minute by default. |
#### `GET /api/public/submissions/latest`
The visitor's latest request and its status. The token goes in a header, not in the query string.
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `status` | one of `new`, `confirmed`, `rejected` | yes |
| `fields` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| `erasedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The request with its current status. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `404` | No request for this token. |
| `429` | Per-IP read limit exceeded: 60 requests per minute by default. |
#### `GET /api/public/availability`
The declared schedule minus the owner's calendar, over a 14-day horizon. `sourceUnavailable: true` means the grid was built from the schedule alone and some of it may already be taken.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `publishableKey` | query | yes | Business key. |
| `field` | query | yes | Name of a `format: date-time` field in the published config. Anything else returns 404. |
| Response | Type | Required |
| --- | --- | --- |
| `slots` | array of string (date-time) | yes |
| `timeZone` | string or null | yes |
| `durationMinutes` | integer | yes |
| `sourceUnavailable` | boolean | yes |
| Status | Meaning |
| --- | --- |
| `200` | `slots`, `timeZone`, `durationMinutes`, `sourceUnavailable`. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `404` | Unknown key, or a field that is not an appointment field. |
| `429` | Per-IP read limit exceeded: 60 requests per minute by default. |
#### `GET /api/public/showcase`
Publishable key, business name and the description each example was generated from.
| Response, array of objects | Type | Required |
| --- | --- | --- |
| `publishableKey` | string | yes |
| `businessName` | string | yes |
| `prompt` | string | yes |
| Status | Meaning |
| --- | --- |
| `200` | Examples in display order; an empty list when the showcase is empty. |
| `429` | Per-IP read limit exceeded: 60 requests per minute by default. |
#### `POST /api/public/configs/validate`
Runs the checks a save runs: JSON Schema first, then flow-lint. Nothing is stored and no key is needed. Status is 200 whether or not the document is valid. Each error carries `path`, `message` and `rule` (`schema` or `flow-lint`).
Body: A process config. Its schema is served at `GET /api/public/config-schema`.
| Response | Type | Required |
| --- | --- | --- |
| `valid` | boolean | yes |
| `errors` | array of object: `path`, `message`, `rule` | yes |
| Status | Meaning |
| --- | --- |
| `200` | `{ "valid": true, "errors": [] }`, or `valid: false` with one entry per problem. |
| `400` | The body is not JSON, or a required parameter is missing. |
| `413` | Body over 64 KB. |
| `429` | Per-IP write limit exceeded: 10 requests per minute by default. |
```bash
curl -X POST https://mindthefront.com/api/public/configs/validate \
-H 'Content-Type: application/json' --data @config.json
```
#### `GET /api/public/config-schema`
The exact file the server validates against, not a copy of it. ETagged and publicly cacheable.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `If-None-Match` | header | no | Conditional request; a matching ETag returns 304. |
Response: A JSON Schema 2020-12 document.
| Status | Meaning |
| --- | --- |
| `200` | JSON Schema 2020-12 document. |
| `304` | Not modified. |
```bash
curl https://mindthefront.com/api/public/config-schema
```
## Account and session
| Method | Path | What it does | Auth |
| --- | --- | --- | --- |
| `POST` | `/api/auth/register` | Start creating an owner account | none |
| `POST` | `/api/auth/register/confirm` | Confirm the email and create the account | none |
| `POST` | `/api/auth/login` | Sign in | none |
| `POST` | `/api/auth/logout` | Sign out | owner session only |
| `GET` | `/api/auth/me` | Read the signed-in account | owner session only |
| `DELETE` | `/api/auth/account` | Delete the account | owner session only |
| `POST` | `/api/auth/password-reset` | Request a password reset | none |
| `POST` | `/api/auth/password-reset/confirm` | Set a new password | none |
#### `POST /api/auth/register`
Sends a confirmation link to the email; the account and its first business are created when the link is confirmed (`confirmRegistration`). A server without email configured creates the account at once and answers 201.
| Body | Type | Required |
| --- | --- | --- |
| `email` | string (email) | yes |
| `password` | string | yes |
| `businessName` | string | yes |
| `agent` | boolean | no |
| Response | Type | Required |
| --- | --- | --- |
| `accountId` | string (uuid) | yes |
| `email` | string | yes |
| `business` | object: `id`, `name`, `publishableKey`, `telegramChatId` | yes |
| Status | Meaning |
| --- | --- |
| `201` | Email is not configured on this server: the account and its business summary; sets the session cookie. |
| `202` | The confirmation link is on its way. No account exists until it is confirmed. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `409` | An account with this email already exists; `field` is `email`. No email is sent. |
| `429` | Registration limit exceeded: 8 per minute per IP or 3 per hour per email by default. |
#### `POST /api/auth/register/confirm`
Takes the token from the confirmation link (the part after `#verify=`). Creates the account and its first business and signs this client in.
| Body | Type | Required |
| --- | --- | --- |
| `token` | string | yes |
| Response | Type | Required |
| --- | --- | --- |
| `accountId` | string (uuid) | yes |
| `email` | string | yes |
| `business` | object: `id`, `name`, `publishableKey`, `telegramChatId` | yes |
| Status | Meaning |
| --- | --- |
| `201` | The account and its business summary; sets the session cookie. |
| `400` | The link is invalid, used or expired. |
| `409` | An account with this email already exists; `field` is `email`. |
| `429` | Login limit exceeded: 8 per minute per IP by default. |
#### `POST /api/auth/login`
Sets the session cookie.
| Body | Type | Required |
| --- | --- | --- |
| `email` | string (email) | yes |
| `password` | string | yes |
| Response | Type | Required |
| --- | --- | --- |
| `accountId` | string (uuid) | yes |
| `email` | string | yes |
| `business` | object: `id`, `name`, `publishableKey`, `telegramChatId` | yes |
| Status | Meaning |
| --- | --- |
| `200` | The account and its business summary; sets the session cookie. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `401` | Wrong email or password. |
| `429` | Login limit exceeded: 8 per minute per IP or 5 per hour per email by default. |
#### `POST /api/auth/logout`
Invalidates the session and clears the cookie.
| Status | Meaning |
| --- | --- |
| `204` | Signed out. |
| `401` | No owner session. |
#### `GET /api/auth/me`
Account id, email and the business summary, including its publishable key. Session only; with a machine token use `GET /api/businesses/current`.
| Response | Type | Required |
| --- | --- | --- |
| `accountId` | string (uuid) | yes |
| `email` | string | yes |
| `business` | object: `id`, `name`, `publishableKey`, `telegramChatId` | yes |
| Status | Meaning |
| --- | --- |
| `200` | Account and business summary. |
| `401` | No owner session. |
#### `DELETE /api/auth/account`
Deletes the account at once with its business, configs, conversations, requests and calendar connection; Google access to the calendar is revoked first. The machine token stops working and the widget stops showing. Only a signed-in owner can do it: the machine token cannot.
| Body | Type | Required |
| --- | --- | --- |
| `email` | string | yes |
| Status | Meaning |
| --- | --- |
| `204` | Deleted; clears the session cookie. |
| `400` | The email does not match the account, or the body fails its constraints. |
| `401` | No owner session. The machine token is not accepted here. |
#### `POST /api/auth/password-reset`
If an account uses this email, a link to set a new password is sent to it. The response is the same whether or not the account exists. The link works once, for 30 minutes.
| Body | Type | Required |
| --- | --- | --- |
| `email` | string (email) | yes |
| Status | Meaning |
| --- | --- |
| `202` | Accepted. Sent or not, the response does not say. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `429` | Reset limit exceeded: 5 per minute per IP or 3 per hour per email by default. |
| `503` | Email is not configured on this server. |
#### `POST /api/auth/password-reset/confirm`
Takes the token from the reset link (the part after `#reset=`). Sets the password, signs out every other session of the account and signs this client in. The machine token does not change.
| Body | Type | Required |
| --- | --- | --- |
| `token` | string | yes |
| `password` | string | yes |
| Response | Type | Required |
| --- | --- | --- |
| `accountId` | string (uuid) | yes |
| `email` | string | yes |
| `business` | object: `id`, `name`, `publishableKey`, `telegramChatId` | yes |
| Status | Meaning |
| --- | --- |
| `200` | The account and its business summary; sets the session cookie. |
| `400` | The password fails its constraints, or the link is invalid, used or expired. |
| `429` | Login limit exceeded: 8 per minute per IP by default. |
## Owner endpoints
| Method | Path | What it does | Auth |
| --- | --- | --- | --- |
| `POST` | `/api/businesses/{businessId}/configs` | Create a draft config | X-Owner-Token or owner session |
| `POST` | `/api/businesses/{businessId}/configs/generate` | Generate a draft from a description | X-Owner-Token or owner session |
| `POST` | `/api/businesses/{businessId}/configs/draft` | Get the editable draft | X-Owner-Token or owner session |
| `GET` | `/api/configs/{configId}` | Read a config | X-Owner-Token or owner session |
| `PUT` | `/api/configs/{configId}` | Replace a draft's document | X-Owner-Token or owner session |
| `POST` | `/api/configs/{configId}/publish` | Publish a draft | X-Owner-Token or owner session |
| `POST` | `/api/businesses/{businessId}/availability-preview` | Preview free slots for an unsaved config | X-Owner-Token or owner session |
| `GET` | `/api/businesses/{businessId}/submissions` | List the requests of a business | X-Owner-Token or owner session |
| `PATCH` | `/api/submissions/{submissionId}/status` | Confirm or decline a request | X-Owner-Token or owner session |
| `PUT` | `/api/businesses/{businessId}/telegram` | Set the Telegram chat for notifications | X-Owner-Token or owner session |
| `GET` | `/api/businesses/current` | Identify the business behind a credential | X-Owner-Token or owner session |
| `GET` | `/api/businesses/{businessId}/plan` | Plan and usage of a business | X-Owner-Token or owner session |
| `GET` | `/api/businesses/{businessId}/owner-token` | Reveal the machine token | owner session only |
| `POST` | `/api/businesses/{businessId}/owner-token/rotate` | Issue a new machine token | owner session only |
#### `POST /api/businesses/{businessId}/configs`
Always creates a new draft with the next version; earlier drafts stay. Validated before it is stored; an invalid document is rejected with 422.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
Body: A process config. Its schema is served at `GET /api/public/config-schema`.
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `businessId` | string (uuid) | yes |
| `version` | integer | yes |
| `status` | one of `draft`, `published` | yes |
| `config` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `201` | The stored draft: id, version, config, status. |
| `400` | The body is not JSON, or a required parameter is missing. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Absent, or not yours. |
| `422` | The config fails the schema or flow-lint; `errors` lists each failure with `path`, `message` and `rule`. |
```bash
curl -X POST https://console.mindthefront.com/api/businesses/$BUSINESS/configs \
-H "X-Owner-Token: $TOKEN" -H 'Content-Type: application/json' --data @config.json
```
#### `POST /api/businesses/{businessId}/configs/generate`
The model assembles a config, the server stamps the time zone onto appointment fields, validates and re-prompts on errors up to the attempt limit. Without `timeZone` the connected calendar's zone is used; with neither the answer is 422.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Body | Type | Required |
| --- | --- | --- |
| `description` | string | yes |
| `timeZone` | string | no |
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `businessId` | string (uuid) | yes |
| `version` | integer | yes |
| `status` | one of `draft`, `published` | yes |
| `config` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `201` | The stored draft. |
| `400` | The body is not JSON, or a required parameter is missing. |
| `401` | No session and no owner token, or an unknown token. |
| `402` | Config generations of the plan are used up. Write the config yourself and save it with `createDraft`: that spends no generations. |
| `404` | Absent, or not yours. |
| `422` | No valid config after the attempt limit, or an appointment field with no time zone to take. |
| `429` | Generation limit for this business exceeded: 10 per hour by default. |
| `503` | Generation is not configured on this deployment. |
#### `POST /api/businesses/{businessId}/configs/draft`
Returns the existing draft, or forks the published config into a new one. 404 when the business has neither.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `businessId` | string (uuid) | yes |
| `version` | integer | yes |
| `status` | one of `draft`, `published` | yes |
| `config` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The draft to edit. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Neither a draft nor a published config. |
#### `GET /api/configs/{configId}`
Any config of your business, draft or published.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `configId` | path | yes | Config id. |
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `businessId` | string (uuid) | yes |
| `version` | integer | yes |
| `status` | one of `draft`, `published` | yes |
| `config` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The config. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Not yours, or absent. |
#### `PUT /api/configs/{configId}`
Validated before it is stored, exactly as on creation. Published configs are not editable in place.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `configId` | path | yes | Draft id. |
Body: A process config. Its schema is served at `GET /api/public/config-schema`.
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `businessId` | string (uuid) | yes |
| `version` | integer | yes |
| `status` | one of `draft`, `published` | yes |
| `config` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The updated draft. |
| `400` | The body is not JSON, or a required parameter is missing. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Not yours, or absent. |
| `409` | A published config cannot be edited; create a new draft. |
| `422` | The config fails the schema or flow-lint; `errors` lists each failure with `path`, `message` and `rule`. |
#### `POST /api/configs/{configId}/publish`
Makes the draft the config served to every widget with this business's key and invalidates the cached copy.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `configId` | path | yes | Draft id. |
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `businessId` | string (uuid) | yes |
| `version` | integer | yes |
| `status` | one of `draft`, `published` | yes |
| `config` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The config, now published. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Not yours, or absent. |
```bash
curl -X POST https://console.mindthefront.com/api/configs/$CONFIG/publish -H "X-Owner-Token: $TOKEN"
```
#### `POST /api/businesses/{businessId}/availability-preview`
Slots for a config sent in the body, before it is saved. Same engine and same calendar as the public endpoint.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| `field` | query | yes | Name of the appointment field in the submitted config. |
Body: A process config. Its schema is served at `GET /api/public/config-schema`.
| Response | Type | Required |
| --- | --- | --- |
| `slots` | array of string (date-time) | yes |
| `timeZone` | string or null | yes |
| `durationMinutes` | integer | yes |
| `sourceUnavailable` | boolean | yes |
| Status | Meaning |
| --- | --- |
| `200` | `slots`, `timeZone`, `durationMinutes`, `sourceUnavailable`. |
| `400` | The body is not JSON, or a required parameter is missing. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | No such field in the submitted config. |
#### `GET /api/businesses/{businessId}/submissions`
Newest first, with values and statuses.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response, array of objects | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `status` | one of `new`, `confirmed`, `rejected` | yes |
| `fields` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| `erasedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The requests. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Absent, or not yours. |
#### `PATCH /api/submissions/{submissionId}/status`
The visitor sees the new status on the page they came back to.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `submissionId` | path | yes | Request id. |
| Body | Type | Required |
| --- | --- | --- |
| `status` | one of `new`, `confirmed`, `rejected` | yes |
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `status` | one of `new`, `confirmed`, `rejected` | yes |
| `fields` | object | yes |
| `createdAt` | string (date-time) | yes |
| `updatedAt` | string (date-time) or null | yes |
| `erasedAt` | string (date-time) or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The request with its new status. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Not yours, or absent. |
| `422` | Unknown status. |
#### `PUT /api/businesses/{businessId}/telegram`
`chatId: null` stops notifications. The notification chat lives here, not in the config's `integrations`.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Body | Type | Required |
| --- | --- | --- |
| `chatId` | string or null | no |
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `name` | string | yes |
| `publishableKey` | string | yes |
| `telegramChatId` | string or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | The business summary. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Absent, or not yours. |
#### `GET /api/businesses/current`
What the machine token manages: business id, name and publishable key. The response carries no token and no account credentials.
| Response | Type | Required |
| --- | --- | --- |
| `id` | string (uuid) | yes |
| `name` | string | yes |
| `publishableKey` | string | yes |
| `telegramChatId` | string or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | Business id, name and publishable key. |
| `401` | No session and no owner token, or an unknown token. |
```bash
curl https://console.mindthefront.com/api/businesses/current -H "X-Owner-Token: $TOKEN"
```
#### `GET /api/businesses/{businessId}/plan`
The plan in force, its dates, usage in the current windows against the limits, prepaid requests and whether the widget shows — with the reason when it does not.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response | Type | Required |
| --- | --- | --- |
| `plan` | one of `free`, `basic`, `premium`, `internal` | yes |
| `trial` | boolean | yes |
| `assignedPlan` | one of `free`, `basic`, `premium`, `internal` | yes |
| `paidUntil` | string (date-time) or null | yes |
| `trialEndsAt` | string (date-time) or null | yes |
| `submissions` | object: `used`, `limit`, `window` | yes |
| `packBalance` | integer | yes |
| `generations` | object: `used`, `limit`, `window` | yes |
| `widget` | object: `available`, `reason` | yes |
| Status | Meaning |
| --- | --- |
| `200` | The plan and usage. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Absent, or not yours. |
#### `GET /api/businesses/{businessId}/owner-token`
Session only; a request authenticated with the token itself gets 401.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response | Type | Required |
| --- | --- | --- |
| `ownerToken` | string | yes |
| Status | Meaning |
| --- | --- |
| `200` | `{ "ownerToken": "ot_..." }` |
| `401` | No owner session. |
| `404` | Not your business. |
#### `POST /api/businesses/{businessId}/owner-token/rotate`
The previous token stops working immediately. The publishable key and the published config are unchanged.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response | Type | Required |
| --- | --- | --- |
| `ownerToken` | string | yes |
| Status | Meaning |
| --- | --- |
| `200` | `{ "ownerToken": "ot_..." }` — the new token. |
| `401` | No owner session. |
| `404` | Not your business. |
## Calendar
Connecting a calendar needs a browser once: OAuth consent cannot be automated. Everything after that is machine-callable.
| Method | Path | What it does | Auth |
| --- | --- | --- | --- |
| `GET` | `/api/businesses/{businessId}/calendar` | Read the calendar connection | X-Owner-Token or owner session |
| `POST` | `/api/businesses/{businessId}/calendar/authorize` | Start connecting a calendar | X-Owner-Token or owner session |
| `GET` | `/api/businesses/{businessId}/calendar/calendars` | List the calendars of the connected account | X-Owner-Token or owner session |
| `POST` | `/api/businesses/{businessId}/calendar/calendar` | Choose the calendar to read busy time from | X-Owner-Token or owner session |
| `POST` | `/api/businesses/{businessId}/calendar/disconnect` | Disconnect the calendar | X-Owner-Token or owner session |
#### `GET /api/businesses/{businessId}/calendar`
Whether a calendar is connected, which one, its time zone, and whether the connection needs re-authorising.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response | Type | Required |
| --- | --- | --- |
| `configured` | boolean | yes |
| `connected` | boolean | yes |
| `status` | string or null | yes |
| `calendarId` | string or null | yes |
| `calendarSummary` | string or null | yes |
| `timeZone` | string or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | Connection state. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Absent, or not yours. |
#### `POST /api/businesses/{businessId}/calendar/authorize`
Returns the consent URL; a person opens it in a browser.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response | Type | Required |
| --- | --- | --- |
| `authorizationUrl` | string (uri) | yes |
| Status | Meaning |
| --- | --- |
| `200` | `{ "authorizationUrl": "..." }` |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Absent, or not yours. |
| `503` | Calendar integration is not configured on this deployment. |
#### `GET /api/businesses/{businessId}/calendar/calendars`
What the owner picks from after consent.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Response, array of objects | Type | Required |
| --- | --- | --- |
| `id` | string | yes |
| `summary` | string | yes |
| `timeZone` | string or null | yes |
| `primary` | boolean | yes |
| Status | Meaning |
| --- | --- |
| `200` | Calendars with ids, names and time zones. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | No connection to list from. |
#### `POST /api/businesses/{businessId}/calendar/calendar`
Its time zone is used by generation when the request names none.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Body | Type | Required |
| --- | --- | --- |
| `calendarId` | string | yes |
| Response | Type | Required |
| --- | --- | --- |
| `configured` | boolean | yes |
| `connected` | boolean | yes |
| `status` | string or null | yes |
| `calendarId` | string or null | yes |
| `calendarSummary` | string or null | yes |
| `timeZone` | string or null | yes |
| Status | Meaning |
| --- | --- |
| `200` | Connection state after the choice. |
| `400` | A required header, parameter or field is missing or fails its constraints. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | No such calendar in the connected account. |
#### `POST /api/businesses/{businessId}/calendar/disconnect`
Busy time stops narrowing the schedule; slots keep being offered from the declared hours with the usual caveat.
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `businessId` | path | yes | Your business id. |
| Status | Meaning |
| --- | --- |
| `204` | Disconnected. |
| `401` | No session and no owner token, or an unknown token. |
| `404` | Absent, or not yours. |
---
# For AI agents
> Put a working widget on a site you are building. Everything in one fetch: [`/llms-full.txt`](/llms-full.txt).
| Step | Endpoint | Credential |
| --- | --- | --- |
| 1. Read the contract | `GET /api/public/config-schema` | none |
| 2. Assemble the config | — | none |
| 3. Validate, repeat until valid | `POST /api/public/configs/validate` | none |
| 4. Find the business, create a draft | `GET /api/businesses/current`, `POST /api/businesses/{id}/configs` | `X-Owner-Token` |
| 5. Publish | `POST /api/configs/{id}/publish` | `X-Owner-Token` |
| 6. Embed | — | `publishableKey` |
Public endpoints are on `https://mindthefront.com`, owner endpoints on `https://console.mindthefront.com`.
## 1–3. Build and validate without an account
```bash
curl https://mindthefront.com/api/public/config-schema -o schema.json
curl -X POST https://mindthefront.com/api/public/configs/validate \
-H "Content-Type: application/json" --data @config.json
```
```json
{ "valid": false, "errors": [
{ "path": "/flow/steps/confirm", "rule": "flow-lint",
"message": "Field \"phone\" is required but is not filled on every path to the submit step \"confirm\"" }
] }
```
| `rule` | What is wrong |
| --- | --- |
| `schema` | the shape of the document |
| `flow-lint` | links between steps and fields |
`path` is a JSON Pointer to the node that breaks the rule: `/flow/steps/confirm`, `/submission/properties/appointment`. Status is `200` in both outcomes; `valid` is the verdict. Nothing is stored. Every rule and its message: [writing a config](/docs/config#rules-a-stored-config-obeys). Declaration-to-input mapping: [widget catalogue](/docs/widgets).
## 4–5. Publish with the owner's token
The owner copies the token from the console — their business page, "Connect an AI agent". An owner without an account signs up at `https://console.mindthefront.com/#agent`, confirms their email from the link we send, and lands on that screen. Don't create the account yourself: the account appears only once the owner confirms the email. The token is a secret: keep it out of the repository, out of committed files and out of anything you print back.
```bash
BUSINESS=$(curl -s https://console.mindthefront.com/api/businesses/current -H "X-Owner-Token: $TOKEN")
ID=$(echo "$BUSINESS" | jq -r .id)
KEY=$(echo "$BUSINESS" | jq -r .publishableKey)
CONFIG=$(curl -sX POST https://console.mindthefront.com/api/businesses/$ID/configs \
-H "X-Owner-Token: $TOKEN" -H 'Content-Type: application/json' \
--data @config.json | jq -r .id)
curl -X POST https://console.mindthefront.com/api/configs/$CONFIG/publish -H "X-Owner-Token: $TOKEN"
```
"Issue a new one" in the console replaces the token: the previous one gets `401` at once, the embedded widget keeps running on the same publishable key.
## 6. Embed
Put the [snippet](/docs/index) on the page with `$KEY` as `publishableKey`.
## Constraints to design around
| Constraint | Consequence for you |
| --- | --- |
| No model call at runtime | anything the visitor is asked must exist as a step in the config |
| `timeZone` is required on an appointment field | the IANA zone where the business operates; ask the owner when the location does not settle it |
| Regeneration in the console rewrites `flow` | key anything you build on submission field names, not step ids |
| `additionalProperties` must be `false` | the server applies your schema exactly; it adds nothing |
| The plan limits requests, and the widget gets `402` and does not show once they run out | read `GET /api/businesses/$ID/plan`: `widget.reason` says why; tell the owner instead of republishing |
| The plan limits config generations in the console | writing the config yourself, as on this page, spends none |