Skip to main content
Three anonymous, cache-friendly GET endpoints under /api/public/v1 let the widget — or any front-end code on your pages — read published per-tenant data before, or entirely without, opening a chat session:
MethodPathReturns
GET/api/public/v1/chat/quick-questionsThe tenant’s quick-question prompts, filterable to several page types in one request.
GET/api/public/v1/config/{section}The resolved values of every publicly readable field of one configuration section.
GET/api/public/v1/forms/{codeOrId}/schemaThe render-safe schema of one live, embeddable form — fields, labels, validation hints, submit label.
All three endpoints share one contract — the same key authentication, the same origin and rate-limit rules, and the same ETag/X-Edge-Source conventions — so read Shared behavior once and it applies to each. The one deliberate difference: quick questions and config are shared-cacheable, the form schema is privately cacheable only — see Caching & revalidation.

Shared behavior

Authentication — the publishable key

Each request is authorized by the same publishable widget key (pk_live_…) you pass to POST /chat/sessions — send it either as the publicKey query parameter or as an X-Comerix-Public-Key header:
GET /api/public/v1/config/widget?publicKey=pk_live_… HTTP/1.1
GET /api/public/v1/config/widget HTTP/1.1
X-Comerix-Public-Key: pk_live_…
The key selects the tenant: the tenant is taken exclusively from the key’s binding, never from request input, so one tenant’s key can never address another tenant’s data. A missing key returns 400 invalid_input; an unknown, paused, or revoked key returns 403 widget_disabled.
There is no session hereNo session token, no Authorization header, no cookies. That’s what makes the responses safely cacheable by browsers — and, for quick questions and config, by shared caches too — the full URL (key included) is the cache key.

Origins & rate limits

The same edge rules as the rest of /api/public/v1 apply:
  • Origin allow-list (CORS) — a browser request from an origin outside the key’s allowedOrigins is rejected with 403 origin_not_allowed and no CORS headers. Manage origins on the Public keys page.
  • Rate limits — two independent sliding-window budgets, shared across the whole public surface: 60 requests/min per source IP and 600 requests/min per public key (across all IPs). Above either budget the call returns 429 rate_limited with a Retry-After header — see Errors → Rate-limit responses.
With client caching in play a normal page should consume a tiny fraction of those budgets — one revalidation per endpoint per minute at most.

Caching & revalidation

Every 200 response is cacheable and revalidatable, but the scope of the cache differs per endpoint. Quick questions and config are shared-cacheable:
HTTP/1.1 200 OK
Cache-Control: max-age=60, public, s-maxage=300, stale-while-revalidate=300
ETag: "0a1b2c3d4e5f6071"
X-Edge-Source: store
Content-Type: application/json
The form schema is privately cacheable only:
HTTP/1.1 200 OK
Cache-Control: max-age=60, private
ETag: "0a1b2c3d4e5f6071"
X-Edge-Source: store
Content-Type: application/json
HeaderMeaning
Cache-ControlBrowsers may reuse the response for 60 s. For quick questions and config, shared caches (CDN, reverse proxy) may keep it for 300 s and serve it stale for another 300 s while revalidating in the background. The form schema says private instead — no s-maxage — so shared caches stay out entirely.
ETagA fingerprint of the exact body. Send it back as If-None-Match to revalidate cheaply.
X-Edge-Sourcestore — the response came from the published edge document; live — it was computed on the spot. Either way the body shape is identical.
Why is the form schema private-only?The form schema’s outcome varies per presenting key: the key’s form scope decides whether the same form answers 200 or 403 form_not_allowed. A shared cache entry warmed by an authorized key could be served past another key’s scope denial, so shared/CDN caching is deliberately not allowed for this endpoint. Quick questions and config have no such per-key allow/deny split, so they stay shared-cacheable.
A conditional request whose ETag still matches returns 304 Not Modified with an empty body — the round-trip costs headers only:
# First fetch — note the ETag:
curl -i 'https://flow.example.com/api/public/v1/chat/quick-questions?publicKey=pk_live_…'
# HTTP/1.1 200 OK
# ETag: "0a1b2c3d4e5f6071"
# …

