Skip to content

API Reference

Mohammad-Hasan-Kaman edited this page Oct 20, 2026 · 2 revisions

API Reference

The widget ships a small, deliberately narrow HTTP API. Three chat endpoints carry the whole conversation; everything else is either a page (the widget itself, the landing page, a per-client demo) or the admin panel, which is server-rendered HTML.

All chat endpoints live under the prefix /api/chat and are mounted from app/routers/chat.py.


Authentication

The chat API authenticates with a tenant API key, not with a user account.

POST /api/chat/session
Content-Type: application/json

{
  "api_key": "your-tenant-api-key",
  "token": null
}

The key identifies the clinic. It selects:

  • the flow the visitor walks through,
  • the welcome message,
  • the announcement banner (if one is active),
  • the counselor roster used for matching,
  • the allowed domains (optional).

Every key belongs to exactly one tenant. A key that does not exist returns 401.

Domain restriction

If the tenant has allowed_domains configured, the server checks the Origin header (falling back to Referer) against that list. The match is on the exact host or one of its subdomains — example.com matches www.example.com and app.example.com, but not notexample.com and not example.com.evil.net.

  • No Origin header (for example a server-to-server call) → the check is skipped.
  • A host outside the list → 403 with "دامنه مجاز نیست.".

Endpoints

POST /api/chat/session

Creates a new session, or restores an existing one.

Field Type Required Notes
api_key string yes Max 128 characters. Must resolve to an active tenant.
token string | null no An existing session token. If it is valid and belongs to the same tenant, the session is restored.

New session response (200):

{
  "token": "3f8a…",
  "state": 0,
  "messages": [
    { "role": "bot", "text": "سلام! خوش آمدید.", "html": "<p>سلام! خوش آمدید.</p>" }
  ],
  "quick_replies": [],
  "announcement": null
}

Restored session response — same shape, but messages contains the single prompt that matches the current state, so the client can re-render mid-conversation after a page reload:

{
  "token": "3f8a…",
  "state": 4,
  "messages": [ { "role": "bot", "text": "شماره تماس خود را وارد کنید…", "html": "…" } ],
  "quick_replies": [],
  "input_type": "phone",
  "announcement": "ساعت کاری مرکز تغییر کرده است."
}

The announcement field is the active broadcast for the tenant, or null.

POST /api/chat/message

Sends one visitor message and advances the flow.

Field Type Required Notes
token string yes Max 64 characters. The session token from /session.
text string yes Max MAX_MESSAGE_LENGTH + 500 characters.

Response (200):

{
  "token": "3f8a…",
  "state": 5,
  "reply": {
    "text": "لطفاً نام و نام خانوادگی خود را بنویسید.",
    "html": "<p>لطفاً نام و نام خانوادگی خود را بنویسید.</p>",
    "quick_replies": ["بله", "خیر"],
    "input_type": "text"
  },
  "done": false
}
Field Meaning
state The new state. 0…N is a flow step, -1 is the GHQ screen, -2 means the session is finished, -3 is the GHQ opt-in question.
reply.text Plain text, Markdown source.
reply.html The same message rendered to HTML by md_to_html().
reply.quick_replies Chips to render under the message. Empty for free-text prompts.
reply.input_type text, phone, or number — tells the client which keyboard to show and which client-side validation to apply.
done true once the flow has reached an ending.

Errors:

Status When Body
401 Unknown or invalid API key. {"detail": "کلید API نامعتبر است."}
403 The tenant's allowed_domains list rejects the request's host. {"detail": "دامنه مجاز نیست."}
403 The tenant has been disabled. {"detail": "سرویس این مرکز موقتاً غیرفعال است."}
404 The token does not exist, or the session expired. {"detail": "session not found or expired"}
429 Rate limit exceeded. {"detail": "درخواست بیش از حد مجاز. لطفاً کمی صبر کنید."}

GET /api/chat/health

{ "status": "ok" }

No authentication. Useful as a liveness probe behind a reverse proxy.


Rate limiting

Both write endpoints are rate limited, 30 requests per 60 seconds, by an in-process sliding window. The key is derived from the client IP:

Endpoint Rate-limit key
POST /api/chat/session {ip}:session
POST /api/chat/message {ip}:{token[:12]}

Consequences worth knowing:

  • The limit is per process. Behind multiple workers or replicas, the effective limit is multiplied by the number of workers, because the counter is a module-level dict[str, deque] and not shared.
  • The message key is the first 12 characters of the token, so two different long tokens that share a prefix would share a bucket.
  • Stale keys are garbage-collected once per window, and the map is capped at 10,000 keys — the oldest key is evicted when the cap is hit.

A 429 is returned before any work is done, so the message is not consumed: the visitor can simply resend it.


Page routes

These return HTML, not JSON, and are not part of the chat API.

Route What it does
GET / The landing page (static/landing.html).
GET /widget?key={api_key} The embeddable widget shell (static/widget.html). __TENANT_NAME__ is replaced with the tenant's name, or "سامانه پذیرش" when the key is missing or unknown.
GET /demo/{api_key} A per-client demo page (static/demo.html). The key is baked into the page as __API_KEY__, and the demo is recorded like a real conversation under that tenant. An unknown key returns a 404 page in Persian.
GET /static/* The widget's static assets, served with Cache-Control: no-cache, max-age=300.

Embedding the widget

The widget is served from /widget?key=… and framed by the host page:

<iframe
  src="https://your-host/widget?key=YOUR_TENANT_API_KEY"
  style="position:fixed; bottom:0; left:0; width:400px; height:640px; border:0; z-index:9999;"
></iframe>

Because the widget lives in an iframe, the server sends X-Frame-Options: SAMEORIGIN for every route except /widget. See Architecture for the full header and CORS story.


The client side

static/widget.js is the loader that a client site includes. It:

  1. reads the API key from the <script> tag's data-key attribute (or the ?key= query string),
  2. injects a floating bubble labelled "ستیار انتخاب مشاور",
  3. lazily loads static/widget-app.js when the visitor opens the bubble,
  4. and the app script drives the session loop: POST /session → render the first prompt → POST /message per visitor input → render reply.html and reply.quick_replies.

The same ?key= also selects the per-client demo, so a clinic can share one link with a prospect and have that prospect's conversation recorded under their own key.


Curl examples

# 1. Start a session
curl -X POST https://your-host/api/chat/session \
  -H 'Content-Type: application/json' \
  -d '{"api_key":"YOUR_TENANT_API_KEY","token":null}'

# 2. Send an answer (use the token from step 1)
curl -X POST https://your-host/api/chat/message \
  -H 'Content-Type: application/json' \
  -d '{"token":"3f8a…","text":"مینا رضایی"}'

# 3. Liveness
curl https://your-host/api/chat/health

Related pages

  • Architecture — how the layers fit together, and where each of these endpoints lives.
  • Admin Panel — the server-rendered side: inbox, leads, flow editor.
  • Multi-Tenancy — how a key maps to a clinic, and what is isolated.
  • Embedding — the loader contract, the iframe, and the client-side loop.
  • Home — the wiki index.

Clone this wiki locally