openapi: 3.1.0 info: title: Chatmode REST API version: "1.0.0" description: | Workspace-scoped REST API for bots, CRM sync, widgets, and backend automations. Organized by purpose: | Purpose | Use for | |---|---| | **Admin & Access** | API keys/CORS (dashboard) and list members | | **Users & Guests** | Create/list/block guests | | **Channels** | List/create/update channels | | **Conversations** | Open/list/get/update guest threads | | **Messaging** | Messages, image attachments, and Easy Templates | | **Events & Webhooks** | Webhook CRUD, delivery logs, and replay | ### Authentication Create an API key in **Dashboard → Integrations**: ``` Authorization: Bearer cm_live_... ``` Optional **scopes** limit which endpoints a key can call. Empty scopes = full workspace access. Missing a required scope returns HTTP 403 with code `insufficient_scope`. ### Rate Limits **60 requests per minute** per API key. Exceeding returns HTTP 429. ### CORS For browser calls from external origins, set **Allowed Origins** on the API key. servers: - url: /api/v1 description: Current API v1 endpoint security: - apiKey: [] tags: - name: Admin description: Workspace members for assignee UX - name: Guests description: Provision and moderate guest sessions - name: Channels description: List, create, and update channels - name: Conversations description: Guest conversation threads (open, status, assignment) - name: Messaging description: Send/list messages and Easy Templates - name: Events description: | Webhook CRUD and outbound signed deliveries. Events: message.created, message.updated, thread.resolved, thread.reopened, thread.assigned, csat.submitted, template.button_clicked. paths: /{workspace}/members: get: tags: [Admin] summary: List workspace members operationId: listMembers description: Returns members for assignee_user_id on PATCH thread. parameters: - $ref: "#/components/parameters/workspace" responses: "200": description: List of members content: application/json: schema: type: object required: [members] properties: members: type: array items: $ref: "#/components/schemas/Member" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /{workspace}/guests: get: tags: [Guests] summary: List recent guests operationId: listGuests parameters: - $ref: "#/components/parameters/workspace" - name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 50 } responses: "200": description: List of guests content: application/json: schema: type: object required: [guests] properties: guests: type: array items: $ref: "#/components/schemas/Guest" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [Guests] summary: Create a guest operationId: createGuest parameters: - $ref: "#/components/parameters/workspace" requestBody: required: true content: application/json: schema: type: object required: [display_name] properties: display_name: { type: string, example: "Sarah Connor" } responses: "201": description: Guest created; session token returned once content: application/json: schema: type: object required: [guest, token] properties: guest: { $ref: "#/components/schemas/Guest" } token: type: string description: Raw guest session token (once). Use as Bearer, chatmode_guest cookie, or WS auth_token. Expires in 7 days. "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/guests/{guest_id}/block: post: tags: [Guests] summary: Block a guest operationId: blockGuest parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/guest_id" responses: "200": description: Guest blocked content: application/json: schema: type: object required: [guest] properties: guest: { $ref: "#/components/schemas/Guest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /{workspace}/guests/{guest_id}/unblock: post: tags: [Guests] summary: Unblock a guest operationId: unblockGuest parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/guest_id" responses: "200": description: Guest unblocked content: application/json: schema: type: object required: [guest] properties: guest: { $ref: "#/components/schemas/Guest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /{workspace}/channels: get: tags: [Channels] summary: List workspace channels operationId: listChannels description: Returns all accessible channels for the workspace associated with the API key. parameters: - $ref: "#/components/parameters/workspace" responses: "200": description: List of channels content: application/json: schema: type: object required: [channels] properties: channels: type: array items: $ref: "#/components/schemas/Channel" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [Channels] summary: Create a channel operationId: createChannel parameters: - $ref: "#/components/parameters/workspace" requestBody: required: true content: application/json: schema: type: object required: [name, slug] properties: name: { type: string, example: "Support" } slug: { type: string, example: "support" } guest_allowed: { type: boolean, default: true } responses: "201": description: Channel created content: application/json: schema: type: object required: [channel] properties: channel: { $ref: "#/components/schemas/Channel" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/channels/{channel}: patch: tags: [Channels] summary: Update a channel operationId: patchChannel parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" requestBody: required: true content: application/json: schema: type: object properties: name: { type: string } guest_allowed: { type: boolean } embed_enabled: { type: boolean } embed_allowed_origins: type: array items: { type: string } ai_enabled: { type: boolean } responses: "200": description: Channel updated content: application/json: schema: type: object required: [channel] properties: channel: { $ref: "#/components/schemas/Channel" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/channels/{channel}/threads: post: tags: [Conversations] summary: Open a conversation thread operationId: openThread description: | Ensure an open thread. Pass guest_id for an existing guest, or display_name to create a guest (returns token once) and open the thread. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" requestBody: required: true content: application/json: schema: type: object properties: guest_id: { type: string, format: uuid } display_name: { type: string } responses: "201": description: Thread opened (token present only when display_name created a guest) content: application/json: schema: type: object required: [thread] properties: thread: { $ref: "#/components/schemas/ThreadState" } guest: { $ref: "#/components/schemas/Guest" } token: type: string description: Returned only when a guest was created via display_name. "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Invalid" } get: tags: [Conversations] summary: List conversation threads operationId: listThreads description: Returns conversation threads in the channel, sorted by most recent activity. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" - name: status in: query description: Filter threads by status. schema: type: string enum: [open, resolved] responses: "200": description: List of conversation threads content: application/json: schema: type: object required: [threads] properties: threads: type: array items: $ref: "#/components/schemas/ThreadState" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /{workspace}/webhooks: get: tags: [Events] summary: List webhooks operationId: listWebhooks description: Secrets are never included in list responses. parameters: - $ref: "#/components/parameters/workspace" responses: "200": description: List of webhooks content: application/json: schema: type: object required: [webhooks] properties: webhooks: type: array items: $ref: "#/components/schemas/Webhook" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [Events] summary: Create a webhook operationId: createWebhook description: Returns the signing secret once. parameters: - $ref: "#/components/parameters/workspace" requestBody: required: true content: application/json: schema: type: object required: [url, events] properties: url: { type: string, format: uri, example: "https://example.com/hooks/chatmode" } events: type: array items: type: string enum: - message.created - message.updated - thread.resolved - thread.reopened - thread.assigned - csat.submitted - template.button_clicked responses: "201": description: Webhook created (includes secret) content: application/json: schema: type: object required: [webhook] properties: webhook: { $ref: "#/components/schemas/WebhookWithSecret" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/webhooks/{id}: patch: tags: [Events] summary: Update a webhook operationId: patchWebhook parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/webhook_id" requestBody: required: true content: application/json: schema: type: object properties: url: { type: string, format: uri } events: type: array items: { type: string } active: { type: boolean } responses: "200": description: Webhook updated content: application/json: schema: type: object required: [webhook] properties: webhook: { $ref: "#/components/schemas/Webhook" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } delete: tags: [Events] summary: Delete a webhook operationId: deleteWebhook parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/webhook_id" responses: "204": description: Webhook deleted "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /{workspace}/webhooks/{id}/rotate-secret: post: tags: [Events] summary: Rotate webhook secret operationId: rotateWebhookSecret parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/webhook_id" responses: "200": description: New secret returned once content: application/json: schema: type: object required: [webhook] properties: webhook: { $ref: "#/components/schemas/WebhookWithSecret" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /{workspace}/webhooks/{id}/deliveries: get: tags: [Events] summary: List webhook deliveries operationId: listWebhookDeliveries description: | Returns recent outbound delivery attempts for one webhook (newest first). Includes payload, HTTP status, and attempt count. Requires `webhooks:read`. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/webhook_id" - name: limit in: query description: Max deliveries to return (default 50, max 100). schema: { type: integer, minimum: 1, maximum: 100, default: 50 } - name: before_id in: query description: Cursor; deliveries older than this delivery id. schema: { type: string, format: uuid } - name: status in: query description: Filter by status. schema: type: string enum: [pending, success, failed, rejected] - name: event in: query description: Filter by event name (e.g. message.created). schema: { type: string } responses: "200": description: Delivery list content: application/json: schema: type: object required: [deliveries] properties: deliveries: type: array items: $ref: "#/components/schemas/WebhookDelivery" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/webhook-deliveries/{delivery_id}: get: tags: [Events] summary: Get a webhook delivery operationId: getWebhookDelivery description: Returns one delivery including the stored JSON payload. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/delivery_id" responses: "200": description: Delivery detail content: application/json: schema: type: object required: [delivery] properties: delivery: $ref: "#/components/schemas/WebhookDelivery" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /{workspace}/webhook-deliveries/{delivery_id}/replay: post: tags: [Events] summary: Replay a webhook delivery operationId: replayWebhookDelivery description: | Re-POSTs the stored payload to the webhook's current URL using the current signing secret. Creates a new delivery row with `replay_of_id` set. Requires `webhooks:manage`. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/delivery_id" responses: "201": description: New delivery from the replay content: application/json: schema: type: object required: [delivery] properties: delivery: $ref: "#/components/schemas/WebhookDelivery" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /{workspace}/channels/{channel}/threads/{guest_id}: get: tags: [Conversations] summary: Get thread details operationId: getThread description: Returns status and assignment for a guest conversation thread. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" - $ref: "#/components/parameters/guest_id" responses: "200": description: Thread details content: application/json: schema: type: object required: [thread] properties: thread: $ref: "#/components/schemas/ThreadState" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } patch: tags: [Conversations] summary: Update thread status or assignment operationId: patchThread description: Resolve/reopen a conversation or assign/unassign an agent. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" - $ref: "#/components/parameters/guest_id" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PatchThreadRequest" responses: "200": description: Updated thread state content: application/json: schema: type: object required: [thread] properties: thread: $ref: "#/components/schemas/ThreadState" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/channels/{channel}/messages: get: tags: [Messaging] summary: List messages operationId: listMessages description: Messages are returned oldest first. Use thread filters and before_id cursor. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" - name: limit in: query description: Number of messages to return (default 50, max 100). schema: { type: integer, minimum: 1, maximum: 100, default: 50 } - name: before_id in: query description: Message UUID cursor; returns messages older than this message. schema: { type: string, format: uuid } - name: thread_guest_id in: query description: Filter by guest conversation thread. schema: { type: string, format: uuid } - name: thread_user_id in: query description: Filter by internal member thread. schema: { type: string, format: uuid } responses: "200": description: Array of messages content: application/json: schema: type: object required: [messages] properties: messages: type: array items: $ref: "#/components/schemas/Message" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } post: tags: [Messaging] summary: Post a message into a thread operationId: postMessage description: | Sends a message as the workspace owner (text and/or a previously uploaded attachment). Provide either `thread_guest_id` or `thread_user_id`. Upload images first via `POST .../attachments`, then pass `attachment_id`. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" requestBody: required: true content: application/json: schema: type: object properties: body: type: string maxLength: 2000 description: Message text. Optional when `attachment_id` is set. example: "Hello! Thank you for reaching out, how can we help?" thread_guest_id: type: string format: uuid description: Guest thread to reply into. thread_user_id: type: string format: uuid description: Member thread to reply into. attachment_id: type: string format: uuid description: Attachment UUID from `POST .../attachments` (same channel). responses: "201": description: Created message content: application/json: schema: type: object required: [message] properties: message: $ref: "#/components/schemas/Message" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/channels/{channel}/attachments: post: tags: [Messaging] summary: Upload an image attachment operationId: uploadAttachment description: | Multipart upload of an image (jpeg, png, webp, or gif). Requires `messages:write`. Returns an attachment id to pass as `attachment_id` when posting a message. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary description: Image file (jpeg/png/webp/gif). responses: "201": description: Uploaded attachment content: application/json: schema: type: object required: [attachment] properties: attachment: type: object required: [id, url, mime_type, filename] properties: id: { type: string, format: uuid } url: { type: string, example: "/files/18cbeff1-0428-4447-9750-f8ec009ae842" } mime_type: { type: string, example: "image/png" } filename: { type: string, example: "photo.png" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "413": description: Upload exceeds plan limit content: application/json: schema: $ref: "#/components/schemas/Error" "422": { $ref: "#/components/responses/Invalid" } /{workspace}/channels/{channel}/messages/template: post: tags: [Messaging] summary: Send an Easy Template into a thread operationId: postTemplateMessage description: | Renders an active Easy Template with `parameters` and posts it into a conversation as the workspace owner. Provide either `thread_guest_id` or `thread_user_id`. parameters: - $ref: "#/components/parameters/workspace" - $ref: "#/components/parameters/channel" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PostTemplateRequest" responses: "201": description: Created template message content: application/json: schema: type: object required: [message] properties: message: $ref: "#/components/schemas/Message" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/messages/template: post: tags: [Messaging] summary: Send an Easy Template (workspace-level alias) operationId: postTemplateMessageAlias description: | Same as `POST /{workspace}/channels/{channel}/messages/template`. Pass `channel` or `channel_id` in the body; if omitted, the first workspace channel is used. parameters: - $ref: "#/components/parameters/workspace" requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/PostTemplateRequest" - type: object properties: channel: type: string description: Channel slug when not in the URL path. channel_id: type: string format: uuid description: Channel UUID when not in the URL path. responses: "201": description: Created template message content: application/json: schema: type: object required: [message] properties: message: $ref: "#/components/schemas/Message" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Invalid" } /{workspace}/templates: get: tags: [Messaging] summary: List active Easy Templates operationId: listTemplates description: Returns active templates including body, buttons, and extracted variables. parameters: - $ref: "#/components/parameters/workspace" responses: "200": description: Active templates content: application/json: schema: type: object required: [templates] properties: templates: type: array items: $ref: "#/components/schemas/Template" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /{workspace}/templates/{slug}/metrics: get: tags: [Messaging] summary: Template engagement metrics operationId: getTemplateMetrics description: Returns sends, clicks, clicks by type, and CTR for a template slug. parameters: - $ref: "#/components/parameters/workspace" - name: slug in: path required: true description: Template slug schema: { type: string } - name: days in: query description: Lookback window in days (default 30). schema: { type: integer, minimum: 1, default: 30 } responses: "200": description: Template metrics content: application/json: schema: $ref: "#/components/schemas/TemplateMetrics" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } components: securitySchemes: apiKey: type: http scheme: bearer bearerFormat: cm_live_... description: | Bearer API key from Dashboard → Integrations. Optional scopes (members:read, guests:read/write, channels:read/write, threads:read/write, messages:read/write, templates:read, webhooks:read/manage). Empty scopes grant full access. parameters: workspace: name: workspace in: path required: true description: Workspace slug (e.g. acme) schema: { type: string } channel: name: channel in: path required: true description: Channel slug (e.g. support or general) schema: { type: string } guest_id: name: guest_id in: path required: true description: Guest UUID identifying the conversation thread schema: { type: string, format: uuid } webhook_id: name: id in: path required: true description: Webhook UUID schema: { type: string, format: uuid } delivery_id: name: delivery_id in: path required: true description: Webhook delivery UUID schema: { type: string, format: uuid } schemas: Member: type: object required: [user_id, username, role] properties: user_id: { type: string, format: uuid } username: { type: string, example: "alex" } display_name: { type: string, example: "Alex" } role: { type: string, example: "owner" } Guest: type: object required: [id, display_name, blocked, expires_at, created_at] properties: id: { type: string, format: uuid } display_name: { type: string, example: "Sarah Connor" } blocked: { type: boolean } expires_at: { type: string, format: date-time } created_at: { type: string, format: date-time } Webhook: type: object required: [id, url, events, active, created_at] properties: id: { type: string, format: uuid } url: { type: string, format: uri } events: type: array items: { type: string } active: { type: boolean } created_at: { type: string, format: date-time } WebhookWithSecret: allOf: - $ref: "#/components/schemas/Webhook" - type: object required: [secret] properties: secret: { type: string, description: "Returned only on create and rotate-secret" } WebhookDelivery: type: object required: [id, webhook_id, event, payload, status, attempts, created_at] properties: id: { type: string, format: uuid } webhook_id: { type: string, format: uuid } event: { type: string, example: "message.created" } payload: type: object additionalProperties: true description: Exact JSON body that was (or will be) POSTed. status: type: string enum: [pending, success, failed, rejected] status_code: { type: integer, nullable: true } response_body: type: string description: Truncated response body from the endpoint (up to 4 KiB). attempts: { type: integer } last_error: { type: string } replay_of_id: type: string format: uuid nullable: true description: Present when this delivery is a replay of another. created_at: { type: string, format: date-time } completed_at: { type: string, format: date-time, nullable: true } Channel: type: object required: [id, slug, name, guest_allowed, created_at] properties: id: { type: string, format: uuid } slug: { type: string, example: "general" } name: { type: string, example: "General" } guest_allowed: { type: boolean, example: true } created_at: { type: string, format: date-time } ThreadState: type: object required: [guest_id, display_name, status] properties: guest_id: type: string format: uuid display_name: type: string example: "Guest 42" status: type: string enum: [open, resolved] example: "open" assignee_user_id: type: string format: uuid nullable: true assignee_name: type: string nullable: true example: "alex" last_activity_at: type: string format: date-time nullable: true resolved_at: type: string format: date-time nullable: true PatchThreadRequest: type: object properties: status: type: string enum: [open, resolved] description: New status for the thread. assignee_user_id: type: string format: uuid description: Member UUID to assign this thread to. clear_assignee: type: boolean description: Set to true to unassign the current assignee. Message: type: object required: [id, channel, author, body, created_at] properties: id: { type: string, format: uuid } channel: { type: string, example: "general" } author: type: object required: [type, name] properties: type: { type: string, enum: [guest, member, owner, assistant] } name: { type: string, example: "Support" } body: { type: string, example: "Hello from Chatmode API" } thread: type: object nullable: true properties: guest_id: { type: string, format: uuid } user_id: { type: string, format: uuid } attachment: type: object nullable: true properties: id: { type: string, format: uuid } url: { type: string } mime_type: { type: string } filename: { type: string } created_at: { type: string, format: date-time } TemplateButton: type: object required: [type, text] properties: type: type: string enum: [quick_reply, url, copy_code] text: { type: string } url: { type: string } code: { type: string } payload: { type: string } Template: type: object required: [id, slug, name, category, body, variables, is_active, created_at] properties: id: { type: string, format: uuid } slug: { type: string, example: "order-shipped" } name: { type: string, example: "Order shipped" } category: { type: string, example: "shipping" } header_text: { type: string } body: { type: string } footer_text: { type: string } buttons: type: array items: { $ref: "#/components/schemas/TemplateButton" } variables: type: array items: { type: string } example: ["name", "order_id"] is_active: { type: boolean } created_at: { type: string, format: date-time } PostTemplateRequest: type: object properties: template: type: string description: Template slug (alias of template_slug). example: "order-shipped" template_slug: type: string description: Template slug. thread_guest_id: type: string format: uuid thread_user_id: type: string format: uuid parameters: type: object additionalProperties: { type: string } description: Variable substitutions for the template. example: { name: "Sarah", order_id: "10492" } TemplateMetrics: type: object required: [template_id, template_slug, days, total_sends, total_clicks, clicks_by_type, ctr] properties: template_id: { type: string, format: uuid } template_slug: { type: string } days: { type: integer } total_sends: { type: integer } total_clicks: { type: integer } clicks_by_type: type: object additionalProperties: { type: integer } ctr: { type: number, format: float } Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: { type: string, example: "forbidden" } message: { type: string, example: "API key does not grant access to this workspace" } responses: BadRequest: description: Invalid JSON or malformed request content: application/json: schema: { $ref: "#/components/schemas/Error" } Unauthorized: description: Missing or invalid API key content: application/json: schema: { $ref: "#/components/schemas/Error" } Forbidden: description: Key does not grant access to this workspace, or missing required scope content: application/json: schema: { $ref: "#/components/schemas/Error" } examples: wrong_workspace: value: error: { code: forbidden, message: "API key does not grant access to this workspace" } insufficient_scope: value: error: { code: insufficient_scope, message: "API key is missing required scope: messages:write" } NotFound: description: Channel, workspace, or template not found content: application/json: schema: { $ref: "#/components/schemas/Error" } Invalid: description: Unprocessable entity / validation failure content: application/json: schema: { $ref: "#/components/schemas/Error" }