Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Platform AI Widget API
description: 'Ethora Platform API.
## Getting Started
- For new backend and tenant integrations, start with the `/v2/...` endpoints.
- Use `/v2/apps/{appId}/...` when your backend/admin flow manages a specific target app explicitly.
- Some legacy operations still exist only under `/v1/...`; when so, the operation description will say so.
## Integration Tracks
- `Frontend / app runtime`: app-auth login/sign-up flows, then user-auth chats/files/sharelinks.
- `Tenant admin / B2B backend`: explicit app management and app-scoped admin flows under `/v2/apps/{appId}/...`.
## Auth Token Legend
- `App JWT`: app-scoped runtime token for sign-up/login/runtime app operations.
- `User JWT`: end-user or admin-user session token.
- `Refresh JWT`: refresh-only token for access token renewal.
- `Client JWT`: legacy client-side token used only for chat/client credential exchange compatibility flows.
- `B2B Server JWT`: tenant/backend integration token. Prefer `Authorization: Bearer `; `x-custom-token` is still accepted for backward compatibility.
## Which Token Where
- `App JWT`: `/v1/users/login-with-email`, `/v2/users/sign-up-with-email`, similar app-runtime auth endpoints.
- `User JWT`: user session endpoints like chats, files, sharelinks, profile/session management.
- `B2B Server JWT`: tenant/backend integration endpoints, especially explicit `/v2/apps/{appId}/...` admin flows.
- `Client JWT`: only where the endpoint explicitly says `Client JWT` and typically only for legacy compatibility.
## Auth Approach
- `/v2/...` endpoints are the canonical API surface for new integrations.
- Endpoints may support one or more token types depending on the actor and scope.
- App-scoped admin/integration endpoints use `/v2/apps/{appId}/...` so the target app is always explicit.
- When an endpoint supports both `User JWT` and `B2B Server JWT`, Swagger will show both under Security.
- Public Swagger excludes internal-only, operational, and experimental surfaces.'
contact:
name: Dappros developers team
version: 26.11.05
servers:
- url: https://api.chat.ethora.com
security:
- JwtAuth: []
tags:
- name: AI Widget
description: 'AI Widget integration endpoints.
ℹ️ The widget is the embeddable chat experience served by `assistant.js`; visitors are anonymous (XMPP-only Mongo Users tagged `isVisitor:true`), conversations are persistent MUC rooms tagged `chats.type=''widget''`, and the AI bot is the App''s configured agent.'
paths:
/v2/widget/sessions:
post:
tags:
- AI Widget
summary: Provision an AI Widget visitor session
description: 'Public, unauthenticated endpoint called by the embedded `ethora-ai-chat-widget`
on mount. Mints (or resumes) an anonymous visitor user in ejabberd, creates a
persistent MUC room, pre-affiliates the visitor and the App''s AI bot as members,
and returns the credentials needed for the widget to SASL-bind and join.
Visitor JIDs follow the convention `${appId}_widget-` so mod_ethora''s
per-app prefix guard accepts them as members of the room. Rooms follow
`${appId}_` and are tagged in the platform DB as `chats.type=''widget''`.
The endpoint is rate-limited per IP (30/minute) since it''s reachable from any
browser without auth. It validates that the App exists and has an AI bot
configured before provisioning anything.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- appId
properties:
appId:
type: string
description: The Ethora App _id whose widget the visitor is opening.
resumeXmppUsername:
type: string
description: 'Optional resume hint. When supplied, the server reuses the existing
visitor identity instead of minting a new one. The widget reads this
from localStorage so a returning browser keeps its identity (and its
conversation history is reachable to operators by visitor _id).
The value MUST start with `${appId}_widget-` or the request 422s.'
responses:
'200':
description: "```\n{\n visitor: {\n xmppUsername: string, // bare localpart, e.g. \"${appId}_widget-<uuid>\"\n xmppPassword: string,\n uuid: string,\n jid?: string // fully-qualified, \"<localpart>@<host>\"\n },\n room: {\n name: string, // bare room name, e.g. \"${appId}_<roomUuid>\"\n chatId: string, // Mongo _id of the chats row\n jid?: string // fully-qualified, \"<name>@conference.<host>\"\n },\n bot: {\n xmppUsername: string,\n jid?: string\n },\n xmpp?: {\n host: string, // ejabberd host, e.g. \"xmpp.example.com\"\n service: string, // MUC service, e.g. \"conference.xmpp.example.com\"\n wsUrl: string // canonical WS URL, e.g. \"wss://xmpp.example.com/ws\"\n }\n}\n```"
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: 'App not found.
```
{ error: ''App not found'', code: ''APP_NOT_FOUND'' }
```'
'409':
description: 'App has no AI bot configured.
```
{ error: ''App has no AI bot configured'', code: ''AI_BOT_NOT_CONFIGURED'' }
```'
'422':
description: 'Validation error (e.g. missing appId, malformed resumeXmppUsername).
```
{ error: string, code: ''VALIDATION_ERROR'' }
```'
'429':
description: 'Rate limit exceeded (default 30 requests / minute / IP).
```
{ error: ''Too many requests'', code: ''RATE_LIMITED'' }
```'
'500':
$ref: '#/components/responses/InternalServerError'
'502':
description: 'Underlying ejabberd registration or room-create call failed.
```
{ error: string, code: ''XMPP_REGISTER_FAILED'' | ''XMPP_ROOM_CREATE_FAILED'' }
```'
operationId: post_v2_widget_sessions
/v2/widget/config:
get:
tags:
- AI Widget
summary: Public AI Widget config for an app
description: 'Public, unauthenticated endpoint the embedded widget calls at load, before it
draws the launcher. Returns the appearance the App''s admins saved with
`PUT /v2/apps/{appId}/widget/appearance`.
An unknown or inactive App returns an empty `appearance` rather than an
error, so the endpoint does not reveal which App ids exist. Everything it
returns is already public on any page that carries the widget snippet.
Responses carry `Cache-Control: public, max-age=60`, so a change can take a
minute to reach visitors. Rate-limited per IP (default 300 requests / minute).'
operationId: v2_widget_config_get
security: []
parameters:
- in: query
name: appId
description: The Ethora App _id (24 hex characters) whose widget is loading.
required: true
schema:
type: string
pattern: ^[0-9a-fA-F]{24}$
responses:
'200':
description: "```\n{\n ok: true,\n appearance: { [attribute: string]: string } // e.g. { 'data-primary-color': '#0052cc' }, {} when none\n}\n```"
'400':
description: '`appId` missing or not a 24-hex id.
```
{ ok: false, error: ''appId required'' }
```'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'429':
description: 'Rate limit exceeded.
```
{ success: false, error: ''Too many requests'', code: ''RATE_LIMITED'' }
```'
'500':
$ref: '#/components/responses/InternalServerError'
/v2/apps/{appId}/widget/appearance:
get:
tags:
- AI Widget
summary: Get the stored AI Widget appearance for an app
description: 'Returns the website widget appearance stored for the App: a map of the
widget''s `data-*` attribute names to string values, holding only the
values that differ from the widget''s defaults.
Auth: tenantActor, with App settings read access (the App''s creator
always has it).'
operationId: v2_apps_widget_appearance_get
parameters:
- in: path
name: appId
description: 'Target App ID. Must be an app the calling actor owns or has been
delegated access to.
'
required: true
schema:
type: string
security:
- UserJwtAuth: []
- B2BServerJwtAuth: []
responses:
'200':
description: '```
{ ok: true, appearance: { [attribute: string]: string } } // {} when nothing is stored
```'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: Caller has no access to this app.
'404':
description: '```
{ error: ''App not found'', code: ''APP_NOT_FOUND'' }
```'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
put:
tags:
- AI Widget
summary: Replace the stored AI Widget appearance for an app
description: 'Replaces the stored appearance with `appearance`, a map of `data-*`
attribute names to values. Send only the values that differ from the
widget''s defaults; `{}` clears everything. Numbers and booleans are
accepted and stored as strings; a `null` value drops that attribute.
The embed reads the stored set at load (through `GET /v2/widget/config`) as
the lowest layer: attributes written on the snippet and URL parameters
override it.
Every attribute is checked against an allowlist, and the whole request is
rejected (422) if any name is unknown or any value invalid:
- text: `data-title`, `data-greeting-title` (120 chars), `data-greeting`
(500), `data-greeting-message` (1000), `data-cta-text` (160); no control
characters.
- colors (hex, rgb/rgba/hsl/hsla, or a color name): `data-primary-color`,
`data-secondary-color`, `data-icons-color`, `data-own-bubble-bg`,
`data-other-bubble-bg`, `data-input-bg`.
- `data-launcher-gradient`: two colors separated by a comma.
- sizes (number with px, %, vw, vh, svh, dvh, rem or em, or a `calc()`):
`data-font-size`, `data-width`, `data-height`, `data-expanded-width`,
`data-expanded-height`, `data-expanded-inset`.
- booleans (`true`/`false`, `1`/`0`, `yes`/`no`, stored as `true` or
`false`): `data-hide-system-messages`, `data-disable-media`,
`data-allow-fullscreen`, `data-start-fullscreen`, `data-flat-launcher`,
`data-launcher-glow`, `data-cta-sparkle`.
- integers: `data-launcher-size` (24 to 160), `data-cta-delay` (0 to 600000).
- `data-position`: `left` or `right`.
- `data-locale`: a language tag such as `en` or `pt-BR`.
- `data-font-family`: letters, digits, spaces, commas, quotes, `_` and `-`
(120 chars).
- `data-google-font`: a Google Fonts family spec (120 chars).
- `data-launcher-icon`: an http(s) URL.
Identity attributes (`data-app-id`, `data-api-base`, `data-bot-id`) cannot be
stored: a saved setting never repoints the widget at another App, bot or
backend.
Auth: tenantActor, with App settings update access (the App''s creator
always has it).'
operationId: v2_apps_widget_appearance_put
parameters:
- in: path
name: appId
description: 'Target App ID. Must be an app the calling actor owns or has been
delegated access to.
'
required: true
schema:
type: string
security:
- UserJwtAuth: []
- B2BServerJwtAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- appearance
properties:
appearance:
type: object
additionalProperties:
oneOf:
- type: string
- type: number
- type: boolean
example:
data-primary-color: '#0052cc'
data-position: left
data-greeting: Hi! How can we help?
responses:
'200':
description: 'The stored set, values normalised.
```
{ ok: true, appearance: { [attribute: string]: string } }
```'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: Caller has no access to this app.
'404':
description: '```
{ error: ''App not found'', code: ''APP_NOT_FOUND'' }
```'
'422':
description: '`appearance` missing or not an object, or attributes that are unknown or
hold invalid values (all named in the message).
```
{ error: ''Invalid widget appearance: data-foo (not a widget appearance attribute), ...'', code: ''VALIDATION_ERROR'' }
```'
'500':
$ref: '#/components/responses/InternalServerError'
/v2/apps/{appId}/widget/conversations:
get:
tags:
- AI Widget
summary: List AI Widget conversations for an app
description: 'Returns paginated rows from the `chats` collection where `type=''widget''` and
`appId` matches. Each row includes resolved visitor identity (joined via
`user_to_chats` -> users where `isVisitor:true`) so the admin Widget
Conversations panel can render visitor handle + first-seen timestamps
without an additional round-trip per row.
Auth: tenantActor. Base-app admins acting on a child app via App Switcher
reach this endpoint through the same path as other app-scope chat reads.'
parameters:
- in: path
name: appId
description: 'Target App ID. Must be an app the calling actor owns or has been
delegated access to.
'
required: true
schema:
type: string
- in: query
name: limit
description: 'Maximum number of items to return per page. Server caps vary per
endpoint; the response carries the actual page size.
'
schema:
type: integer
minimum: 1
maximum: 200
default: 50
- in: query
name: offset
description: 'Number of items to skip from the beginning of the result set. Use
with `limit` for paging.
'
schema:
type: integer
minimum: 0
default: 0
security:
- UserJwtAuth: []
- B2BServerJwtAuth: []
responses:
'200':
description: "```\n{\n results: [\n {\n _id: string, // Mongo chat _id\n name: string, // bare room name, \"${appId}_<roomUuid>\"\n title: string,\n type: 'widget',\n createdAt: ISODate,\n updatedAt: ISODate,\n visitor: {\n _id: string,\n uuid: string,\n xmppUsername: string,\n firstSeenAt: ISODate\n } | null\n }\n ],\n pagination: { limit: number, offset: number, total: number },\n total: number, limit: number, offset: number,\n items: [...same as results...]\n}\n```"
'400':
$ref: '#/components/responses/BadRequest'
'401':
description: Missing / invalid token.
'403':
description: Caller has no access to this app.
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
operationId: get_v2_apps_appid_widget_conversations
components:
responses:
UnprocessableEntity:
description: Unprocessable Entity
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
NotFound:
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
InternalServerError:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Forbidden:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
ErrorResponse:
type: object
properties:
success:
type: boolean
description: Always false for error responses.
example: false
error:
type: string
description: A brief, human-readable message about the error.
code:
type: string
description: A stable machine-readable error code.
example: VALIDATION_ERROR
details:
type: object
description: Optional, more specific details about the error (e.g., validation errors).
errors:
description: Legacy validation errors (if applicable).
requestId:
type: string
description: Request correlation id for debugging/support.
required:
- error
example:
success: false
error: Validation Failed
code: VALIDATION_ERROR
details:
field: Reason
requestId: '...'
securitySchemes:
JwtAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'Authorization: Bearer <JWT>'
AppJwtAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'App JWT (data.type="app"). Use this token to call app-authorized endpoints (e.g. create user, login, signup). Authorization: Bearer <JWT>. Wrong token type returns code=INVALID_TOKEN_TYPE.'
UserJwtAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'User JWT (data.type="user"). Use this token for user-authorized endpoints (e.g. chats/files/sharelinks/me). Authorization: Bearer <JWT>. Wrong token type returns code=INVALID_TOKEN_TYPE.'
RefreshJwtAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'Refresh JWT (data.type="refresh"). Use this token only for refresh flow. Authorization: Bearer <JWT>. Wrong token type returns code=INVALID_TOKEN_TYPE.'
ClientJwtAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'Client JWT (data.type="client"). Legacy compatibility token used only for client credential exchange endpoints. Preferred header: x-custom-token; Authorization Bearer may also be accepted by some deployments.'
B2BServerJwtAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'B2B Server JWT (data.type="server"). Use this token for tenant/backend integration endpoints. Preferred header: Authorization: Bearer <JWT>. Legacy header x-custom-token is also accepted for backward compatibility.'