The JSBOX integration type powers website chat: visitors send messages that appear in your Mailoo dashboard. You can reply from the chat console; visitors see updates over WebSocket or polling.
::: {.contents local=""} :::
Concepts
- Integration type:
JSBOX(created in the dashboard or via the public API with an API key). - Sessions: Each visitor thread is a chat session (opaque
sessionPublicId). The first message creates a session; later messages includesessionPublicId. - API key: Required for server-side calls to Mailoo webhooks (your BFF, backend, or automation). Never put the API key in browser JavaScript.
Public API (X-API-Key)
Restricted keys can include:
webhook.chat-integration-create---POST /api/v1/webhooks/chat/{projectUid}/integrations(create a JSBOX integration).chat.send-message--- chat messages, session history, and chat status under/api/v1/webhooks/chat/....
Create a JSBOX integration
curl -X POST "https://api.mailoo.app/api/v1/webhooks/chat/proj_1/integrations" \\
-H "X-API-Key: YOUR_KEY" \\
-H "Content-Type: application/json" \\
-d '{"name":"Support chat","config":{"allowedOrigins":["https://example.com"]}}'
Send a visitor message (scoped to project + integration):
curl -X POST "https://api.mailoo.app/api/v1/webhooks/chat/{projectUid}/{integrationId}/messages" \\
-H "X-API-Key: YOUR_KEY" \\
-H "Content-Type: application/json" \\
-H "Origin: https://your-site.com" \\
-d '{"content":"Hello","sessionPublicId":"optional-if-continuing"}'
List messages in a session (for server/BFF polling):
GET /api/v1/webhooks/chat/{projectUid}/{integrationId}/sessions/{sessionPublicId}/messages?since=ISO8601
Chat integration status
GET /api/v1/webhooks/chat/{projectUid}/{integrationId}/status
Legacy path (integration id only): POST /api/v1/webhooks/messages/{integrationId} still exists but requires chat.send-message and a JSBOX integration.
Public chat webhooks share the API-key rate limit. When the limit is exceeded, Mailoo responds with HTTP 429. The @mailoo/chat widget classifies that (and gateway/network failures) and shows an unavailable panel instead of raw API text --- the host should pass unavailableAction (see chat-widget-nextjs-example{.interpreted-text role="doc"}).
Dashboard (Bearer JWT)
- Sessions:
GET /api/v1/chat/{integrationId}/sessions--- optionalstatus=OPEN|PENDING|CLOSEDandassignedUserId=<userId>orassignedUserId=unassigned. - History:
GET /api/v1/chat/{integrationId}/sessions/{sessionPublicId}/messages - Owner reply:
POST /api/v1/chat/{integrationId}/sessions/{sessionPublicId}/replywith JSON{"content":"..."}. - Assign / close:
PATCH /api/v1/chat/{integrationId}/sessions/{sessionPublicId}with{"assignedUserId":"<userId>"|null}(project members only) or{"action":"close"}.
Sessions are OPEN, PENDING, or CLOSED. Widget settings on the JSBOX integration (config.chatWidget) drive:
- Autoresponder --- first visitor message on a new session sends
autoresponder.message; optionalexpectedReplyHourssets statusPENDINGuntil an operator replies. - Reply templates ---
replyTemplates(not FORM campaign templates); the console inserts them into the reply box. - Close follow-up --- on
action: close,closeFollowUp.messageand/orcloseFollowUp.reviewUrlare sent to the visitor. Further visitor messages on a closed session return 409. - Optional AI agent ---
config.chatWidget.agent(enabled,name, optionalimageMediaId,kbIntegrationIdfor a Blog in the same project, plus markdowninstructions,kbDigestPrompt,kbArticlePrompt, andkbFiles). When enabled, the agent appears first in the widget team roster (synthetic operator idjsbox_chat_agent). Mailoo API prependsinstructionsand fills the digest/article templates (placeholders{{kb_digest}},{{visitor_message}},{{article_title}},{{article_slug}},{{article_body}}) then calls Wednesday in the background after inbound with a KB digest (published titles + slugs only); the agent may request one article body viaKB_REQUEST:<slug>. Knowledge-base files are stored on S3 and listed on the agent; they are not injected into the model yet. The widget never talks to Wednesday. Agent replies are OUTBOUND withmetadata.source: jsbox_chat_agentandsenderNamefrom the agent display name. Assigning the session to an operator stops the agent. The agent may also hand off to a human viaHANDOFF_TO_HUMAN:(visitor-facing line plus operator brief); Mailoo setshandedOffAt, statusPENDING, stores the brief as OUTBOUNDmetadata.source: jsbox_chat_agent_handoff(dashboard only), and skips further agent turns. Requires APIWEDNESDAY_BASE_URL(dev:http://127.0.0.1:4080) when enabled. - MCP / agent settings ---
GET/PATCH /api/v1/chat/{projectUid}/integrations/{integrationId}/settings(scopechat.manageor MCP token) reads/updates the same sanitized widget config and returns lifetime counts for messages and sessions (no session list or message bodies). MCP tool:manage_jsbox_integration_settings(seemailoo-mcp{.interpreted-text role="doc"}). Agent skill (minimum rules):configure-chat-mcp{.interpreted-text role="doc"}.
The web app proxies these through its BFF under the same paths (with session auth).
WebSocket
On a Next.js host using @mailoo/chat, the browser WebSocket origin is not a NEXT_PUBLIC_* build-time variable: the server resolves it at runtime (MAILOO_CHAT_WS_ORIGIN, else chat/API base env --- see getMailooChatWebSocketOrigin).
- Owner (dashboard):
/api/v1/chat/ws?integrationId=...&token=<JWT>--- use the same access token asAuthorization: Bearer. - Visitor:
/api/v1/chat/visitor-ws?projectUid=...&integrationId=...&sessionPublicId=...--- no API key; the session must already exist (created by posting a message). Prefer exposing onlysessionPublicIdto the browser and validating on your BFF when possible.
For architecture details, event payloads, reverse proxy (wss), and multi-replica caveats, see chat-websocket-production{.interpreted-text role="doc"}.
Dashboard (integration detail)
On the JSBOX integration page, project owners use tabs:
- Widget --- brand colours, welcome copy, privacy URL, launcher position (four corners plus middle-left / middle-right; default bottom-right), and topic chips.
- Replies & automation --- operator visibility, first-message autoresponder, reply templates, and close-session follow-up.
- AI Agent --- owner-only settings for
config.chatWidget.agent: enable toggle, display name, optional avatar, Blog KB, markdown instruction windows, and knowledge-base file upload (S3). Editors can open the tab; only the owner can save. When enabled, the agent is listed in the widget team and in Setup → Operators (not toggleable there). The mailoo.app floating chat (MailooSiteChatShell/MAILOO_CHAT_*) is this same JSBOX; dev seed enables the agent onmailoo-websitewith name, Blog KB, and default prompt templates (requires Wednesday +WEDNESDAY_BASE_URL). - Connection & settings --- images library, API keys, server env snippet, and delete. Raw stored
configis available behind View configuration JSON (pretty-printed reference dialog), not as an always-visible block.
There is no Products/content tab for JSBOX --- status, message count, and last activity stay in the page header. Editors see those header stats, Connection & settings, and the AI Agent tab (toggle disabled). Widget/replies editors remain owner-only.
CORS and allowedOrigins
Browser calls to Mailoo webhooks may send an Origin header. Optionally configure allowed origins on the JSBOX integration. If the list is empty, Origin is not checked (API key still required). Legacy domain in stored config remains a CORS fallback for older rows but is no longer collected in the dashboard.
Launcher position is stored as config.chatWidget.position and returned on the public widget-config endpoint. The default host path (resolveMailooChatHostConfig + MailooSiteChatShell) always applies it via widgetConfig. An explicit position prop on MailooSiteChatWidget still overrides the stored value when set.
See also
chat-widget-nextjs-example{.interpreted-text role="doc"} --- Next.js BFF pattern and env variableschat-websocket-production{.interpreted-text role="doc"} --- WebSocket behaviour and production notes- OpenAPI:
https://api.mailoo.app/docs/v1