# Revalidate with If-None-Match — nothing changed, no body transferred:
curl -i -H 'If-None-Match: "0a1b2c3d4e5f6071"' \
  'https://flow.example.com/api/public/v1/chat/quick-questions?publicKey=pk_live_…'
# HTTP/1.1 304 Not Modified
Browsers do this automatically for fetch() once the response is in the HTTP cache — you don’t have to manage the ETag yourself unless you cache in your own storage.

Where the data comes from

All three endpoints serve published documents: whenever the underlying data changes — an admin saves configuration, a flow or form is published or withdrawn — the platform recomputes the affected document and writes it into a key-value store, so the hot path is a single key lookup. When no published document exists the response is computed live instead; X-Edge-Source tells you which path served you, and the body’s publishedAt is the publication time of the served document (null for live responses). Which backend holds the documents is an operator concern — see Operate → Edge document store.

Quick questions

GET /api/public/v1/chat/quick-questions
Returns the tenant’s pre-written one-tap prompts (configured under Settings → Widget → Quick Questions, up to six) so the host page can render chips on page load — before any session exists, and cached across visitors.
Query parameterRequiredMeaning
publicKeyyes (or the X-Comerix-Public-Key header)The publishable widget key.
pageTypesnoComma-separated page types to include — e.g. pageTypes=general,category fetches the rows for two page contexts in one request. Omit it for all rows. The page types a prompt can carry come from the widget configuration: general, category, product, cart; an unknown value simply matches no rows.
curl 'https://flow.example.com/api/public/v1/chat/quick-questions?publicKey=pk_live_…&pageTypes=general,category'
Response (200)
{
  "quickQuestions": [
    { "question": "Where is my order?", "pageType": "general",
      "flowId": "0193aa…", "intentName": "order_status" },
    { "question": "How do I return an item?", "pageType": "category",
      "flowId": "0193bb…", "intentName": null }
  ],
  "publishedAt": "2026-06-09T08:00:00+00:00"
}
FieldMeaning
questionThe text to show on the chip.
pageTypeThe page context the prompt is meant for. Filtering beyond your pageTypes request is up to the host page — only it knows which page the visitor is on.
flowIdThe flow the prompt is bound to; empty when unbound.
intentNameThe intent to trigger when the visitor taps the chip — or null when the bound flow is unset, not currently published, or outside this key’s intent allow-list. Render such a prompt disabled rather than dropping it.
publishedAtPublication time of the served edge document; null when the response was computed live.
Tapping a chip with a non-null intentName is a normal trigger turn: open a session if you don’t have one, then post { text: question, intentName } to /chat/messages — see Lifecycle → Quick Questions. The rows are the same ones the session response carries in its quickQuestions array; this endpoint just makes them available earlier and cacheable.
Per-key viewintentName is evaluated per presenting key: a prompt bound to an intent the key may not trigger keeps its row but comes back with intentName: null. Two keys can therefore see different intentName values for the same prompt — another reason the key is part of the cache key (the URL).

Public configuration

GET /api/public/v1/config/{section}
Returns the resolved values of every field of {section} that is declared publicly readable — for example GET /config/widget serves the widget’s appearance values (accent color, title, subtitle, welcome text, position) and its quick-question rows, so an embed can paint itself before loading anything heavier.
ParameterRequiredMeaning
section (path)yesThe configuration section id, lowercase ([a-z][a-z0-9_]*) — e.g. widget.
publicKey (query)yes (or the X-Comerix-Public-Key header)The publishable widget key.
curl 'https://flow.example.com/api/public/v1/config/widget?publicKey=pk_live_…'
Response (200)
{
  "section": "widget",
  "values": {
    "widget/appearance/accent": "#4b7bf5",
    "widget/appearance/title": "Chat with us",
    "widget/appearance/subtitle": "Your personal AI Assistant",
    "widget/appearance/welcome": "Hi! How can we help you today?",
    "widget/appearance/position": "right",
    "widget/quick_questions/questions": [ { "question": "…", "pageType": "general", "assignedFlow": "…" } ]
  },
  "publishedAt": null
}
values is keyed by the full field path (<section>/<group>/<field>) and holds the value resolved for the key’s tenant — workspace overrides included. A section that doesn’t exist, or exists but has no publicly readable fields, returns 404 not_found; the two cases are deliberately indistinguishable.
What can ever be publicA field is exposed here only when the module that ships it declares public_readable: true in its configuration schema — there is no admin toggle, and the values are shown to anonymous visitors by design. Secret fields (sensitive or encrypted — SMTP passwords, API keys) can never be public: the platform rejects a schema that tries to combine public_readable with a secret field. Everything else stays admin-only behind comerix.config.<section>.view.

