openapi: 3.1.0 info: title: Kavchat API version: 0.0.11 summary: Scripted support chat for apps and websites. description: | Two public surfaces: * **Client API** (`/v1/client/{publicKey}`): called by the web widget and the mobile SDKs. The public key (`pk_live_...`) is safe to embed. Open to guests. * **Server API** (`/v1/server`): called by your backend with the secret key (`sk_live_...`). Never ship the secret key to a browser or an app. ### Identifying your users To tag sessions and tickets with your own user id, or to show the bot only to testers, sign a JWT on your backend with the secret key and pass it to the widget/SDK: * algorithm `HS256`, key = your secret key (the full `sk_live_...` string, as UTF-8 bytes) * claims: `sub` (your user id, required), `exp` (required, at most 30 days ahead), optional `phone` (E.164 or local), `name`, `email` * sent as `Authorization: Bearer ` on client calls ### Webhooks When a ticket is opened Kavchat POSTs `{ "type": "ticket.created", "data": Ticket }` to your webhook URL with header `Kavchat-Signature: t=,v1=` where `v1 = HMAC-SHA256(secretKey, ".")`. Reject requests older than 5 minutes. Failed deliveries are retried after 5 s, 30 s and 2 min. ### Live chat (talk to a representative) Next to the contact form, the bot can offer "talk to a representative". The full client guide (state machines, reconnect algorithm, what to show) is `docs/live-chat-protocol.md`. * **Show the option** only when the release has `liveChat.enabled: true`, and ask `GET /live/availability` right before showing it (availability changes during the day). * **No CORS preflight from browsers**: no custom headers, POST bodies are JSON sent as `Content-Type: text/plain` (application/json is accepted too), the public key stays in the path. * **Visitor credential**: `POST /live/requests` returns an opaque `credential` (`lvc_...`) scoped to one conversation of one workspace, valid 12 hours. Every visitor command carries it in the body. Keep it in memory or session storage, never in a URL or a log. * **Streams**: `EventSource` cannot send headers, so get a single-use `ticket` (`lst_...`, 60 s) with a POST, then open `GET .../stream?ticket=...&after=`. A ticket works once; on any stream error close the EventSource yourself, get a new ticket and reconnect with `after`. * **Ordering and delivery**: every persisted event has `seq`, increasing per conversation. Sequences can have gaps (never duplicates): order and dedupe by `seq`, never wait for a missing one. An event is broadcast only after it is stored; anything missed is recovered with `POST /live/sync { after }` (also a long-poll fallback with `wait`). * **Idempotency**: `requestId` on a handoff and `clientId` on every message are generated by the client (8-64 chars of `A-Za-z0-9_-`). Retrying with the same id returns the original result (`duplicate: true` for messages). A retried handoff returns the same conversation with a new credential; the previous credential stops working. * **Fallback**: when nobody can answer (outside business hours, no free agent, monthly quota used up, or live chat off), `POST /live/requests` answers `accepted: false` with `fallback: "ticket"`: show the regular contact form. A queued chat that nobody claims within `waitSeconds` (default 120) becomes a ticket automatically, in the same conversation, and the visitor receives `conversation.ticket`. ### Responses and errors Success: `{ "success": true, "data": ... }`. Error: `{ "success": false, "code": "", "message": "", "details"?: ... }`. Messages follow `Accept-Language` (he, en, ar, ru, es, fr, de, pt, it, tr, th). servers: - url: https://api.kavchat.com tags: - name: Client - name: Server - name: Dashboard auth description: | Sign-in for the Kavchat dashboard. There are no passwords: a one-time code by email, or a passkey (WebAuthn, bound to app.kavchat.com). Not needed for the SDKs. - name: Admin description: | Dashboard bot management for multilingual bots (docs/multilingual.md). `Authorization: Bearer ` and `X-Kavchat-Workspace`. Script translations are saved with the script content (`translations`). - name: Leads description: Contact details visitors leave in a lead step of a script. See docs/collect-and-conditions.md - name: Live chat (visitor) description: | "Talk to a representative" from the widget and the mobile SDKs. Public key in the path, JSON bodies sent as text/plain, the visitor credential in the body. See docs/live-chat-protocol.md. - name: Live chat (agent) description: | The dashboard inbox. `Authorization: Bearer ` and `X-Kavchat-Workspace`. Permissions: `live_chat` (inbox; owners, publishers, editors) and `live_chat_admin` (settings, departments, agents, saved replies, deletion; owners). Claiming and replying also need an agent seat (`/live/agents`). paths: /v1/client/{publicKey}/release: get: tags: [Client] operationId: getRelease summary: Published bot content and appearance description: | Returns 304 when `If-None-Match` matches the current ETag. Cache the body on the device. Multilingual bots (docs/multilingual.md): send the visitor's preferred languages in `locale`. The server answers in the first one the workspace offers (compared by base language: `en-US` matches `en`), or in the default language. `locale` in the body is the content language actually used; `locales` lists the available ones. Missing translated texts fall back to the default language field by field. The ETag differs per language. `Accept-Language` is ignored on purpose, so a client that sends no `locale` always gets the default language, exactly as before. parameters: - $ref: '#/components/parameters/PublicKey' - in: query name: locale required: false description: 'Preferred languages, most preferred first, comma separated, at most 5 (e.g. `fr-CA,en-US,he`)' schema: { type: string, maxLength: 200 } - in: header name: If-None-Match schema: { type: string } security: [{}, { identityToken: [] }] responses: '200': description: Release headers: ETag: { schema: { type: string } } content: application/json: schema: type: object required: [success, data] properties: success: { const: true } data: { $ref: '#/components/schemas/ClientRelease' } '304': { description: Not modified } '401': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/events: post: tags: [Client] operationId: reportEvents summary: Report the conversation path and unanswered questions description: Send the full path so far each time; the server keeps the latest. Idempotent per session. parameters: [{ $ref: '#/components/parameters/PublicKey' }] security: [{}, { identityToken: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/EventsInput' } responses: '200': { $ref: '#/components/responses/Saved' } '400': { $ref: '#/components/responses/Error' } '409': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/screens: post: tags: [Client] operationId: registerScreens summary: Declare the screens of your app or website description: | The SDKs call this automatically when you pass `screens` in their configuration: only when the list changed, or once a week. The dashboard editor then offers these screens in a dropdown for "Open a screen" buttons and warns about buttons that point to an unknown screen. At most 100 screens per workspace. parameters: [{ $ref: '#/components/parameters/PublicKey' }] security: [{}, { identityToken: [] }] requestBody: required: true content: application/json: schema: type: object required: [platform, screens] properties: platform: { type: string, enum: [web, react-native, flutter, ios, android] } screens: type: array maxItems: 100 items: type: object required: [id, label] properties: id: { type: string, pattern: '^[A-Za-z0-9_.-]{1,40}$', description: Passed to your onScreen callback } label: { type: string, maxLength: 60, description: Shown to the admin in the editor } needsTarget: { type: boolean, default: false, description: 'The screen needs an item id, for example a product number' } responses: '200': description: Saved content: application/json: schema: type: object properties: success: { const: true } data: { type: object, properties: { saved: { type: integer } } } '400': { $ref: '#/components/responses/Error' } '401': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/site/seen: post: tags: [Client] operationId: siteSeen summary: Website learning, a page was opened with the bot (web only) description: | Send once per page view when the release has `settings.siteLearning: true`, so the bot learns the page for AI answers. Only the address (origin and path) is sent, nothing about the visitor. The server ignores a page whose host differs from the request's `Origin` or is not one of the workspace's learned websites (chosen by the owner, or a site where the bot is installed and the public key was found in its page). JSON sent as `text/plain` (no preflight, works with `navigator.sendBeacon`). Always `200`. parameters: [{ $ref: '#/components/parameters/PublicKey' }] security: [{}] requestBody: required: true content: text/plain: schema: type: object required: [url] properties: url: { type: string, maxLength: 500, example: 'https://example.com/shipping' } responses: '200': description: Received content: application/json: schema: { type: object, properties: { success: { type: boolean }, data: { type: object, properties: { ok: { type: boolean } } } } } /v1/client/{publicKey}/ai/answer: post: tags: [Client] operationId: aiAnswer summary: AI answer for a question no script matched (Premium, customer's own AI key) description: | Call only when the release has `settings.aiAnswers: true` and the typed question matched no script. The answer is generated from the workspace's published scripts, knowledge notes and (with website learning on) the public pages of its own website only, with the AI provider key the workspace owner saved. Answers are kept for 90 days to find questions that keep coming back; those become AI script suggestions for the owner to approve (see adminApproveAiScript). Any failure (feature off, monthly AI budget reached, provider error) returns `answer: null`; show the regular "no match" message then. Limited to 10 answers per conversation every 15 minutes. parameters: [{ $ref: '#/components/parameters/PublicKey' }] security: [{}, { identityToken: [] }] requestBody: required: true content: application/json: schema: type: object required: [sessionId, question] properties: sessionId: { type: string, maxLength: 64 } question: { type: string, maxLength: 300 } locale: { $ref: '#/components/schemas/Locale', description: 'The release `locale`: the answer is written in it. Missing: the default language' } responses: '200': description: The answer, or null content: application/json: schema: type: object properties: success: { const: true } data: type: object properties: answer: { type: [string, 'null'], maxLength: 600 } scriptId: { type: [string, 'null'], description: A script to offer as the next step } '400': { $ref: '#/components/responses/Error' } '401': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/tickets: post: tags: [Client] operationId: createTicket summary: Open a support request from the bot contact form description: Guests must leave an email or a phone. Identified users may leave neither. parameters: [{ $ref: '#/components/parameters/PublicKey' }] security: [{}, { identityToken: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TicketInput' } responses: '201': description: Created content: application/json: schema: type: object properties: success: { const: true } data: type: object properties: id: { type: string } number: { type: integer } '400': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/leads: post: tags: [Client, Leads] operationId: createLead summary: Send a lead from a lead step (BotResult.lead) description: | Idempotent per `clientId`: generate it once per lead and reuse it on every retry. The same `clientId` again returns `200` with the same lead. `email` and `phone` variables are normalized like tickets (libphonenumber with the workspace default country); invalid keys and empty values are dropped. Accepts JSON, or JSON sent as `text/plain` (no preflight). Limited to 20 per IP per hour and 10 per session. parameters: [{ $ref: '#/components/parameters/PublicKey' }] security: [{}, { identityToken: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LeadInput' } text/plain: schema: { $ref: '#/components/schemas/LeadInput' } responses: '201': description: Created content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: object, properties: { id: { type: string }, number: { type: integer, description: '7 random digits of ref. Kept a number for older clients' }, ref: { type: string, description: 'The request number shown to the requester, k--<7 random digits>-, for example k-22-7489392-1' } } } } } } } '200': description: Already received (same clientId) content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: object, properties: { id: { type: string }, number: { type: integer, description: '7 random digits of ref. Kept a number for older clients' }, ref: { type: string, description: 'The request number shown to the requester, k--<7 random digits>-, for example k-22-7489392-1' } } } } } } } '400': { description: '`lead_invalid`, `lead_empty`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '429': { $ref: '#/components/responses/Error' } /v1/server/workspace: get: tags: [Server] operationId: getWorkspace summary: Workspace details and public key security: [{ secretKey: [] }] responses: '200': description: Workspace content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/Workspace' } '401': { $ref: '#/components/responses/Error' } /v1/server/tickets: get: tags: [Server] operationId: listTickets summary: Newest first. Page with `before=`. security: [{ secretKey: [] }] parameters: - { in: query, name: status, schema: { $ref: '#/components/schemas/TicketStatus' } } - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } - { in: query, name: before, schema: { type: integer, minimum: 1 } } responses: '200': description: Tickets content: application/json: schema: type: object properties: success: { const: true } data: { type: array, items: { $ref: '#/components/schemas/Ticket' } } '401': { $ref: '#/components/responses/Error' } /v1/server/tickets/{id}: get: tags: [Server] operationId: getTicket summary: One ticket with its transcript security: [{ secretKey: [] }] parameters: [{ $ref: '#/components/parameters/TicketId' }] responses: '200': description: Ticket content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/Ticket' } '404': { $ref: '#/components/responses/Error' } /v1/server/tickets/{id}/status: put: tags: [Server] operationId: setTicketStatus summary: Close or reopen a ticket security: [{ secretKey: [] }] parameters: [{ $ref: '#/components/parameters/TicketId' }] requestBody: required: true content: application/json: schema: type: object required: [status] properties: status: { $ref: '#/components/schemas/TicketStatus' } responses: '200': description: Updated ticket content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/Ticket' } '400': { $ref: '#/components/responses/Error' } '404': { $ref: '#/components/responses/Error' } /v1/server/leads: get: tags: [Server, Leads] operationId: listLeads summary: Newest first. Page with `before=`. security: [{ secretKey: [] }] parameters: &leadFilters - { in: query, name: status, schema: { $ref: '#/components/schemas/LeadStatus' } } - { in: query, name: scriptId, schema: { type: string } } - { in: query, name: from, schema: { type: string, format: date-time } } - { in: query, name: to, schema: { type: string, format: date-time } } - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } - { in: query, name: before, schema: { type: integer, minimum: 1 } } responses: '200': { description: Leads, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Lead' } } } } } } } '401': { $ref: '#/components/responses/Error' } /v1/server/leads/{id}: get: tags: [Server, Leads] operationId: getLead summary: One lead with its transcript security: [{ secretKey: [] }] parameters: [{ $ref: '#/components/parameters/Id' }] responses: '200': { description: Lead, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Lead' } } } } } } '404': { $ref: '#/components/responses/Error' } /v1/server/leads/{id}/status: put: tags: [Server, Leads] operationId: setLeadStatus summary: Move a lead through new, contacted, won, lost security: [{ secretKey: [] }] parameters: [{ $ref: '#/components/parameters/Id' }] requestBody: required: true content: application/json: schema: { type: object, required: [status], properties: { status: { $ref: '#/components/schemas/LeadStatus' } } } responses: '200': { description: Updated lead, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Lead' } } } } } } '400': { $ref: '#/components/responses/Error' } '404': { $ref: '#/components/responses/Error' } /v1/server/reports: get: tags: [Server] operationId: getReports summary: Sessions, outcomes, drop-off points and unanswered questions. Defaults to the last 30 days. security: [{ secretKey: [] }] parameters: - { in: query, name: from, schema: { type: string, format: date-time } } - { in: query, name: to, schema: { type: string, format: date-time } } responses: '200': description: Report content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/Report' } /v1/server/settings: get: tags: [Server] operationId: getSettings summary: Bot settings and audience security: [{ secretKey: [] }] responses: '200': description: Bot settings content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/Settings' } put: tags: [Server] operationId: updateAudience summary: Change who sees the bot. Other settings are edited in the dashboard. security: [{ secretKey: [] }] requestBody: required: true content: application/json: schema: type: object properties: audience: { $ref: '#/components/schemas/Audience' } testerPhones: { type: array, items: { type: string }, maxItems: 50 } responses: '200': description: Bot settings content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/Settings' } '400': { $ref: '#/components/responses/Error' } /v1/server/releases: get: tags: [Server] operationId: listReleases summary: Published versions security: [{ secretKey: [] }] responses: '200': description: Last 50 releases, newest first content: application/json: schema: type: object properties: success: { const: true } data: { type: array, items: { $ref: '#/components/schemas/ReleaseSummary' } } # ---------- Dashboard sign-in (used by app.kavchat.com, not by the SDKs) ---------- /v1/auth/email/start: post: tags: [Dashboard auth] summary: Email a 6-digit sign-in code description: | Same answer whether or not the address has an account. The code is valid for 10 minutes and only together with the returned `attemptId` (keep it in the browser that asked). Limits: one code per 30 s and 5 per hour per address, 20 per hour per IP (`429` with `code_wait` / `code_limit`). requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: { type: string, format: email } locale: { type: string, enum: [he, en, ar, ru, es, fr, de, pt, it, tr, th] } purpose: { type: string, enum: [signin, signup] } responses: '200': description: Code sent (or silently not sent when sign-up is closed and there is no account) content: application/json: schema: type: object properties: success: { type: boolean } data: type: object properties: attemptId: { type: string, description: 128-bit random id, bound to this code } expiresIn: { type: integer, example: 600 } resendIn: { type: integer, example: 30 } '429': { $ref: '#/components/responses/Error' } /v1/auth/email/verify: post: tags: [Dashboard auth] summary: Verify the code and get a session description: | Existing account: returns a session. No account: without `signup` returns `{ signupRequired: true }` and the code stays valid; with `signup` the account and its first workspace are created. 5 wrong tries invalidate the code (`code_attempts`); 10 wrong codes in an hour lock code sign-in for the address for 15 minutes (`code_locked`). requestBody: required: true content: application/json: schema: type: object required: [attemptId, email, code] properties: attemptId: { type: string } email: { type: string, format: email } code: { type: string, pattern: '^\d{6}$' } deviceId: { type: string, description: Random id kept by the browser, used for new-device alerts } signup: type: object required: [name] properties: name: { type: string, maxLength: 80 } workspaceName: { type: string, maxLength: 80 } country: { type: string, example: IL } locale: { type: string } responses: '200': description: '`{ token, admin }` (JWT, 30 days sliding, 90 days at most) or `{ signupRequired: true }`' '400': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/auth/providers: get: tags: [Dashboard auth] summary: Sign-in options the dashboard can offer responses: '200': description: '`{ google: { clientId } | null }`. Google sign-in is shown only when the server has a client id' /v1/auth/google: post: tags: [Dashboard auth] summary: Sign in or sign up with a Google ID token (Google Identity Services) description: > The server checks the signature, expiry, issuer and audience, and requires a Google-verified email. Without an account the answer is `{ signupRequired, email, name }`; send the same credential again with `signup`. requestBody: required: true content: application/json: schema: type: object required: [credential] properties: credential: { type: string, description: ID token from Google Identity Services } deviceId: { type: string } signup: type: object required: [name] properties: name: { type: string, maxLength: 80 } workspaceName: { type: string, maxLength: 80 } country: { type: string, example: IL } locale: { type: string } responses: '200': description: '`{ token, admin }` or `{ signupRequired: true, email, name }`' '401': { $ref: '#/components/responses/Error' } '403': { $ref: '#/components/responses/Error' } '404': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/auth/passkey/options: post: tags: [Dashboard auth] summary: WebAuthn options for passkey sign-in (discoverable credentials, user verification required) responses: '200': description: '`{ flowId, options }`. The challenge is single-use and expires after 5 minutes' /v1/auth/passkey/verify: post: tags: [Dashboard auth] summary: Verify a passkey assertion and get a session requestBody: required: true content: application/json: schema: type: object required: [flowId, response] properties: flowId: { type: string } response: { type: object, description: AuthenticationResponseJSON } deviceId: { type: string } responses: '200': { description: '`{ token, admin }`' } '400': { $ref: '#/components/responses/Error' } /v1/auth/step-up/email/start: post: tags: [Dashboard auth] summary: Email a code to the signed-in admin before a sensitive action security: [{ adminToken: [] }] responses: '200': { description: '`{ attemptId, expiresIn, resendIn }`' } /v1/auth/step-up/email/verify: post: tags: [Dashboard auth] summary: Confirm the step-up code (`{ attemptId, code }`) security: [{ adminToken: [] }] responses: '200': { description: Sensitive actions are allowed for the next 10 minutes } /v1/auth/step-up/passkey/options: post: tags: [Dashboard auth] summary: WebAuthn options limited to the admin's own passkeys security: [{ adminToken: [] }] responses: '200': { description: '`{ flowId, options }`' } /v1/auth/step-up/passkey/verify: post: tags: [Dashboard auth] summary: Confirm the step-up with a passkey (`{ flowId, response }`) security: [{ adminToken: [] }] responses: '200': { description: Sensitive actions are allowed for the next 10 minutes } /v1/auth/passkeys: get: tags: [Dashboard auth] summary: List the admin's passkeys security: [{ adminToken: [] }] responses: '200': { description: '`[{ id, name, deviceType, backedUp, createdAt, lastUsedAt }]`' } post: tags: [Dashboard auth] summary: Register a passkey (`{ flowId, response, name? }`). Needs a sign-in in the last 10 minutes security: [{ adminToken: [] }] responses: '201': { description: The new passkey } '403': { description: '`reauth_required`: verify again with /v1/auth/step-up/...' } /v1/auth/passkeys/options: post: tags: [Dashboard auth] summary: WebAuthn registration options (resident key and user verification required, at most 10 passkeys) security: [{ adminToken: [] }] responses: '200': { description: '`{ flowId, options }`' } '403': { description: '`reauth_required`' } /v1/auth/passkeys/{id}: put: tags: [Dashboard auth] summary: Rename a passkey (`{ name }`) security: [{ adminToken: [] }] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: '200': { description: The passkey } delete: tags: [Dashboard auth] summary: Remove a passkey. Needs a sign-in in the last 10 minutes security: [{ adminToken: [] }] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: '200': { description: The remaining passkeys } '403': { description: '`reauth_required`' } /v1/auth/sessions: get: tags: [Dashboard auth] summary: Signed-in devices of the admin security: [{ adminToken: [] }] responses: '200': { description: '`[{ id, current, method, device, ipPrefix, createdAt, lastSeenAt }]`' } /v1/auth/sessions/{id}: delete: tags: [Dashboard auth] summary: Sign one device out security: [{ adminToken: [] }] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: '200': { description: '`{ ok: true }`' } /v1/auth/sign-ins: get: tags: [Dashboard auth] summary: The last 20 sign-ins and verifications (kept 90 days) security: [{ adminToken: [] }] responses: '200': { description: '`[{ method, purpose, success, device, ipPrefix, createdAt }]`' } /v1/auth/logout: post: tags: [Dashboard auth] summary: Sign this device out security: [{ adminToken: [] }] responses: '200': { description: '`{ ok: true }`' } /v1/auth/logout-all: post: tags: [Dashboard auth] summary: Sign out of all devices security: [{ adminToken: [] }] responses: '200': { description: '`{ ok: true }`' } # ---------- Live chat: visitor (widget and apps) ---------- /v1/client/{publicKey}/live/availability: get: tags: [Live chat (visitor)] operationId: liveAvailability security: [{}] summary: Can a representative be reached right now description: | Ask right before showing "talk to a representative" and again when the user taps it. `open` is true when the department is within its business hours AND at least one agent is available with room for another chat AND the monthly conversations quota is not used up. Do not cache for more than 30 seconds. parameters: - $ref: '#/components/parameters/PublicKey' - { in: query, name: departmentId, schema: { type: string }, description: Answer for one department } responses: '200': description: Availability content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/LiveAvailability' } '401': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/live/requests: post: tags: [Live chat (visitor)] operationId: liveRequest summary: Ask for a representative (handoff from the bot) description: | Guests must leave an email or a phone (the chat may become a ticket if nobody answers in time); identified users (identity token in `identityToken`, or `Authorization` for apps that do not care about preflight) may leave neither, and their details are marked `verified`. * `201` + `accepted: true`: the conversation is queued. Keep `credential`, open the stream. * `200` + `accepted: false`: show the regular contact form (`POST /tickets`). `reason` says why. Idempotent per `requestId`: a retry returns the same conversation with a NEW credential (the previous one stops working). Sending a still-valid `credential` of an open conversation returns that conversation instead of opening another. At most one open conversation per `sessionId`: a guest gets `409 live_conversation_open`; the same identified user gets their conversation back with a new credential. Limited to 10 requests per IP every 10 minutes. parameters: [{ $ref: '#/components/parameters/PublicKey' }] security: [{}, { identityToken: [] }] requestBody: { $ref: '#/components/requestBodies/LiveRequest' } responses: '201': description: Queued content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/LiveAccepted' } '200': description: Nobody can answer now. Show the contact form content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/LiveUnavailable' } '400': { description: '`ticket_contact_required`, `email_invalid`, `phone_invalid`, `id_invalid`, `live_message_too_long`, `field_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '401': { description: '`invalid_key`, `identity_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '404': { description: '`live_department_not_found`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '409': { description: '`live_conversation_open`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/live/messages: post: tags: [Live chat (visitor)] operationId: liveVisitorMessage security: [{}] summary: Send a message (plain text, up to 2000 characters) description: | HTML tags are removed. Answer only after the message is stored. A retry with the same `clientId` returns the stored event with `duplicate: true` (even if the chat closed meanwhile). Allowed while `queued` or `active`. parameters: [{ $ref: '#/components/parameters/PublicKey' }] requestBody: required: true content: text/plain: schema: { $ref: '#/components/schemas/LiveVisitorMessageInput' } application/json: schema: { $ref: '#/components/schemas/LiveVisitorMessageInput' } responses: '200': { $ref: '#/components/responses/LiveVisitorEventSaved' } '400': { description: '`live_message_required`, `live_message_too_long`, `id_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '401': { description: '`live_credential_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '409': { description: '`live_conversation_closed`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/live/typing: post: tags: [Live chat (visitor)] operationId: liveVisitorTyping security: [{}] summary: Typing indicator for the agent (not stored) description: 'Send `on: true` at most every 3 seconds while typing, and `on: false` when the field is cleared or sent.' parameters: [{ $ref: '#/components/parameters/PublicKey' }] requestBody: required: true content: text/plain: schema: { $ref: '#/components/schemas/LiveTypingInput' } application/json: schema: { $ref: '#/components/schemas/LiveTypingInput' } responses: '200': { $ref: '#/components/responses/Ok' } '401': { $ref: '#/components/responses/Error' } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/live/sync: post: tags: [Live chat (visitor)] operationId: liveVisitorSync security: [{}] summary: Events after a sequence number; long-poll fallback description: | Returns the conversation state and up to 200 visitor-visible events with `seq > after`, in order. `hasMore: true`: call again with the last `seq`. With `wait` (1-25 s) and nothing new, the server holds the request until an event or state change arrives, or the wait ends. Use after every reconnect, and instead of the stream where EventSource is not available. parameters: [{ $ref: '#/components/parameters/PublicKey' }] requestBody: required: true content: text/plain: schema: { $ref: '#/components/schemas/LiveSyncInput' } application/json: schema: { $ref: '#/components/schemas/LiveSyncInput' } responses: '200': description: Events content: application/json: schema: type: object properties: success: { const: true } data: { $ref: '#/components/schemas/LiveVisitorSync' } '401': { description: '`live_credential_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '429': { $ref: '#/components/responses/Error' } '503': { description: '`live_busy`: too many open streams on the server. Retry with backoff', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/client/{publicKey}/live/stream-ticket: post: tags: [Live chat (visitor)] operationId: liveVisitorStreamTicket security: [{}] summary: Single-use ticket for opening the SSE stream parameters: [{ $ref: '#/components/parameters/PublicKey' }] requestBody: required: true content: text/plain: schema: { $ref: '#/components/schemas/LiveCredentialInput' } application/json: schema: { $ref: '#/components/schemas/LiveCredentialInput' } responses: '200': { $ref: '#/components/responses/LiveStreamTicket' } '401': { description: '`live_credential_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '429': { $ref: '#/components/responses/Error' } /v1/client/{publicKey}/live/stream: get: tags: [Live chat (visitor)] operationId: liveVisitorStream security: [{}] summary: Server-Sent Events for one conversation description: | `new EventSource(url)` with `ticket` from `/live/stream-ticket` (works once, within 60 s) and `after` = the highest `seq` already shown (0 for none). The server first sends `ready`, then every stored event after `after` (replay), then live events. Named events (see `LiveVisitorStreamEvent`): * `ready` `{ conversation, lastSeq }` * `event` (SSE `id` = `seq`): a `LiveVisitorEvent` * `state`: a `LiveVisitorConversation`, whenever the state changes * `typing` `{ author: { kind: "agent", name }, on }` (not stored) * `bye` `{ reason }`: `replaced` (a newer credential took over), `expired`, `deleted`; the server closes the stream A comment line (`: ping`) every 25 s keeps proxies open. The stream ends after one hour or when the credential expires. On any error or end: close the EventSource (do not let it reconnect on its own: the ticket is spent), get a new ticket and reconnect with `after`. At most 5 streams per conversation and 20 per IP. parameters: - $ref: '#/components/parameters/PublicKey' - { in: query, name: ticket, required: true, schema: { type: string, pattern: '^lst_[A-Za-z0-9_-]{20,80}$' } } - { in: query, name: after, schema: { type: integer, minimum: 0, default: 0 } } responses: '200': description: The event stream content: text/event-stream: schema: { $ref: '#/components/schemas/LiveVisitorStreamEvent' } '401': { description: '`live_stream_ticket_invalid` (spent or expired ticket), `live_credential_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '429': { description: '`too_many_requests`: too many streams for this conversation or IP', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '503': { description: '`live_busy`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/client/{publicKey}/live/leave-message: post: tags: [Live chat (visitor)] operationId: liveLeaveMessage security: [{}] summary: Leave a message instead of waiting description: | The conversation becomes a ticket now (`state: ticket`, reason `visitor`), with the bot and chat transcript. An optional last `text` is stored first. Safe to repeat. parameters: [{ $ref: '#/components/parameters/PublicKey' }] requestBody: required: true content: text/plain: schema: { $ref: '#/components/schemas/LiveLeaveMessageInput' } application/json: schema: { $ref: '#/components/schemas/LiveLeaveMessageInput' } responses: '200': { $ref: '#/components/responses/LiveVisitorConversation' } '401': { $ref: '#/components/responses/Error' } '409': { description: '`live_conversation_closed`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/client/{publicKey}/live/end: post: tags: [Live chat (visitor)] operationId: liveEnd security: [{}] summary: End the chat description: 'Closes the conversation (`closeReason: visitor`). Safe to repeat; a chat that already ended is returned as it is.' parameters: [{ $ref: '#/components/parameters/PublicKey' }] requestBody: required: true content: text/plain: schema: { $ref: '#/components/schemas/LiveCredentialInput' } application/json: schema: { $ref: '#/components/schemas/LiveCredentialInput' } responses: '200': { $ref: '#/components/responses/LiveVisitorConversation' } '401': { $ref: '#/components/responses/Error' } # ---------- Live chat: agents (dashboard) ---------- /v1/admin/bot/languages: get: tags: [Admin] operationId: adminBotLanguages summary: Bot languages, the plan limit, settings translations and per-script progress (docs/multilingual.md) responses: '200': { description: 'defaultLocale, languages, limit (null = unlimited), served, ai, settings { units, translations }, scripts [{ id, kind, name, progress }]' } put: tags: [Admin] operationId: adminSetBotLanguages summary: Set the additional content languages (bot_publish). Free plan 2 languages including the default requestBody: required: true content: application/json: schema: { type: object, required: [languages], properties: { languages: { type: array, items: { $ref: '#/components/schemas/Locale' } } } } responses: '200': { description: The languages view } '400': { $ref: '#/components/responses/Error' } '402': { description: '`plan_limit_languages`' } /v1/admin/bot/languages/{locale}/settings: put: tags: [Admin] operationId: adminSaveSettingsTranslation summary: Translate bot name, subtitle, attention text, contact topics and live-chat departments (applies immediately) parameters: [{ in: path, name: locale, required: true, schema: { $ref: '#/components/schemas/Locale' } }] requestBody: required: true content: application/json: schema: type: object properties: texts: { type: object, additionalProperties: { type: string }, description: 'Keys: botName, subtitle, attention, topic:, dept:' } machine: { type: array, items: { type: string } } responses: '200': { description: The languages view } /v1/admin/bot/scripts/{id}/translate: post: tags: [Admin] operationId: adminTranslateScript summary: Translate a script draft automatically (Premium, the workspace AI key). Fills empty fields unless overwrite parameters: [{ in: path, name: id, required: true, schema: { type: string } }] requestBody: required: true content: application/json: schema: { type: object, required: [locale], properties: { locale: { $ref: '#/components/schemas/Locale' }, overwrite: { type: boolean } } } responses: '200': { description: 'The script, with translated { filled, failed }' } '400': { description: '`locale_invalid`: the default language or a language that is not enabled' } '402': { description: '`plan_feature_ai`' } '409': { description: '`ai_not_configured`' } /v1/admin/bot/translate-all: post: tags: [Admin] operationId: adminTranslateAll summary: Translate every script and the settings texts to one language (Premium, bot_edit and bot_publish) requestBody: required: true content: application/json: schema: { type: object, required: [locale], properties: { locale: { $ref: '#/components/schemas/Locale' }, overwrite: { type: boolean } } } responses: '200': { description: '{ filled, failed, view }' } /v1/admin/bot/scripts/{id}/ai/approve: post: tags: [Admin] operationId: adminApproveAiScript summary: Approve an AI script (bot_publish). Publishes its draft and records a new approved version description: | AI scripts are built from questions that keep getting AI answers: when the same question (same `groupKey` from `@kavchat/core` and the same conversation language) got an AI answer in 5 different conversations within 30 days, a suggestion is created from the phrasings and the last AI answer. No model call is made for this. Languages are the workspace content language and the bot's additional languages; answers the AI took from an existing script (also from its translation) create nothing. A suggestion from an additional language keeps the customer's text in `translations[locale]` (and, for now, in the default-language fields, listed in `ai.defaultMissing`). It cannot be approved until the name and answer are written in the default language, by hand or with `POST /v1/admin/bot/scripts/{id}/ai/translate-default`. A suggestion (`ai.state: suggested`) never answers customers. Approving publishes it: from then on it is a regular script that answers first, with no model call. Every approval (also through `POST /v1/admin/bot/publish`) bumps `ai.version` and keeps the content in `ai.versions`. An approved AI script changes only through a new approval. parameters: [{ in: path, name: id, required: true, schema: { type: string } }] responses: '200': { description: 'The script, with `ai` (see AiScript)' } '400': { description: '`not_ai_script`, or `publish_no_changes` when the draft is already the approved version' } '402': { description: '`plan_limit_scripts`: approving a suggestion adds a topic script' } '404': { description: '`script_not_found`' } '409': { description: '`ai_script_needs_default`: a suggestion from an additional language has no default-language name or answer yet (also from `POST /v1/admin/bot/publish`)' } /v1/admin/bot/scripts/{id}/ai/reject: post: tags: [Admin] operationId: adminRejectAiScript summary: Reject an AI suggestion, or keep the approved version of an AI script (bot_publish) description: | A suggestion is removed, and the same question is not suggested again for 90 days (deleting a suggestion does the same). On an approved AI script, unapproved draft changes are discarded, the "website changed" review flag is cleared, and the approved version keeps answering. parameters: [{ in: path, name: id, required: true, schema: { type: string } }] responses: '200': { description: '`{ rejected: true, id }` for a suggestion, `{ rejected: false, id, script }` for an approved AI script' } '400': { description: '`not_ai_script`' } '404': { description: '`script_not_found`' } /v1/admin/bot/scripts/{id}/ai/versions/{version}/restore: post: tags: [Admin] operationId: adminRestoreAiScriptVersion summary: Approve the content of an earlier approved version again, as a new version (bot_publish) parameters: - { in: path, name: id, required: true, schema: { type: string } } - { in: path, name: version, required: true, schema: { type: integer, minimum: 1 } } responses: '200': { description: 'The script. The new entry in `ai.versions` has `restoredFrom`' } '400': { description: '`not_ai_script`, or `publish_no_changes` when that content is already live' } '404': { description: '`script_not_found`, `version_not_found`' } /v1/admin/bot/scripts/{id}/ai/rewrite: post: tags: [Admin] operationId: adminRewriteAiScript summary: Write a new answer from the current website content into the draft (Premium, the workspace AI key, bot_edit) description: | Runs only when an admin asks for it, typically after the script was flagged because a website page it relied on changed (`ai.review`). The result goes into the draft; the approved version keeps answering until the next approval. parameters: [{ in: path, name: id, required: true, schema: { type: string } }] responses: '200': { description: The script } '400': { description: '`not_ai_script`' } '402': { description: '`plan_feature_ai`' } '409': { description: '`ai_not_configured`, `ai_budget_reached`' } '422': { description: '`ai_no_answer`: the AI found no answer in the content' } /v1/admin/bot/scripts/{id}/ai/translate-default: post: tags: [Admin] operationId: adminTranslateAiScriptToDefault summary: Translate an AI suggestion from an additional language into the default language (Premium, the workspace AI key, bot_edit) description: | Runs only when an admin asks for it. Translates the name, keywords and answer that still hold the customer's original text (source: `translations[ai.locale]`) into the workspace content language and saves them in the draft. Text the admin already wrote is not overwritten, and the customer's language stays in `translations[ai.locale]`. Approval stays a separate step. parameters: [{ in: path, name: id, required: true, schema: { type: string } }] responses: '200': { description: 'The script, plus `translated: { filled, failed }`' } '400': { description: '`not_ai_script`, or `locale_invalid` when the script is not a suggestion from an additional language' } '402': { description: '`plan_feature_ai`' } '404': { description: '`script_not_found`' } '409': { description: '`ai_not_configured`, `ai_budget_reached`' } '422': { description: '`ai_translate_failed`: nothing came back from the translation' } /v1/admin/leads: get: tags: [Leads] operationId: adminListLeads summary: The leads inbox (tickets permission) security: [{ adminToken: [] }] parameters: - { $ref: '#/components/parameters/Workspace' } - { in: query, name: status, schema: { $ref: '#/components/schemas/LeadStatus' } } - { in: query, name: scriptId, schema: { type: string } } - { in: query, name: from, schema: { type: string, format: date-time } } - { in: query, name: to, schema: { type: string, format: date-time } } - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } - { in: query, name: before, schema: { type: integer, minimum: 1 } } responses: '200': { description: Leads, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Lead' } } } } } } } /v1/admin/leads/export: get: tags: [Leads] operationId: adminExportLeads summary: CSV of the filtered leads (Premium) description: | UTF-8 with BOM. Columns: number, createdAt, status, name, email, phone, script, page, locale, one column per other variable, admin_note. Cells that start with = + - @ are prefixed with an apostrophe. At most 10000 rows. security: [{ adminToken: [] }] parameters: - { $ref: '#/components/parameters/Workspace' } - { in: query, name: status, schema: { $ref: '#/components/schemas/LeadStatus' } } - { in: query, name: scriptId, schema: { type: string } } - { in: query, name: from, schema: { type: string, format: date-time } } - { in: query, name: to, schema: { type: string, format: date-time } } responses: '200': { description: CSV file, content: { text/csv: { schema: { type: string } } } } '402': { description: '`plan_feature_lead_export`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/leads/settings: get: tags: [Leads] operationId: adminLeadSettings summary: Lead retention and plan features (tickets permission) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: Settings, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LeadSettings' } } } } } } put: tags: [Leads] operationId: adminUpdateLeadSettings summary: Set lead retention (workspace_admin). A daily job deletes older leads security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: required: true content: application/json: schema: { type: object, required: [retentionDays], properties: { retentionDays: { type: integer, enum: [30, 90, 365, 0], description: '0 = keep forever (Premium)' } } } responses: '200': { description: Settings, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LeadSettings' } } } } } } '400': { description: '`lead_retention_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '402': { description: '`plan_limit_lead_retention` (free plan: up to 30 days)', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/leads/{id}: get: tags: [Leads] operationId: adminGetLead summary: One lead security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { description: Lead, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Lead' } } } } } } '404': { $ref: '#/components/responses/Error' } put: tags: [Leads] operationId: adminUpdateLead summary: Change the status or the internal note security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: required: true content: application/json: schema: { type: object, properties: { status: { $ref: '#/components/schemas/LeadStatus' }, note: { type: string, maxLength: 2000, description: 'Empty clears it' } } } responses: '200': { description: Lead, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Lead' } } } } } } '400': { $ref: '#/components/responses/Error' } '404': { $ref: '#/components/responses/Error' } delete: tags: [Leads] operationId: adminDeleteLead summary: Delete a lead immediately (privacy request) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { description: Deleted, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: object, properties: { deleted: { const: true } } } } } } } } '404': { $ref: '#/components/responses/Error' } /v1/admin/live/settings: get: tags: [Live chat (agent)] operationId: liveGetSettings summary: Live chat settings (live_chat) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: '`LiveSettings`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveSettings' } } } } } } put: tags: [Live chat (agent)] operationId: liveUpdateSettings summary: Turn live chat on or off, set the queue wait (live_chat_admin) description: Turning it on for the first time creates one department. security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: content: application/json: schema: type: object properties: enabled: { type: boolean } waitSeconds: { type: integer, minimum: 30, maximum: 600 } prechat: { type: array, maxItems: 3, items: { $ref: '#/components/schemas/PrechatQuestion' }, description: 'Questions before connecting to an agent (Premium; `plan_feature_prechat`, `prechat_invalid`)' } responses: '200': { description: '`LiveSettings`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveSettings' } } } } } } '400': { description: '`live_wait_invalid`, `field_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '403': { $ref: '#/components/responses/Error' } /v1/admin/live/departments: get: tags: [Live chat (agent)] operationId: liveListDepartments summary: Departments with business hours (live_chat) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: '`LiveDepartment[]`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/LiveDepartment' } } } } } } } post: tags: [Live chat (agent)] operationId: liveCreateDepartment summary: Add a department (live_chat_admin). Free plan - one department security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveDepartmentInput' } responses: '201': { description: '`LiveDepartment`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveDepartment' } } } } } } '400': { description: '`field_missing`, `live_timezone_invalid`, `live_hours_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '402': { description: '`plan_limit_departments`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/departments/{id}: put: tags: [Live chat (agent)] operationId: liveUpdateDepartment summary: Update a department, its time zone, weekly hours and holidays (live_chat_admin) description: Send only the fields to change. `schedule` and `holidays` replace the whole list. security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveDepartmentInput' } responses: '200': { description: '`LiveDepartment`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveDepartment' } } } } } } '400': { $ref: '#/components/responses/Error' } '404': { description: '`live_department_not_found`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } delete: tags: [Live chat (agent)] operationId: liveDeleteDepartment summary: Delete a department (live_chat_admin). Refused while it has open chats security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { $ref: '#/components/responses/Deleted' } '409': { description: '`live_department_busy`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/agents: get: tags: [Live chat (agent)] operationId: liveListAgents summary: Workspace members with their agent settings, status and presence (live_chat) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: '`LiveAgent[]`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/LiveAgent' } } } } } } } /v1/admin/live/agents/{adminId}: put: tags: [Live chat (agent)] operationId: liveUpdateAgent summary: Make a member an agent, set departments and max concurrent chats (live_chat_admin) description: | Agent seats are separate from dashboard roles: any member (even a viewer) can be an agent, and only agents use seats (Free 1, Premium 5). `departments: []` means all departments. After a downgrade, the earliest agents keep their seats (`seated: true`); the others cannot claim chats or go available. security: [{ adminToken: [] }] parameters: - $ref: '#/components/parameters/Workspace' - { in: path, name: adminId, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: enabled: { type: boolean } departments: { type: array, items: { type: string }, maxItems: 50 } maxChats: { type: integer, minimum: 1, maximum: 20, default: 3 } displayName: { type: string, maxLength: 40, description: 'Shown to customers. Default: the first word of the profile name' } responses: '200': { description: '`LiveAgent`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveAgent' } } } } } } '400': { description: '`live_max_chats_invalid`, `field_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '402': { description: '`plan_limit_agents`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '404': { description: '`member_not_found`, `live_department_not_found`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/me: get: tags: [Live chat (agent)] operationId: liveMe summary: My agent settings, status and presence security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: '`LiveMe`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveMe' } } } } } } /v1/admin/live/me/status: put: tags: [Live chat (agent)] operationId: liveSetStatus summary: Set my status (available / away) description: '`available` needs an agent seat (`live_not_agent`, `plan_limit_agents`). `away` is always allowed.' security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: required: true content: application/json: schema: { type: object, required: [status], properties: { status: { type: string, enum: [available, away] } } } responses: '200': { description: '`LiveMe`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveMe' } } } } } } '402': { $ref: '#/components/responses/Error' } '403': { $ref: '#/components/responses/Error' } /v1/admin/live/presence: post: tags: [Live chat (agent)] operationId: livePresence summary: Device heartbeat (every 25 s; the lease expires after 75 s) description: | An agent is online while at least one device lease is valid. An open agent stream with a `deviceId` renews the lease by itself, so a tab with the stream open does not need to call this. Mobile apps without a stream call it every 25 s while in the foreground. security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveDeviceInput' } responses: '200': { description: '`{ ok: true, ttlSeconds: 75 }`' } '400': { $ref: '#/components/responses/Error' } /v1/admin/live/presence/offline: post: tags: [Live chat (agent)] operationId: liveOffline summary: Drop this device's lease now (sign-out, tab closed with sendBeacon) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveDeviceInput' } responses: '200': { $ref: '#/components/responses/Ok' } /v1/admin/live/canned-replies: get: tags: [Live chat (agent)] operationId: liveListCanned summary: Saved replies (live_chat) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: '`LiveCannedReply[]`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/LiveCannedReply' } } } } } } } post: tags: [Live chat (agent)] operationId: liveCreateCanned summary: Add a saved reply (live_chat_admin, Premium) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveCannedInput' } responses: '201': { description: '`LiveCannedReply`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveCannedReply' } } } } } } '400': { description: '`field_missing`, `field_invalid`, `live_canned_limit`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '402': { description: '`plan_feature_canned`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/canned-replies/{id}: put: tags: [Live chat (agent)] operationId: liveUpdateCanned summary: Update a saved reply (live_chat_admin, Premium) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveCannedInput' } responses: '200': { description: '`LiveCannedReply`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveCannedReply' } } } } } } '402': { $ref: '#/components/responses/Error' } '404': { description: '`live_canned_not_found`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } delete: tags: [Live chat (agent)] operationId: liveDeleteCanned summary: Delete a saved reply (live_chat_admin) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { $ref: '#/components/responses/Deleted' } /v1/admin/live/conversations: get: tags: [Live chat (agent)] operationId: liveListConversations summary: The inbox (live_chat) description: | Newest first. Closed and ticket conversations are listed only within the plan's transcript window (`transcriptDays`: Free 7, Premium 365). Page with `before=`. security: [{ adminToken: [] }] parameters: - $ref: '#/components/parameters/Workspace' - { in: query, name: state, schema: { type: string }, description: 'Comma list of queued, active, closed, ticket' } - { in: query, name: departmentId, schema: { type: string } } - { in: query, name: assignee, schema: { type: string }, description: '`me`, `none` or an admin id' } - { in: query, name: before, schema: { type: string } } - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } responses: '200': { description: '`LiveConversation[]`', content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/LiveConversation' } } } } } } } '400': { $ref: '#/components/responses/Error' } /v1/admin/live/conversations/{id}: get: tags: [Live chat (agent)] operationId: liveGetConversation summary: A conversation with the bot part and the first 200 events security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': description: Conversation and transcript content: application/json: schema: type: object properties: success: { const: true } data: type: object properties: conversation: { $ref: '#/components/schemas/LiveConversation' } bot: type: object properties: transcript: { type: array, items: { $ref: '#/components/schemas/TranscriptEntry' } } path: { type: array, items: { type: object }, description: Bot steps the user went through (from /events) } events: { type: array, items: { $ref: '#/components/schemas/LiveEvent' } } hasMore: { type: boolean, description: 'Load the rest with /events?after=' } '404': { description: '`live_conversation_not_found` (also when outside the plan window)', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } delete: tags: [Live chat (agent)] operationId: liveDeleteConversation summary: Delete a conversation and all its events (privacy request, live_chat_admin) description: An open chat is closed first, so the customer sees it end. A ticket created from it is kept; delete it separately if needed. security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { $ref: '#/components/responses/Deleted' } '403': { $ref: '#/components/responses/Error' } '404': { $ref: '#/components/responses/Error' } /v1/admin/live/conversations/{id}/events: get: tags: [Live chat (agent)] operationId: liveConversationEvents summary: Events after a sequence number (sync after reconnect), including internal notes security: [{ adminToken: [] }] parameters: - $ref: '#/components/parameters/Workspace' - $ref: '#/components/parameters/Id' - { in: query, name: after, schema: { type: integer, minimum: 0, default: 0 } } - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200, default: 200 } } responses: '200': description: Events content: application/json: schema: type: object properties: success: { const: true } data: type: object properties: conversation: { $ref: '#/components/schemas/LiveConversation' } events: { type: array, items: { $ref: '#/components/schemas/LiveEvent' } } hasMore: { type: boolean } /v1/admin/live/conversations/{id}/claim: post: tags: [Live chat (agent)] operationId: liveClaim summary: Take a queued chat (first claim wins) description: | Needs an agent seat and a department match. The agent's open-chat limit is reserved atomically before the claim; the loser of a race gets `409 live_already_claimed`. Claiming a chat you already hold returns it (safe retry). security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { $ref: '#/components/responses/LiveConversation' } '402': { description: '`plan_limit_agents` (not within the seats)', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '403': { description: '`live_not_agent`, `live_department_forbidden`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '409': { description: '`live_already_claimed`, `live_agent_full`, `live_conversation_closed`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/conversations/{id}/messages: post: tags: [Live chat (agent)] operationId: liveReply summary: Reply to the customer (assignee only) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveAgentMessageInput' } responses: '200': { $ref: '#/components/responses/LiveEventSaved' } '400': { $ref: '#/components/responses/Error' } '403': { description: '`live_not_assignee`, `live_not_agent`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '409': { description: '`live_conversation_closed`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/conversations/{id}/notes: post: tags: [Live chat (agent)] operationId: liveNote summary: Internal note, never sent to the customer (Premium) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LiveAgentMessageInput' } responses: '200': { $ref: '#/components/responses/LiveEventSaved' } '402': { description: '`plan_feature_notes`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/conversations/{id}/typing: post: tags: [Live chat (agent)] operationId: liveAgentTyping summary: Typing indicator for the customer (assignee only, not stored) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: content: application/json: schema: { type: object, properties: { on: { type: boolean, default: true } } } responses: '200': { $ref: '#/components/responses/Ok' } /v1/admin/live/conversations/{id}/transfer: post: tags: [Live chat (agent)] operationId: liveTransfer summary: Offer the chat to another agent or department description: | Exactly one of `toAgentId`, `toDepartmentId`. The current assignee keeps the chat (and keeps answering) until the target accepts. One pending transfer at a time; it expires after 2 minutes (`transfer.expired`). security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: required: true content: application/json: schema: type: object properties: toAgentId: { type: string } toDepartmentId: { type: string } note: { type: string, maxLength: 300, description: For the next agent only } responses: '200': { $ref: '#/components/responses/LiveConversation' } '400': { description: '`live_transfer_target_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '403': { description: '`live_not_assignee`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '409': { description: '`live_transfer_pending`, `live_conversation_closed`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/conversations/{id}/transfer/accept: post: tags: [Live chat (agent)] operationId: liveTransferAccept summary: Accept a transfer (the target agent, or an eligible agent of the target department) description: The accepting agent's open-chat limit is reserved first; only then the previous assignee is released. security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: { $ref: '#/components/requestBodies/LiveTransferRef' } responses: '200': { $ref: '#/components/responses/LiveConversation' } '403': { description: '`live_department_forbidden`, `live_not_agent`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } '409': { description: '`live_transfer_gone`, `live_agent_full`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/conversations/{id}/transfer/decline: post: tags: [Live chat (agent)] operationId: liveTransferDecline summary: Decline a transfer offered to me. The assignee does not change security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: { $ref: '#/components/requestBodies/LiveTransferRef' } responses: '200': { $ref: '#/components/responses/LiveConversation' } '403': { $ref: '#/components/responses/Error' } '409': { description: '`live_transfer_gone`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/conversations/{id}/transfer/cancel: post: tags: [Live chat (agent)] operationId: liveTransferCancel summary: Cancel my pending transfer (the requester, the assignee or a live_chat_admin) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] requestBody: { $ref: '#/components/requestBodies/LiveTransferRef' } responses: '200': { $ref: '#/components/responses/LiveConversation' } '409': { $ref: '#/components/responses/Error' } /v1/admin/live/conversations/{id}/close: post: tags: [Live chat (agent)] operationId: liveClose summary: Close the chat (assignee or live_chat_admin). Safe to repeat security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { $ref: '#/components/responses/LiveConversation' } '403': { $ref: '#/components/responses/Error' } /v1/admin/live/conversations/{id}/ticket: post: tags: [Live chat (agent)] operationId: liveToTicket summary: Turn the chat into a ticket, with the full transcript (assignee or live_chat_admin) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }, { $ref: '#/components/parameters/Id' }] responses: '200': { $ref: '#/components/responses/LiveConversation' } '403': { $ref: '#/components/responses/Error' } '409': { $ref: '#/components/responses/Error' } /v1/admin/live/stream-ticket: post: tags: [Live chat (agent)] operationId: liveAgentStreamTicket summary: Single-use ticket for the agent stream (live_chat) description: '`deviceId` (8-64 chars, stable per tab or device) makes the open stream renew the presence lease.' security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: content: application/json: schema: { $ref: '#/components/schemas/LiveDeviceInput' } responses: '200': { $ref: '#/components/responses/LiveStreamTicket' } /v1/admin/live/stream: get: tags: [Live chat (agent)] operationId: liveAgentStream security: [{}] summary: Server-Sent Events for the whole workspace inbox description: | `new EventSource('https://api.kavchat.com/v1/admin/live/stream?ticket=...')`. No Authorization header and no workspace header: the ticket carries both. Named events (see `LiveAgentStreamEvent`): * `ready` `{ tenantId, adminId, heartbeatSeconds }` * `conversation`: a `LiveConversation` whenever one is created or changes state, assignee or transfer * `event` (`LiveEvent` + `conversationId`): every stored event, including internal notes * `typing` `{ conversationId, author: { kind: "visitor" }, on }` * `presence` `{ adminId, status, online, activeChats }` * `transfer.offer` `{ conversationId, transferId, fromAgentName }` (only to the target agent) * `conversation.deleted` `{ conversationId }` * `bye` `{ reason }` (`unauthorized`: signed out or removed) No replay on this stream: after reconnecting, reload the inbox list and call `/conversations/{id}/events?after=` for the open conversation. The stream ends after one hour; the session and membership are re-checked every 5 minutes. parameters: - { in: query, name: ticket, required: true, schema: { type: string } } responses: '200': description: The event stream content: text/event-stream: schema: { $ref: '#/components/schemas/LiveAgentStreamEvent' } '401': { description: '`live_stream_ticket_invalid`, `unauthorized`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } /v1/admin/live/push/key: get: tags: [Live chat (agent)] operationId: livePushKey summary: VAPID public key for Web Push (null when the server has no keys) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: '`{ enabled: boolean, publicKey: string | null }`' } /v1/admin/live/push/subscriptions: post: tags: [Live chat (agent)] operationId: livePushSubscribe summary: Save this browser's PushSubscription (the JSON of subscription.toJSON()) description: | Available agents of the department get a notification when a chat is queued. The payload is `{ type: "live.queued", conversationId, departmentName, url }`, with no customer details or message text. At most 10 subscriptions per agent (the oldest is replaced). security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] requestBody: required: true content: application/json: schema: type: object required: [endpoint, keys] properties: endpoint: { type: string, format: uri, description: 'https, not an internal address' } keys: { type: object, required: [p256dh, auth], properties: { p256dh: { type: string }, auth: { type: string } } } responses: '201': { description: '`{ subscribed: true }`' } '400': { description: '`live_push_unavailable`, `live_push_invalid`', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } delete: tags: [Live chat (agent)] operationId: livePushUnsubscribe summary: Remove a subscription (`{ endpoint }`) security: [{ adminToken: [] }] parameters: [{ $ref: '#/components/parameters/Workspace' }] responses: '200': { description: '`{ subscribed: false }`' } components: securitySchemes: secretKey: type: http scheme: bearer description: '`sk_live_...`' identityToken: type: http scheme: bearer bearerFormat: JWT description: HS256 JWT signed with the secret key. See "Identifying your users". adminToken: type: http scheme: bearer bearerFormat: JWT description: Dashboard session token from /v1/auth/email/verify, /v1/auth/google or /v1/auth/passkey/verify. parameters: PublicKey: in: path name: publicKey required: true schema: { type: string, pattern: '^pk_(live|test)_[A-Za-z0-9_-]{10,64}$' } TicketId: in: path name: id required: true schema: { type: string } Id: in: path name: id required: true schema: { type: string } Workspace: in: header name: X-Kavchat-Workspace schema: { type: string } description: The workspace id. Optional when the admin belongs to one workspace only requestBodies: LiveRequest: required: true content: text/plain: schema: { $ref: '#/components/schemas/LiveRequestInput' } application/json: schema: { $ref: '#/components/schemas/LiveRequestInput' } LiveTransferRef: content: application/json: schema: type: object properties: transferId: { type: string, description: 'When sent, must match the pending transfer (protects against acting on a newer one)' } responses: Error: description: Error content: application/json: schema: { $ref: '#/components/schemas/Error' } Saved: description: Saved content: application/json: schema: type: object properties: success: { const: true } data: { type: object, properties: { saved: { const: true } } } Ok: description: Done content: application/json: schema: { type: object, properties: { success: { const: true }, data: { type: object, properties: { ok: { const: true } } } } } Deleted: description: Deleted content: application/json: schema: { type: object, properties: { success: { const: true }, data: { type: object, properties: { deleted: { const: true } } } } } LiveStreamTicket: description: Single-use stream ticket content: application/json: schema: type: object properties: success: { const: true } data: type: object properties: ticket: { type: string, description: 'lst_..., valid once within expiresIn seconds. Never log it' } expiresIn: { type: integer, const: 60 } LiveVisitorEventSaved: description: Stored (or the original, on a retry) content: application/json: schema: type: object properties: success: { const: true } data: type: object properties: event: { $ref: '#/components/schemas/LiveVisitorEvent' } duplicate: { type: boolean } LiveEventSaved: description: Stored (or the original, on a retry) content: application/json: schema: type: object properties: success: { const: true } data: type: object properties: event: { $ref: '#/components/schemas/LiveEvent' } duplicate: { type: boolean } LiveVisitorConversation: description: The conversation after the command content: application/json: schema: type: object properties: success: { const: true } data: { type: object, properties: { conversation: { $ref: '#/components/schemas/LiveVisitorConversation' } } } LiveConversation: description: The conversation after the command content: application/json: schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/LiveConversation' } } } schemas: AiScript: type: object description: | Present on scripts built from repeated AI answers (`ai` in the script list and details). `versions` and `sources` only in the script details. properties: state: { type: string, enum: [suggested, approved, rejected] } locale: { type: string, description: The conversation language of the questions } defaultMissing: type: array items: { type: string, enum: [name, answer] } description: A suggestion from an additional language whose default-language name or answer is not written yet. Non-empty blocks approval answer: { type: string, description: 'The answer text in the draft (the root step). A suggestion from an additional language: the answer in that language' } questions: { type: array, items: { type: string }, description: How customers asked (up to 10) } askedCount: { type: integer, description: Conversations that got an AI answer when the suggestion was made } suggestedAt: { type: string, format: date-time } version: { type: integer, description: 0 until the first approval } approvedAt: { type: [string, 'null'], format: date-time } approvedBy: { type: [object, 'null'], properties: { id: { type: string }, name: { type: string } } } savedAnswers: { type: integer, description: Times the approved script answered a typed question instead of an AI call } review: type: [object, 'null'] description: A website page the answer relied on changed after approval. The approved version keeps answering properties: reason: { const: site_changed } at: { type: string, format: date-time } urls: { type: array, items: { type: string } } sources: { type: array, items: { type: string }, description: Website pages checked for changes } versions: type: array description: Approved versions, newest first items: type: object properties: version: { type: integer } approvedAt: { type: string, format: date-time } approvedBy: { type: object, properties: { id: { type: string }, name: { type: string } } } restoredFrom: { type: [integer, 'null'] } content: { type: object } Error: type: object required: [success, code, message] properties: success: { const: false } code: { type: string, examples: [invalid_key, identity_invalid, too_many_requests] } message: { type: string } details: {} Locale: type: string enum: [he, en, ar, ru, es, fr, de, pt, it, tr, th] Audience: type: string enum: ['off', testers, all] ButtonAction: oneOf: - { type: object, required: [type, stepId], properties: { type: { const: step }, stepId: { type: string } } } - type: object required: [type, stepId] properties: type: { const: goto } stepId: { type: string } scriptId: { type: string, description: Present when jumping to another script } - { type: object, required: [type], properties: { type: { const: contact } } } - type: object required: [type, screen] properties: type: { const: screen } screen: { type: string, description: 'A screen name your app understands, e.g. "orders"' } targetId: { type: string } - { type: object, required: [type, url], properties: { type: { const: link }, url: { type: string } } } - { type: object, required: [type, scriptId], properties: { type: { const: script }, scriptId: { type: string } } } Button: type: object required: [id, label, action] properties: id: { type: string } label: { type: string, description: 'Up to 25 characters for a button, 100 for a question' } style: type: string enum: [question] description: | `question`: a full question shown as a wide row with a chevron (consecutive questions share one card). Absent: a regular short button. Up to 20 buttons and 20 questions per step, counted separately. Clients that do not know `style` show the question as a regular button. action: oneOf: - $ref: '#/components/schemas/ButtonAction' - type: 'null' Step: type: object required: [id, text, keywords, buttons] description: | A step without `type` (or `message`) is text and buttons. `question`, `condition` and `lead` are described in docs/collect-and-conditions.md. In the client release their `text` and `buttons` are a fallback for SDKs that do not know the type (the text and a "contact the team" button); engines that know the type ignore those buttons. properties: id: { type: string } text: { type: string, description: '`{name}` is replaced with ", " for identified users, or removed. `{{variable}}` is replaced with a collected value' } imageUrl: { type: string } keywords: { type: array, items: { type: string } } buttons: { type: array, items: { $ref: '#/components/schemas/Button' } } type: { type: string, enum: [message, question, condition, lead] } question: { $ref: '#/components/schemas/Question' } condition: { $ref: '#/components/schemas/Condition' } lead: { $ref: '#/components/schemas/LeadStep' } BotNext: oneOf: - { type: object, required: [type, stepId], properties: { type: { const: step }, stepId: { type: string } } } - { type: object, required: [type, stepId], properties: { type: { const: goto }, stepId: { type: string }, scriptId: { type: string } } } - { type: object, required: [type, scriptId], properties: { type: { const: script }, scriptId: { type: string } } } - { type: object, required: [type], properties: { type: { const: contact } } } - { type: object, required: [type], properties: { type: { const: live } } } Question: type: object required: [kind, variable, next] properties: kind: { type: string, enum: [name, phone, email, number, text, choice] } variable: { type: string, pattern: '^[a-z][a-z0-9_]{0,31}$' } optional: { type: boolean } skipIfKnown: { type: boolean, default: true } retryText: { type: string, maxLength: 500 } min: { type: number, description: 'number: minimum value. text: minimum length. Not used by choice' } max: { type: number, description: 'number: maximum value. text: maximum length. Not used by choice' } options: type: array minItems: 2 maxItems: 8 items: { $ref: '#/components/schemas/QuestionOption' } description: 'choice only: one chip per option (`opt:`). Labels are unique, case-insensitive' next: { oneOf: [{ $ref: '#/components/schemas/BotNext' }, { type: 'null' }] } QuestionOption: type: object required: [label] properties: label: { type: string, minLength: 1, maxLength: 40, description: The chip text } value: { type: string, maxLength: 100, description: 'Stored in the variable. Missing or empty: the label' } ConditionClause: type: object required: [field, op] properties: field: type: string description: '`var.`, `service.agentsAvailable`, `service.withinBusinessHours`, `visitor.identified`, `visitor.name`, `visitor.email`, `visitor.phone`, `context.locale`, `context.page`' op: { type: string, enum: [equals, not_equals, contains, not_contains, is_empty, is_filled, gt, gte, lt, lte, one_of, is_true, is_false] } value: oneOf: - { type: string, maxLength: 200 } - { type: array, maxItems: 20, items: { type: string, maxLength: 200 } } Condition: type: object required: [rules, else] description: First rule whose clauses all hold wins, otherwise `else`. Premium. properties: rules: type: array maxItems: 10 items: type: object required: [id, when, next] properties: id: { type: string } when: { type: array, maxItems: 5, items: { $ref: '#/components/schemas/ConditionClause' } } next: { oneOf: [{ $ref: '#/components/schemas/BotNext' }, { type: 'null' }] } else: { oneOf: [{ $ref: '#/components/schemas/BotNext' }, { type: 'null' }] } LeadStep: type: object required: [next] properties: consent: type: string maxLength: 500 description: 'confirm false: a small muted line under the last question before the lead (or before sending), and the lead is sent at once. confirm true: shown with Send and No thanks chips' confirm: { type: boolean, default: false, description: 'true: a separate consent step with Send and No thanks chips' } doneText: { type: string, maxLength: 500 } variables: { type: array, items: { type: string }, description: 'Which variables go into the lead; empty = all' } next: { oneOf: [{ $ref: '#/components/schemas/BotNext' }, { type: 'null' }] } Variables: type: object description: 'Collected values by variable name ([a-z][a-z0-9_]{0,31}), at most 30, each up to 500 characters. Personal data' additionalProperties: { type: string } LeadStatus: type: string enum: [new, contacted, won, lost] LeadInput: type: object required: [clientId, variables] properties: clientId: { type: string, pattern: '^[A-Za-z0-9_-]{8,64}$' } sessionId: { type: string, pattern: '^[A-Za-z0-9_-]{8,64}$' } scriptId: { type: string } stepId: { type: string } variables: { $ref: '#/components/schemas/Variables' } locale: { type: string } consent: { type: [string, 'null'], description: The consent line as shown } page: { type: string, description: 'Page URL or app screen. Stored without query string and fragment' } releaseVersion: { type: integer } transcript: { type: array, maxItems: 200, items: { $ref: '#/components/schemas/TranscriptEntry' } } Lead: type: object properties: id: { type: string } number: { type: integer } status: { $ref: '#/components/schemas/LeadStatus' } variables: { $ref: '#/components/schemas/Variables' } name: { type: [string, 'null'] } email: { type: [string, 'null'] } phone: { type: [string, 'null'], description: E.164 } scriptId: { type: [string, 'null'] } scriptName: { type: [string, 'null'] } stepId: { type: [string, 'null'] } page: { type: [string, 'null'] } locale: { type: [string, 'null'] } consent: { type: [string, 'null'] } sessionId: { type: [string, 'null'] } userId: { type: [string, 'null'] } note: { type: [string, 'null'] } transcript: { type: array, items: { $ref: '#/components/schemas/TranscriptEntry' } } webhook: { type: [object, 'null'] } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } LeadSettings: type: object properties: retentionDays: { type: integer, description: 'Effective retention in days; 0 = forever' } maxRetentionDays: { type: [integer, 'null'], description: 'Plan cap; null = unlimited' } export: { type: boolean } webhook: { type: boolean, description: 'Premium: leads also go to the workspace webhook as `lead.created`' } ScriptContent: type: object required: [name, keywords, rootStepId, steps] properties: name: { type: string } keywords: { type: array, items: { type: string } } contactTopicId: { type: string } menu: const: false description: >- `false`: answer questions only. The script is not shown in the main menu (nor in the "more topics" list), but a typed question still reaches it. Missing: shown in the menu. Only `false` is stored. Scripts created from AI suggestions start with `false`. rootStepId: { type: string } steps: { type: array, items: { $ref: '#/components/schemas/Step' } } ReleaseScript: type: object required: [id, kind, sortOrder, content] properties: id: { type: string } kind: { type: string, enum: [opening, topic] } sortOrder: { type: integer } content: { $ref: '#/components/schemas/ScriptContent' } ClientRelease: oneOf: - type: object required: [enabled] properties: enabled: { const: false } - type: object required: [enabled, version, locale, settings, openingScriptId, scripts] properties: enabled: { const: true } version: { type: integer } locale: allOf: [{ $ref: '#/components/schemas/Locale' }] description: 'The content language of this response: drives keyword matching. Send it back as `locale` on events, tickets, leads and AI answers' locales: type: array items: { $ref: '#/components/schemas/Locale' } description: 'Available content languages, the default first. Missing on older servers (one language)' settings: type: object properties: botName: { type: string } avatarText: { type: string } avatarImageUrl: { type: [string, 'null'] } subtitle: { type: string } primaryColor: { type: string, pattern: '^#[0-9a-fA-F]{6}$' } position: { type: string, enum: [start, end] } contactTopics: type: array items: { type: object, properties: { id: { type: string }, name: { type: string } } } attention: { $ref: '#/components/schemas/Attention' } theme: { $ref: '#/components/schemas/Theme' } removeBranding: type: boolean description: >- When false (the default on every plan), show the "Powered by Kavchat by maxsave" badge (`widget.poweredBy`) linking to https://kavchat.com. True only when the workspace is on Premium and chose to hide it. defaultCountry: { type: string, description: 'Workspace default country (ISO 3166 alpha-2) for phone answers without a country code' } callingCode: { type: string, description: 'Its calling code without +, e.g. "972"' } openingScriptId: { type: string } scripts: { type: array, items: { $ref: '#/components/schemas/ReleaseScript' } } liveChat: { $ref: '#/components/schemas/LiveChatRelease' } EventsInput: type: object required: [sessionId, releaseVersion, startedAt] properties: sessionId: { type: string, pattern: '^[A-Za-z0-9_-]{8,64}$', description: Random id generated on the device } releaseVersion: { type: integer, minimum: 0 } startedAt: { type: string, format: date-time } outcome: { type: string, enum: [open, answered, contact, abandoned], default: open } path: type: array maxItems: 200 items: type: object required: [scriptId, stepId, via, at] properties: scriptId: { type: string } stepId: { type: string } via: { type: string, enum: [start, button, text, back, home] } at: { type: string, format: date-time } unmatched: type: array maxItems: 20 items: type: object required: [text, at] properties: text: { type: string, maxLength: 300 } at: { type: string, format: date-time } locale: { $ref: '#/components/schemas/Locale', description: 'The release `locale` of the conversation. Missing: the default language' } TranscriptEntry: type: object required: [from, text] properties: from: { type: string, enum: [bot, user, agent], description: '`agent` appears in tickets created from a live chat. Show unknown values like bot' } text: { type: string, maxLength: 1000 } name: { type: string, description: The agent's display name (agent entries only) } stepId: { type: string } at: { type: string, format: date-time } TicketInput: type: object required: [message] properties: message: { type: string, maxLength: 2000 } name: { type: string, maxLength: 100 } email: { type: string, format: email } phone: { type: string } topicId: { type: string } scriptId: { type: string } sessionId: { type: string } transcript: { type: array, maxItems: 200, items: { $ref: '#/components/schemas/TranscriptEntry' } } variables: { $ref: '#/components/schemas/Variables' } locale: { $ref: '#/components/schemas/Locale', description: 'The release `locale` of the conversation. Missing: the default language' } TicketStatus: type: string enum: [open, closed] Ticket: type: object properties: id: { type: string } number: { type: integer, description: 'Internal running number in the workspace (1, 2, 3...)' } ref: { type: string, description: 'The request number shown to the requester, k--<7 random digits>-, for example k-22-7489392-1' } status: { $ref: '#/components/schemas/TicketStatus' } name: { type: [string, 'null'] } email: { type: [string, 'null'] } phone: { type: [string, 'null'], description: E.164 } userId: { type: [string, 'null'], description: 'The sub claim of the identity token, when the user was identified' } topicId: { type: [string, 'null'] } topicName: { type: [string, 'null'] } scriptId: { type: [string, 'null'] } sessionId: { type: [string, 'null'] } message: { type: string } transcript: { type: array, items: { $ref: '#/components/schemas/TranscriptEntry' } } variables: { $ref: '#/components/schemas/Variables' } source: { type: string, enum: [form, live], description: '`live`: a live chat that became a ticket (nobody answered in time, the visitor left a message, or the agent converted it)' } liveConversationId: { type: [string, 'null'] } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } Settings: type: object properties: enabled: { type: boolean } audience: { $ref: '#/components/schemas/Audience' } testerPhones: { type: array, items: { type: string } } botName: { type: string } avatarText: { type: string } avatarImageUrl: { type: [string, 'null'] } subtitle: { type: string } primaryColor: { type: string } position: { type: string, enum: [start, end] } contactTopics: type: array items: { type: object, properties: { id: { type: string }, name: { type: string } } } attention: allOf: - { $ref: '#/components/schemas/Attention' } - type: object properties: text: { type: string, maxLength: 40, description: 'As saved. Empty means the default text in the workspace content language' } defaultText: { type: string, description: The default text in the workspace content language } theme: { $ref: '#/components/schemas/Theme' } hideBranding: type: boolean default: false description: >- Hide the Kavchat badge in the chat. Premium only: setting true on the free plan fails with 402 `plan_feature_branding`. After a downgrade the value is kept but the badge is shown again. Theme: type: object description: >- The look of the chat window and the launcher, on every platform. The defaults are the look before the field existed, so a release without `theme` (or a client that ignores it) looks the same. Unknown enum values must be treated as the default. Send a partial object to update only some fields. properties: mode: { type: string, enum: [light, dark, auto], default: light, description: '`auto` follows the device setting (prefers-color-scheme)' } radius: { type: string, enum: [square, rounded, pill], default: rounded, description: 'Corners of the window, bubbles, buttons and fields' } headerColor: { type: string, pattern: '^(#[0-9a-fA-F]{6})?$', default: '', description: 'Header background. Empty means primaryColor' } backgroundColor: { type: string, pattern: '^(#[0-9a-fA-F]{6})?$', default: '', description: 'Chat background in light mode. Empty means the default. Dark mode always uses its own palette' } launcherSize: { type: string, enum: [small, medium, large], default: medium, description: '48, 56 or 64 px' } launcherIcon: { type: string, enum: [chat, help, message, image, face], default: chat, description: '`face`: a white chat bubble with two eyes and a smile in the accent color, blinking now and then' } launcherImageUrl: { type: string, maxLength: 500, default: '', description: 'https only. Required when launcherIcon is image; empty in the client release otherwise' } font: { type: string, enum: [system, inherit, serif, rounded], default: system, description: '`inherit`: the host page font (web only; apps use the system font)' } Attention: type: object description: >- The attention grabber next to the launcher. In the client release `text` is final (already localized). Clients that do not know the field ignore it; unknown `style` values must be treated as `none`. properties: style: type: string enum: [none, bubble, arc, talk, route] default: bubble description: >- `bubble`: a small speech bubble with a close button. `arc`: the text curved above the round launcher. `talk`: the launcher stretches into a pill with a face icon, the text and a call to action, then collapses back. `route`: a line drawn up from the launcher with stations for the first options of the opening menu (the topic scripts by sortOrder); tapping a station opens the chat and selects that option. With no options it falls back to `bubble`. text: { type: string, maxLength: 40, description: Plain text, one line, up to 40 characters } emoji: { type: string, default: "\U0001F44B", description: One emoji or empty } showAfterSeconds: { type: integer, minimum: 0, maximum: 60, default: 3 } showOnce: { type: boolean, default: true, description: At most once per visitor per day } hideOnMobile: { type: boolean, default: false } stations: { type: integer, minimum: 1, maximum: 3, default: 3, description: '`route` only: how many stations to show (fewer when there are fewer options; at most 2 on phones). Missing in old servers: 3' } Workspace: type: object properties: id: { type: string } name: { type: string } locale: { $ref: '#/components/schemas/Locale' } defaultCountry: { type: string } plan: { type: string } publicKey: { type: string } secretKeyHint: { type: string } webhookUrl: { type: [string, 'null'] } ReleaseSummary: type: object properties: version: { type: integer } note: { type: string } restoredFrom: { type: integer } publishedAt: { type: string, format: date-time } scripts: type: array items: { type: object, properties: { id: { type: string }, kind: { type: string }, name: { type: string } } } Report: type: object properties: from: { type: string, format: date-time } to: { type: string, format: date-time } totals: type: object properties: sessions: { type: integer } open: { type: integer } answered: { type: integer } contact: { type: integer } abandoned: { type: integer } byScript: { type: array, items: { type: object } } dropOff: { type: array, items: { type: object } } unmatched: type: array items: type: object properties: groupKey: { type: string } sample: { type: string } count: { type: integer } lastAt: { type: string, format: date-time } # ---------- Live chat ---------- LiveClientId: type: string pattern: '^[A-Za-z0-9_-]{8,64}$' description: Generated by the client (for example a UUID without braces). The same id on a retry returns the original result LiveDepartmentRef: type: object properties: id: { type: string } name: { type: string } LiveChatRelease: type: object description: | In the release. `enabled`: show "talk to a representative" next to the contact form. `open` is a hint for the moment of loading only; ask `/live/availability` before showing the option. Changes of `open` change the ETag. required: [enabled, open, departments] properties: enabled: { type: boolean } open: { type: boolean } departments: { type: array, items: { $ref: '#/components/schemas/LiveDepartmentRef' } } prechat: { type: array, items: { $ref: '#/components/schemas/PrechatQuestion' }, description: 'Premium. Ask these before a live handoff (engine: collectLive)' } PrechatQuestion: type: object required: [kind, optional] properties: kind: { type: string, enum: [name, email, phone], description: Also the variable name } optional: { type: boolean } LiveUnavailableReason: type: string enum: [disabled, closed, no_agents, quota] description: | `disabled`: live chat is off (or the bot is off for this user). `closed`: outside business hours or a holiday. `no_agents`: no agent is available with room for another chat. `quota`: the monthly conversations quota is used up. LiveAvailability: type: object required: [enabled, open, reason, waitSeconds, departments] properties: enabled: { type: boolean } open: { type: boolean } reason: { oneOf: [{ $ref: '#/components/schemas/LiveUnavailableReason' }, { type: 'null' }] } waitSeconds: { type: integer, description: How long a queued chat waits before it becomes a ticket } departments: type: array items: type: object properties: id: { type: string } name: { type: string } open: { type: boolean } reason: { oneOf: [{ $ref: '#/components/schemas/LiveUnavailableReason' }, { type: 'null' }] } nextOpenAt: { type: [string, 'null'], format: date-time, description: When business hours start again (within 8 days) } LiveRequestInput: type: object required: [requestId] properties: requestId: { $ref: '#/components/schemas/LiveClientId' } credential: { type: string, description: The credential of a chat this client already holds } identityToken: { type: string, description: 'The identity JWT (same as Authorization: Bearer), sent in the body to avoid preflight' } departmentId: { type: string, description: 'Default: the first department' } sessionId: { type: string, pattern: '^[A-Za-z0-9_-]{8,64}$', description: The bot session id (links the bot path and counts the conversation once) } name: { type: string, maxLength: 100 } email: { type: string, format: email } phone: { type: string } topicId: { type: string } message: { type: string, maxLength: 2000, description: Optional first message } messageClientId: { $ref: '#/components/schemas/LiveClientId' } page: { type: string, description: 'Current page URL or app screen name. Stored without query string and fragment' } transcript: { type: array, maxItems: 200, items: { $ref: '#/components/schemas/TranscriptEntry' }, description: The bot part of the conversation } variables: { $ref: '#/components/schemas/Variables' } LiveCredentialInput: type: object required: [credential] properties: credential: { type: string, pattern: '^lvc_[A-Za-z0-9_-]{20,80}$' } LiveVisitorMessageInput: allOf: - $ref: '#/components/schemas/LiveCredentialInput' - type: object required: [clientId, text] properties: clientId: { $ref: '#/components/schemas/LiveClientId' } text: { type: string, minLength: 1, maxLength: 2000 } LiveTypingInput: allOf: - $ref: '#/components/schemas/LiveCredentialInput' - type: object properties: on: { type: boolean, default: true } LiveSyncInput: allOf: - $ref: '#/components/schemas/LiveCredentialInput' - type: object properties: after: { type: integer, minimum: 0, default: 0, description: The highest seq already shown } wait: { type: integer, minimum: 0, maximum: 25, default: 0, description: Long-poll seconds when nothing is new } LiveLeaveMessageInput: allOf: - $ref: '#/components/schemas/LiveCredentialInput' - type: object properties: text: { type: string, maxLength: 2000 } clientId: { $ref: '#/components/schemas/LiveClientId' } LiveAgentMessageInput: type: object required: [clientId, text] properties: clientId: { $ref: '#/components/schemas/LiveClientId' } text: { type: string, minLength: 1, maxLength: 2000 } LiveDeviceInput: type: object properties: deviceId: { type: string, pattern: '^[A-Za-z0-9_-]{8,64}$' } LiveState: type: string enum: [queued, active, closed, ticket] description: | `queued`: waiting for an agent. `active`: an agent is answering. `closed`: ended (`closeReason`). `ticket`: became a regular ticket (`ticket.reason`: timeout, visitor, agent, quota). closed and ticket are final. LiveTicketRef: type: object properties: number: { type: [integer, 'null'], description: 'null for a moment while the ticket is being created' } ref: { type: [string, 'null'], description: 'The full request number to show, for example k-22-7489392-1. number holds its 7 random digits' } reason: { type: string, enum: [timeout, visitor, agent, quota] } LiveVisitorConversation: type: object required: [id, state, lastSeq, credentialExpiresAt] properties: id: { type: string } state: { $ref: '#/components/schemas/LiveState' } departmentId: { type: string } agent: { type: [object, 'null'], properties: { name: { type: string } }, description: The agent answering now } queueDeadline: { type: string, format: date-time, description: 'When a queued chat becomes a ticket' } closeReason: { type: [string, 'null'], enum: [agent, visitor, expired, null] } ticket: { oneOf: [{ $ref: '#/components/schemas/LiveTicketRef' }, { type: 'null' }] } lastSeq: { type: integer, description: 'The last seq allocated in the conversation (may belong to an event the visitor does not see)' } credentialExpiresAt: { type: string, format: date-time } LiveAccepted: type: object required: [accepted, conversation, credential, credentialExpiresAt, waitSeconds] properties: accepted: { const: true } conversation: { $ref: '#/components/schemas/LiveVisitorConversation' } credential: { type: string, description: 'lvc_... Secret. Required by every visitor command. Never put it in a URL' } credentialExpiresAt: { type: string, format: date-time } waitSeconds: { type: integer } LiveUnavailable: type: object required: [accepted, reason, fallback] properties: accepted: { const: false } reason: { $ref: '#/components/schemas/LiveUnavailableReason' } fallback: { const: ticket } LiveEventType: type: string enum: - conversation.queued - message - note - agent.joined - transfer.requested - transfer.accepted - transfer.declined - transfer.canceled - transfer.expired - conversation.closed - conversation.ticket description: | Visitor-visible: conversation.queued `{ departmentId, deadline }`, message (`text`), agent.joined `{ agent: { name } }`, conversation.closed `{ reason }`, conversation.ticket `{ reason, ticketNumber }`. Agents only: note (`text`), transfer.requested `{ transferId, toAgentId, toDepartmentId, note }`, transfer.accepted `{ transferId, fromAgentId }`, transfer.declined / canceled / expired `{ transferId }`. Unknown types may be added later: ignore them. LiveAuthor: type: object required: [kind] properties: kind: { type: string, enum: [visitor, agent, system] } id: { type: string, description: Admin id of the agent (agent view only) } name: { type: string } LiveVisitorEvent: type: object required: [seq, type, author, at] properties: seq: { type: integer, description: 'Increasing per conversation. Order and dedupe by it; gaps are normal' } type: { $ref: '#/components/schemas/LiveEventType' } author: { type: object, properties: { kind: { type: string, enum: [visitor, agent, system] }, name: { type: string } } } text: { type: [string, 'null'] } data: { type: object } clientId: { type: [string, 'null'], description: 'Only on the visitor''s own messages: match it to the message shown as "sending"' } at: { type: string, format: date-time } LiveEvent: type: object required: [seq, type, visibility, author, at] properties: seq: { type: integer } type: { $ref: '#/components/schemas/LiveEventType' } visibility: { type: string, enum: [all, agents] } author: { $ref: '#/components/schemas/LiveAuthor' } text: { type: [string, 'null'] } data: { type: object } clientId: { type: [string, 'null'] } at: { type: string, format: date-time } LiveVisitorSync: type: object properties: conversation: { $ref: '#/components/schemas/LiveVisitorConversation' } events: { type: array, items: { $ref: '#/components/schemas/LiveVisitorEvent' } } hasMore: { type: boolean } LiveConversation: type: object properties: id: { type: string } state: { $ref: '#/components/schemas/LiveState' } departmentId: { type: string } visitor: type: object properties: userId: { type: [string, 'null'] } name: { type: [string, 'null'] } email: { type: [string, 'null'] } phone: { type: [string, 'null'], description: E.164 } verified: { type: boolean, description: 'true: from an identity token signed by the customer''s server. false: typed by the visitor' } sessionId: { type: [string, 'null'] } topicId: { type: [string, 'null'] } topicName: { type: [string, 'null'] } page: { type: [string, 'null'], description: Without query string and fragment } assignee: { type: [object, 'null'], properties: { id: { type: string }, name: { type: string } } } transfer: type: [object, 'null'] properties: id: { type: string } fromAgentId: { type: string } fromAgentName: { type: [string, 'null'] } toAgentId: { type: [string, 'null'] } toDepartmentId: { type: [string, 'null'] } note: { type: [string, 'null'] } at: { type: string, format: date-time } expiresAt: { type: string, format: date-time } queuedAt: { type: string, format: date-time } queueDeadline: { type: string, format: date-time } assignedAt: { type: [string, 'null'], format: date-time } closedAt: { type: [string, 'null'], format: date-time } closeReason: { type: [string, 'null'], enum: [agent, visitor, expired, null] } ticket: { type: [object, 'null'], properties: { id: { type: [string, 'null'] }, number: { type: [integer, 'null'] }, reason: { type: string } } } lastSeq: { type: integer } lastMessageAt: { type: [string, 'null'], format: date-time } lastActivityAt: { type: string, format: date-time } createdAt: { type: string, format: date-time } LiveSettings: type: object properties: enabled: { type: boolean } waitSeconds: { type: integer } prechat: { type: array, items: { $ref: '#/components/schemas/PrechatQuestion' } } updatedAt: { type: [string, 'null'], format: date-time } LiveHours: type: object required: [from, to] properties: from: { type: string, pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$' } to: { type: string, pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$|^24:00$', description: 'Exclusive, after from. Split a night shift into two days' } LiveDepartmentInput: type: object properties: name: { type: string, maxLength: 40 } timezone: { type: string, description: 'IANA time zone, for example Asia/Jerusalem', default: UTC } hoursEnabled: { type: boolean, description: 'false: open around the clock (when an agent is available)' } schedule: type: array maxItems: 28 items: allOf: - type: object required: [day] properties: { day: { type: integer, minimum: 0, maximum: 6, description: '0 = Sunday' } } - $ref: '#/components/schemas/LiveHours' holidays: type: array maxItems: 100 description: Dates in the department's time zone. No intervals = closed all day; with intervals = these hours that day items: type: object required: [date] properties: date: { type: string, format: date } name: { type: string, maxLength: 60 } intervals: { type: array, maxItems: 4, items: { $ref: '#/components/schemas/LiveHours' } } sortOrder: { type: integer, minimum: 0, maximum: 1000 } LiveDepartment: allOf: - $ref: '#/components/schemas/LiveDepartmentInput' - type: object properties: id: { type: string } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } LiveAgentSettings: oneOf: - type: object properties: { enabled: { const: false } } - type: object properties: enabled: { const: true } departments: { type: array, items: { type: string }, description: Empty = all departments } maxChats: { type: integer } displayName: { type: string } since: { type: string, format: date-time } seated: { type: boolean, description: 'Within the plan''s agent seats (the earliest agents first)' } LiveAgent: type: object properties: adminId: { type: string } name: { type: string } email: { type: string } role: { type: string, enum: [owner, publisher, editor, viewer] } agent: { $ref: '#/components/schemas/LiveAgentSettings' } status: { type: string, enum: [available, away] } online: { type: boolean, description: At least one device lease is valid } activeChats: { type: integer } LiveMe: type: object properties: adminId: { type: string } agent: { $ref: '#/components/schemas/LiveAgentSettings' } status: { type: string, enum: [available, away] } online: { type: boolean } activeChats: { type: integer } LiveCannedInput: type: object properties: title: { type: string, maxLength: 60 } text: { type: string, maxLength: 2000 } shortcut: { type: [string, 'null'], pattern: '^[\p{L}\p{N}_-]{1,30}$' } departmentId: { type: [string, 'null'] } LiveCannedReply: type: object properties: id: { type: string } title: { type: string } text: { type: string } shortcut: { type: [string, 'null'] } departmentId: { type: [string, 'null'] } updatedAt: { type: string, format: date-time } LiveVisitorStreamEvent: description: 'One SSE message. `event:` is the name below, `data:` is JSON, `id:` is set only on `event`' oneOf: - { type: object, title: ready, properties: { conversation: { $ref: '#/components/schemas/LiveVisitorConversation' }, lastSeq: { type: integer } } } - { title: event, $ref: '#/components/schemas/LiveVisitorEvent' } - { title: state, $ref: '#/components/schemas/LiveVisitorConversation' } - { type: object, title: typing, properties: { author: { type: object }, on: { type: boolean } } } - { type: object, title: bye, properties: { reason: { type: string, enum: [replaced, expired, deleted] } } } LiveAgentStreamEvent: description: One SSE message on the agent stream oneOf: - { type: object, title: ready, properties: { tenantId: { type: string }, adminId: { type: string }, heartbeatSeconds: { type: integer } } } - { title: conversation, $ref: '#/components/schemas/LiveConversation' } - title: event allOf: - $ref: '#/components/schemas/LiveEvent' - { type: object, properties: { conversationId: { type: string } } } - { type: object, title: typing, properties: { conversationId: { type: string }, author: { type: object }, on: { type: boolean } } } - { type: object, title: presence, properties: { adminId: { type: string }, status: { type: string }, online: { type: boolean }, activeChats: { type: integer } } } - { type: object, title: transfer.offer, properties: { conversationId: { type: string }, transferId: { type: string }, fromAgentName: { type: [string, 'null'] } } } - { type: object, title: conversation.deleted, properties: { conversationId: { type: string } } } - { type: object, title: bye, properties: { reason: { type: string } } }