# 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 |