Form schema

GET /api/public/v1/forms/{codeOrId}/schema
Returns the render-safe schema of one live, embeddable form — its fields, labels, validation hints, and submit label — so an embed can (re)render the form without the admin builder. It is the cache-friendly sibling of the plain GET /api/public/v1/forms/{codeOrId} read: identical field shapes, but served from the published edge document when one exists, and it never writes. Which schema read should I use?
GET /forms/{codeOrId} (plain)GET /forms/{codeOrId}/schema (this one)
Usage meteringCounts a “mount” toward the key’s usage each call.Never records usage — sits on the hottest read path and must not write.
CachingUncached.Cache-Control: max-age=60, private + ETag/304.
locale query parameter / response fieldYes — localises labels/help.No — the document is locale-less by design.
publishedAt response fieldNo.Yes — null when computed live.
Use forThe initial, metered mount of an embed.Polling / refreshing the schema from embeds and widgets.
ParameterRequiredMeaning
codeOrId (path)yesThe form’s public code (a slug like contact_us) or its UUID. An uppercase UUID is accepted and canonicalised internally.
publicKey (query)yes (or the X-Comerix-Public-Key header)The publishable widget key. Its form scope must allow the requested form — the same scope rules as the other public form endpoints. A publicKey body field is not read here.
curl 'https://flow.example.com/api/public/v1/forms/contact_us/schema?publicKey=pk_live_…'
Response (200)
{
  "code": "contact_us",
  "title": "Contact us",
  "submitLabel": "Send",
  "fields": [
    {
      "name": "email",
      "type": "email",
      "label": "Email",
      "required": true,
      "placeholder": null,
      "help": null,
      "options": null,
      "config": null,
      "validation": { "format": "email" }
    }
  ],
  "publishedAt": "2026-06-10T00:00:00+00:00"
}
FieldMeaning
codeThe form’s public code.
titleThe form’s display title.
submitLabelThe label for the submit button.
fieldsThe render-safe field list — exactly the same item shape as the plain schema read, including the per-field validation object (server-only rules are withheld but still enforced on submit).
publishedAtPublication time of the served edge document; null when the response was computed live for this request.
Unlike the plain schema read, the response intentionally does not include a locale field. Caching is private-only (max-age=60, private) — see Caching & revalidation for why — and X-Edge-Source: store|live reports where the document came from, same as the other two endpoints. To go from schema to data, submit the form with POST /api/public/v1/forms/{codeOrId}/submissions — the error catalog for the whole forms surface is under Errors → Public Forms API errors.

Errors

The error envelope is the standard public-API shape (error, message, details) — see Errors for the catalog. The codes you can get here:
HTTPerrorWhen
400invalid_inputNo publicKey query parameter and no X-Comerix-Public-Key header.
403widget_disabledThe key doesn’t exist, is paused, or was revoked.
403origin_not_allowedThe browser Origin isn’t in the key’s allow-list.
403form_not_allowedForm schema only: the key’s form scope doesn’t cover the requested form.
404not_foundQuick questions: the document can’t be resolved. Config: unknown section, or a section with no public fields.
404form_not_foundForm schema only: no live, embeddable form by that code/UUID.
429rate_limitedPer-IP or per-key budget exceeded — honor Retry-After.

Where to go next