openapi: 3.1.0
info:
  title: Deskwoot API
  description: |
    REST API for the Deskwoot customer support platform.

    Deskwoot is an AI-powered customer support platform with a multi-channel inbox
    (email, live chat, WhatsApp, Telegram, X, LINE, SMS),
    AI chatbot, AI copilot, help center, automation engine, and integrations with
    Shopify, Stripe, WooCommerce, and more.

    ## Authentication

    All API requests require authentication via a Bearer token. Generate an API token
    in your Deskwoot dashboard under **Settings > API Tokens** (requires Enterprise plan).

    ```
    Authorization: Bearer dw_your-api-token
    ```

    API tokens support scoped access: `read`, `write`, `contacts:read`, `contacts:write`,
    `conversations:read`, `conversations:write`.
  version: "1.0.0"
  contact:
    name: Deskwoot Support
    email: hello@deskwoot.com
    url: https://deskwoot.com
  license:
    name: Proprietary
    url: https://deskwoot.com/terms

servers:
  - url: https://deskwoot.com/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Conversations
    description: Manage customer conversations across all channels
  - name: Messages
    description: Send and retrieve messages within conversations
  - name: Tickets
    description: |
      Internal work split off a customer conversation (Enterprise plan, Settings > Tickets switched on).
      A ticket is a conversation with `kind` TICKET: the conversation's contact, no inbox, a title
      (`subject`), an optional category and `sourceConversationId`. Read and change it with the
      conversation endpoints. A ticket never writes to the customer: a public reply, a file, a draft or a
      macro message in a ticket answers 409 `ticket_public_reply`; answer the customer with
      customer-reply. Lists, counts and reports leave tickets out unless `type` asks for them.
  - name: Contacts
    description: Manage customer contacts
  - name: Custom Attributes
    description: Fields you define for conversations and contacts
  - name: Inboxes
    description: Manage inboxes (channels)
  - name: Labels
    description: Organize conversations with labels
  - name: Teams
    description: Manage agent teams
  - name: Agents
    description: Manage agents and invitations
  - name: Canned Responses
    description: Pre-written response templates
  - name: Automation Rules
    description: Event-driven automation rules
  - name: Webhooks
    description: |
      Real-time event notifications via HMAC-signed webhooks. Deskwoot POSTs a JSON
      envelope `{ event, data, timestamp, accountId }` to your URL with the headers
      `X-Deskwoot-Event: <event name>` and `X-Deskwoot-Signature: sha256=<hex HMAC-SHA256 of the raw body>`.
      Delivery waits up to 10 seconds; a non 2xx answer, timeout or connection error
      triggers exactly one retry after 5 seconds, then the event is dropped. Redirects
      are not followed. Event names are dotted (`conversation.created`); underscored
      spellings (`conversation_created`) are accepted as aliases in `subscriptions`.
      An empty `subscriptions` array subscribes to every event except the ticket events,
      which go only to a webhook that names them. An unknown event name is refused with
      400. The full event list with payload shapes is under the top level `webhooks` key
      of this document.
  - name: REST Hooks
    description: |
      Subscriptions of an automation app (the Deskwoot app for Zapier) to one trigger each.
      A REST hook gets the trigger's flat object (the shape the samples endpoint returns, plus
      `trigger` and `occurredAt`; `actorType` and `actorName` are null), signed and retried like a
      webhook.
      Requires a REST API token (Enterprise) created by an administrator, with the trigger's read
      scope. A receiver that answers `410 Gone` loses its hook, and revoking the token removes
      every hook it subscribed. REST hooks are not listed under Webhooks.
      No trigger is a ticket event, so a ticket and a comment on a ticket set off no REST hook,
      as with a webhook that names no ticket event.
  - name: Campaigns
    description: One-off and ongoing messaging campaigns
  - name: Bots
    description: AI chatbot configuration and training
  - name: Integrations
    description: Shopify, Stripe, WooCommerce, Slack, and more
  - name: Commerce
    description: |
      Refunds of the orders of a connected Shopify or WooCommerce store and of Stripe
      payments, made at the order's own provider. A token needs the exact commerce
      scope of the action (`commerce:refund`); `read` or `write` never grant it.
      Amounts you send are a decimal in the order's currency for every provider
      (20.50 EUR, 1000 JPY); amounts in answers are in the smallest unit of the
      currency they name (`...Minor`, 2050 is 20.50 EUR).
  - name: Help Center
    description: Knowledge base portals, categories, and articles
  - name: SLA Policies
    description: Service level agreement policies
  - name: Reports
    description: |
      Analytics and reporting.

      Changes on 2026-10-12. Per agent first response: in agent scoped results (group_by=agent, agent_id, or a member who is no administrator) `firstResponse` is now the agent's own first response, measured from the moment the agent had the conversation, instead of the customer's wait on the conversations assigned to them (likewise `first_response_*` in the breakdown CSV, `firstReplyAt` and `firstResponseSeconds` in conversation rows, and Avg Response Time (min) in the agents export, a cell that is now empty instead of 0 when the agent answered no waiting customer in the range). Outside agent scoped results `firstResponse` now runs from the customer's first message instead of the conversation's creation, so a conversation an agent started counts only once the customer has written, and `unanswered` counts only conversations where the customer asked something and nobody has answered. `agent_id` now means the conversations the agent worked on in the period (replied in, was assigned, or held when it was resolved) instead of the current assignee. Agent groups overlap and do not add up to the totals. Every field and CSV column keeps its name and position; new ones come last (`replyTime`, the `reply_time_*` CSV columns, `agentFirstResponses`, the agents export columns after Avg Response Time (min), and the replies endpoint).
  - name: Settings
    description: Account settings and configuration

paths:
  /accounts/{accountId}/conversations:
    get:
      tags: [Conversations]
      summary: List conversations
      operationId: listConversations
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: status
          in: query
          schema:
            type: string
            enum: [OPEN, PENDING, RESOLVED, SNOOZED]
        - name: assignee_id
          in: query
          schema:
            type: string
        - name: team_id
          in: query
          schema:
            type: string
        - name: inbox_id
          in: query
          schema:
            type: string
        - name: label_id
          in: query
          schema:
            type: string
        - name: type
          in: query
          description: "conversations (the default, never a ticket), tickets or all. Any other value answers 400."
          schema:
            type: string
            enum: [conversations, tickets, all]
            default: conversations
        - name: category_id
          in: query
          description: A category id (see listTicketCategories).
          schema:
            type: string
        - name: ticket_status_id
          in: query
          description: Tickets in this custom ticket status (see listTicketStatuses).
          schema:
            type: string
        - name: priority
          in: query
          description: "Comma separated priorities (URGENT, HIGH, MEDIUM, LOW, any letter case), any of them. An unknown value answers 400."
          schema:
            type: string
        - name: created_by
          in: query
          description: "me: only the tickets the caller created; it lists tickets whatever the type. Any other value answers 400."
          schema:
            type: string
            enum: [me]
        - name: tracker
          in: query
          description: "only: just the trackers; exclude: everything but them. Trackers are tickets, so only lists something with type tickets or all. Any other value answers 400."
          schema:
            type: string
            enum: [only, exclude]
        - name: issue_key
          in: query
          description: "A Jira issue key such as TAB-12, any letter case: only the tickets linked to that issue, so it lists something with type tickets or all. A value that is no issue key answers 400."
          schema:
            type: string
        - name: attr
          in: query
          style: deepObject
          explode: true
          description: "Conversation custom attributes that must all match, as attr[<key>]=<value> (for example attr[order_id]=1001). The key uses lowercase letters, digits and underscores; the value (1 to 255 characters) equals the stored text, or the number or true/false it spells. Case sensitive, at most 5 keys, each once; anything else answers 400 Invalid attr filter."
          schema:
            type: object
            additionalProperties: { type: string }
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/pageSize"
      responses:
        "200":
          description: "Paginated list of conversations. Each row also carries needsAttention: true while a team has it, nobody holds it, it is open or pending and the AI bot is not answering it. A conversation that waits in the round robin queue (queuedAt set) never needs attention, since the queue hands it to an agent on its own. While Jira is connected and tickets are on, a ticket with linked Jira issues also carries linkedIssues (the first 3: key, url, statusName, statusCategory, deleted, notFound) and linkedIssueCount."
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Conversation"
                  meta:
                    $ref: "#/components/schemas/PaginationMeta"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Conversations]
      summary: Create a conversation
      operationId: createConversation
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [inboxId, content]
              properties:
                contactId:
                  type: string
                contactEmail:
                  type: string
                  format: email
                inboxId:
                  type: string
                content:
                  type: string
                subject:
                  type: string
                customAttributes:
                  $ref: "#/components/schemas/CustomAttributesWrite"
          multipart/form-data:
            schema:
              type: object
              description: A first message with files. Repeat `file` for several files; `content` may then be empty.
              required: [inboxId]
              properties:
                contactId:
                  type: string
                contactEmail:
                  type: string
                  format: email
                inboxId:
                  type: string
                content:
                  type: string
                subject:
                  type: string
                file:
                  type: array
                  items:
                    type: string
                    format: binary
      responses:
        "201":
          description: Conversation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId:
                    type: string
                  messageId:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /accounts/{accountId}/inbound:
    post:
      tags: [Conversations]
      summary: Push an inbound customer message into a REST API inbox
      description: |
        Stores a message from your own system (a phone call, a form, another tool) as an incoming
        customer message in a REST API inbox, like a message on any other channel: rules, assignment
        and notifications run. The token needs the `inbound:ingest` scope, which the broad `write`
        scope does not cover. The contact is found or created by `externalContactId` (stored as the
        contact's `identifier`). contactName names the contact (a new one, and an existing one whose name
        differs); contactEmail and contactPhone fill a contact the message creates (an email another
        contact has is left out). subject, priority,
        assigneeId and customAttributes set up a conversation the message
        opens (`isNewConversation` true); a message that joins a conversation still open leaves that
        conversation as it is. A retry with the same `sourceId` (or `Idempotency-Key` header, or
        `messageId`) stores nothing and answers `deduped: true`. 120 requests a minute per token.
      operationId: ingestInboundMessage
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: Idempotency-Key
          in: header
          required: false
          description: Your own id for the message, used when the body has no sourceId.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [inboxId, externalContactId, content]
              properties:
                inboxId:
                  type: string
                  description: A REST API inbox of this workspace.
                externalContactId:
                  type: string
                  description: Your own id for the customer. Messages with the same id share one contact.
                content:
                  type: string
                contentType:
                  type: string
                  enum: [TEXT, HTML]
                sourceId:
                  type: string
                  description: Your own id for the message. A retry with the same id stores nothing.
                messageId:
                  type: string
                  description: Your own id for the message, used when there is no sourceId and no Idempotency-Key.
                contactName:
                  type: string
                contactEmail:
                  type: string
                  format: email
                contactPhone:
                  type: string
                  description: Up to 64 characters with digits.
                subject:
                  type: string
                  maxLength: 1024
                priority:
                  type: string
                  enum: [URGENT, HIGH, MEDIUM, LOW]
                assigneeId:
                  type: string
                  description: An agent of this workspace. The conversation is assigned to them instead of auto assignment.
                customAttributes:
                  $ref: "#/components/schemas/CustomAttributesWrite"
      responses:
        "200":
          description: Stored, or a retry of a stored message (`deduped` true, no ids)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  deduped:
                    type: boolean
                  conversationId:
                    type: string
                  messageId:
                    type: string
                  isNewConversation:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: inboxId is no REST API inbox of this workspace
        "429":
          description: More than 120 requests a minute with this token

  /conversations/{conversationId}:
    get:
      tags: [Conversations]
      summary: Get a conversation
      operationId: getConversation
      parameters:
        - $ref: "#/components/parameters/conversationId"
      responses:
        "200":
          description: Full conversation with messages
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Conversation"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Conversations]
      summary: Update a conversation
      operationId: updateConversation
      parameters:
        - $ref: "#/components/parameters/conversationId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [OPEN, PENDING, RESOLVED, SNOOZED]
                priority:
                  type: string
                  enum: [NONE, LOW, MEDIUM, HIGH, URGENT]
                assigneeId:
                  type: string
                  nullable: true
                teamId:
                  type: string
                  nullable: true
                muted:
                  type: boolean
                customAttributes:
                  $ref: "#/components/schemas/CustomAttributesWrite"
                categoryId:
                  type: string
                  nullable: true
                  description: Category id, or null to clear. Needs tickets switched on and in the plan; an archived category is refused.
                ticketStatusId:
                  type: string
                  nullable: true
                  description: >-
                    Tickets only (400 not_ticket otherwise). A custom ticket status: the ticket takes its base status, and a
                    status that names another one answers 400 ticket_status_mismatch; an archived one is refused (409
                    ticket_status_archived). null keeps the base status without a custom one. Works after a downgrade.
                slaPolicyId:
                  type: string
                  nullable: true
                  description: >-
                    The conversation's or the ticket's own SLA policy: an active policy of the account (400
                    invalid_sla_policy otherwise), with SLA in the plan. It comes before the inbox's policy and the
                    default; a ticket is timed by no other. null takes it away, also after a downgrade. Only
                    administrators can change it (403 for an agent and for an agent's API token). The policy it has already is no change and
                    passes unchecked.
                title:
                  type: string
                  description: Tickets only (400 not_ticket otherwise). 1 to 200 characters.
                summary:
                  type: string
                  description: Tickets only (400 not_ticket otherwise). 1 to 20,000 characters; replaces the summary note.
      responses:
        "200":
          description: Updated conversation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Conversation"
        "400":
          $ref: "#/components/responses/BadRequest"

  /conversations/{conversationId}/history:
    get:
      tags: [Conversations]
      summary: Who did what to a conversation or ticket
      description: |
        Its activity lines (the team only ones too) and who held it when, newest first, under the read rules
        of the conversation itself. `activity` holds the lines (Message rows of type ACTIVITY) with
        `meta` { hasMore, nextCursor, limit }; `before` answers the next page of them alone. `assignments` holds
        one row per stretch of time an agent held it: `assignee`, `assignedAt`, `unassignedAt` (null while it
        lasts), `startInferred`, how it started (`source`, `assignedBy`) and ended (`endSource`,
        `unassignedBy`); `assignmentsBefore` answers the next page of them alone. New source values can appear
        in later releases, so treat an unknown one as a plain change. A line or a stretch a merge moved in
        carries `mergedFrom` { conversationId, displayId } (displayId null when not known), otherwise null.
        Both cursors at once answer 400 invalid_cursor. Tokens need conversations:read.
      operationId: getConversationHistory
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: limit
          in: query
          description: Rows per page of each list
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: before
          in: query
          description: activity.meta.nextCursor of the previous page
          schema: { type: string }
        - name: assignmentsBefore
          in: query
          description: assignments.meta.nextCursor of the previous page
          schema: { type: string }
      responses:
        "200":
          description: The history
          content:
            application/json:
              schema:
                type: object
                properties:
                  activity:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          allOf:
                            - $ref: "#/components/schemas/Message"
                            - type: object
                              properties:
                                mergedFrom: { $ref: "#/components/schemas/HistoryMergedFrom" }
                      meta: { $ref: "#/components/schemas/HistoryPageMeta" }
                  assignments:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            assignee: { $ref: "#/components/schemas/HistoryPerson" }
                            assignedAt: { type: string, format: date-time }
                            startInferred: { type: boolean, description: The start is the earliest later evidence, not the moment it was assigned }
                            unassignedAt: { type: string, format: date-time, nullable: true }
                            source: { type: string, description: "How it started, for example CLAIM, TRANSFER, ROUND_ROBIN, TICKET_CREATE" }
                            assignedBy: { $ref: "#/components/schemas/HistoryPerson" }
                            endSource: { type: string, nullable: true }
                            unassignedBy: { $ref: "#/components/schemas/HistoryPerson" }
                            mergedFrom: { $ref: "#/components/schemas/HistoryMergedFrom" }
                      meta: { $ref: "#/components/schemas/HistoryPageMeta" }
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /conversations/{conversationId}/messages:
    get:
      tags: [Messages]
      summary: List messages in a conversation
      operationId: listMessages
      parameters:
        - $ref: "#/components/parameters/conversationId"
      responses:
        "200":
          description: List of messages
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Message"
    post:
      tags: [Messages]
      summary: Send a message
      operationId: sendMessage
      parameters:
        - $ref: "#/components/parameters/conversationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
                private:
                  type: boolean
                  default: false
                messageType:
                  type: string
                  enum: [OUTGOING, ACTIVITY]
                  default: OUTGOING
                contentType:
                  type: string
                  enum: [TEXT, HTML]
                  default: TEXT
                cc:
                  type: array
                  items:
                    type: string
                    format: email
                bcc:
                  type: array
                  items:
                    type: string
                    format: email
                subject:
                  type: string
      responses:
        "201":
          description: Message sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"

  /conversations/{conversationId}/tickets:
    get:
      tags: [Tickets]
      summary: List the tickets made from a conversation
      description: Newest first, up to 100. Tokens need conversations:read.
      operationId: listConversationTickets
      parameters:
        - $ref: "#/components/parameters/conversationId"
      responses:
        "200":
          description: The conversation's tickets
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        displayId: { type: integer }
                        title: { type: string }
                        status: { type: string }
                        priority: { type: string, nullable: true }
                        createdAt: { type: string, format: date-time }
                        lastActivityAt: { type: string, format: date-time }
                        assignee: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        team: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        category: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        createdBy: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        canOpen: { type: boolean, description: Whether the caller may open the ticket itself }
    post:
      tags: [Tickets]
      summary: Create a ticket from a conversation
      description: |
        Splits a ticket off the conversation and hands it to a team and/or an agent. The agent gets it at
        once, without a capacity check; a team without an agent leaves it unassigned in the team's folder and
        its members are notified. The conversation gets a team only status card. Nothing reaches the
        customer. An `Idempotency-Key` header names the request; without one the request itself does, so the
        same request within 2 minutes answers with the first ticket (200). Tokens need conversations:write.
      operationId: createTicket
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              additionalProperties: false
              properties:
                title: { type: string, minLength: 1, maxLength: 200 }
                summary: { type: string, maxLength: 20000, nullable: true, description: Stored as the ticket's first internal note }
                contextMessageIds:
                  type: array
                  maxItems: 500
                  items: { type: string }
                  description: Messages of the conversation the ticket is made from, shown in the conversation's order
                context:
                  type: string
                  enum: [interaction]
                  nullable: true
                  description: Make it from the conversation's current interaction (what ticket-context selects by default) instead of contextMessageIds. Read only when the request makes a new ticket, so a retry answers with the ticket it made.
                categoryId: { type: string, nullable: true, description: Required when the workspace requires a category; not archived }
                priority:
                  type: string
                  enum: [URGENT, HIGH, MEDIUM, LOW]
                  nullable: true
                  description: Left out, the ticket gets the workspace's default priority for new tickets (Settings > Tickets; none unless an admin set one). null means no priority.
                teamId: { type: string, nullable: true }
                assigneeId: { type: string, nullable: true, description: An active member of the workspace }
                resolveSource:
                  type: boolean
                  description: Also resolve the conversation once the ticket is made, as a plain resolve does (its CSAT survey waits for the open ticket). Needs the right to resolve it; a retry with the same key resolves it if it is still open and its status did not change since the ticket was made. The answer then has `resolved`.
      responses:
        "201":
          description: Ticket created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket: { $ref: "#/components/schemas/Conversation" }
                  resolved: { type: boolean, description: "With resolveSource: whether this request resolved the conversation (false when it was resolved already, or on a retry when its status changed since the ticket was made)" }
        "200":
          description: The same request again; the first ticket
        "400":
          description: invalid_title, invalid_summary, invalid_context (with messageIds), invalid_category, category_required, invalid_team, invalid_target
        "403":
          description: The plan does not include tickets, or the caller may not change the conversation
        "409":
          description: tickets_disabled, ticket_source, source_spam, category_archived, idempotency_mismatch (with the ticket the key made)
        "500":
          description: "resolve_failed: with resolveSource, the ticket was made (`ticket`) but the conversation could not be resolved; retry with the same key"

  /conversations/{conversationId}/ticket-context:
    get:
      tags: [Tickets]
      summary: What a new ticket of a conversation can be made from
      description: |
        `defaultSelection` lists the ids picked by default, oldest first: the messages since the conversation
        was last resolved or last made into a ticket, at least the customer's last messages. `since` is that
        boundary (null for the conversation's start). `data` and `meta` are one page of the messages that can
        be context, newest page first. 409 ticket_source for a ticket.
      operationId: getTicketContext
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: before
          in: query
          description: meta.nextCursor of the previous page
          schema: { type: string }
      responses:
        "200":
          description: Default selection and candidates
          content:
            application/json:
              schema:
                type: object
                properties:
                  defaultSelection: { type: array, items: { type: string } }
                  since: { type: string, format: date-time, nullable: true }
                  data: { type: array, items: { $ref: "#/components/schemas/Message" } }
                  meta: { type: object, additionalProperties: true }

  /conversations/{conversationId}/linked-messages:
    get:
      tags: [Tickets]
      summary: The conversation a ticket came from, read through the ticket
      description: |
        `conversationId` is the ticket. Whoever may read the ticket reads these, also without access to the
        conversation itself. `source` is the conversation (null once it was deleted); `context` holds the
        messages the ticket was made from, in their order, with `contextMeta`; `newer` holds the
        conversation's messages after the context, newest page first, and is empty with `code`
        source_locked while the plan locks or hides the conversation. 409 not_ticket for a conversation.
      operationId: getTicketLinkedMessages
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: before
          in: query
          description: newer.meta.nextCursor of the previous page
          schema: { type: string }
        - name: contextLimit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 500 }
        - name: contextAfter
          in: query
          description: contextMeta.nextCursor; answers the next page of the context alone
          schema: { type: string }
      responses:
        "200":
          description: The linked conversation
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /conversations/{conversationId}/customer-reply:
    post:
      tags: [Tickets]
      summary: Reply to the customer from a ticket
      description: |
        `conversationId` is the ticket. The reply is stored in the conversation the ticket came from and
        delivered there on its channel (an email in the original thread), with the limits, virus scan and
        Idempotency-Key replay of a normal reply; the ticket gets a team only line pointing to it. The
        conversation is not claimed and stays resolved if it was. With files, send the attachments
        endpoint's multipart form plus `resolve` "true" and `ticketStatusId`. A draft only token is refused with 403; an inbox
        restricted token needs the conversation's inbox. Tokens need conversations:write.
      operationId: replyFromTicket
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content: { type: string }
                contentType: { type: string, enum: [TEXT, HTML] }
                subject: { type: string }
                cc: { type: array, items: { type: string } }
                bcc: { type: array, items: { type: string } }
                resolve: { type: boolean, description: Also resolve the ticket (never the conversation) }
                ticketStatusId: { type: string, description: "With resolve: a ticket status under Resolved to resolve the ticket into" }
      responses:
        "200":
          description: The reply in the conversation, the ticket's line for it, and whether this request resolved the ticket
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { $ref: "#/components/schemas/Message" }
                  reference: { $ref: "#/components/schemas/Message" }
                  resolved: { type: boolean }
        "403":
          description: Not allowed, a draft only token, or source_locked while the plan locks the conversation
        "409":
          description: not_ticket, tracker_no_customer (a tracker has no customer), source_deleted, source_spam, contact_blocked, source_no_inbox, ticket_status_archived

  /conversations/{conversationId}/tracker-links:
    get:
      tags: [Tickets]
      summary: What is linked to a tracker, or the trackers a conversation is linked to
      description: |
        On a tracker: what is linked to it that the caller may open, newest link first; `hiddenCount` counts
        the linked rows the caller may not open, `shownCount` the ones listed over all pages. `lastUpdate` is
        what the tracker's latest update did for the row (null when it did not name it). On a conversation or
        a customer's ticket: `{ trackers }`, the trackers it is linked to that the caller may open (at most
        100). Tokens need conversations:read.
      operationId: listTrackerLinks
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 100 }
        - name: before
          in: query
          description: meta.nextCursor of the previous page; a value that is no cursor answers 400
          schema: { type: string }
      responses:
        "200":
          description: The linked rows (on a tracker) or the trackers (on anything else)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        displayId: { type: integer }
                        kind: { type: string, enum: [CONVERSATION, TICKET] }
                        title: { type: string }
                        status: { type: string }
                        priority: { type: string, nullable: true }
                        sourceConversationId: { type: string, nullable: true }
                        contact: { type: object, nullable: true, additionalProperties: true, description: "id, name and organization { id, name }" }
                        channelType: { type: string, nullable: true, description: A ticket's is its conversation's }
                        assignee: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        team: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        linkedAt: { type: string, format: date-time }
                        linkedBy: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        lastUpdate: { type: object, nullable: true, additionalProperties: true }
                  meta:
                    type: object
                    properties:
                      hasMore: { type: boolean }
                      nextCursor: { type: string, nullable: true }
                      limit: { type: integer }
                      hiddenCount: { type: integer }
                      shownCount: { type: integer }
                  trackers:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        displayId: { type: integer }
                        title: { type: string }
                        status: { type: string }
                        priority: { type: string, nullable: true }
                        assignee: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
                        linkedAt: { type: string, format: date-time }
    post:
      tags: [Tickets]
      summary: Link conversations and tickets to a tracker
      description: |
        Links 1 to 100 conversations or customers' tickets to the tracker, with a line on both, and sends the
        tracker's ticket.updated with `changes.links`. One result per id in the order asked; a row the caller
        may not open is not_found. Needs tickets switched on and the Enterprise plan. A token limited to some
        inboxes cannot change a tracker's links. Tokens need conversations:write.
      operationId: linkToTracker
      parameters:
        - $ref: "#/components/parameters/conversationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [conversationIds]
              additionalProperties: false
              properties:
                conversationIds: { type: array, minItems: 1, maxItems: 100, items: { type: string } }
      responses:
        "200":
          description: One result per id
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        conversationId: { type: string }
                        outcome: { type: string, enum: [linked, already_linked, refused] }
                        code: { type: string, enum: [not_found, tracker, source_spam] }
                        error: { type: string }
        "403":
          description: The plan does not include tickets, the caller may not change the tracker, or the token is limited to some inboxes
        "409":
          description: not_tracker, tickets_disabled

  /conversations/{conversationId}/tracker-links/{linkedConversationId}:
    delete:
      tags: [Tickets]
      summary: Unlink a conversation or ticket from a tracker
      description: |
        `conversationId` is the tracker. Unlinks the row, with a line on both. A conversation whose survey
        waited for the tracker gets it now, unless something else holds it. Works after a downgrade. A token
        limited to some inboxes cannot change a tracker's links. Tokens need conversations:write.
      operationId: unlinkFromTracker
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: linkedConversationId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Unlinked
          content:
            application/json:
              schema:
                type: object
                properties:
                  unlinked: { type: boolean }
        "404":
          description: The tracker or the row is not found, or the caller may not open the row
        "409":
          description: not_tracker, not_linked

  /conversations/{conversationId}/linked-issues:
    get:
      tags: [Tickets]
      summary: A ticket's linked Jira issues
      description: |
        The ticket's linked issues, the first linked first, each with its last known state from Jira
        (`syncedAt` is when Jira reported it; `deleted` once it was deleted in Jira, `notFound` when the
        hourly check no longer finds it). `jira` is the workspace's connection, null without one. Follows the
        ticket's read rules and works without the plan or a connection. Tokens need conversations:read.
      operationId: listLinkedIssues
      parameters:
        - $ref: "#/components/parameters/conversationId"
      responses:
        "200":
          description: The linked issues
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LinkedIssue" }
                  jira:
                    type: object
                    nullable: true
                    properties:
                      state: { type: string }
                      siteId: { type: string }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: The token lacks conversations:read, the caller may not read the ticket, or the plan's monthly cap locks it (plan_cap_locked)
        "404":
          description: The ticket is not found, or it is older than the plan's history window (outside_retention)
        "409":
          description: not_a_ticket
    post:
      tags: [Tickets]
      summary: Link a Jira issue to a ticket
      description: |
        Links an existing Jira issue by its key, read with the workspace's Jira connection, with a line in the
        ticket and a link back to the ticket in Jira, and sends the ticket's ticket.updated with
        `changes.linkedIssues`. An issue linked already answers 200 and changes nothing. Needs tickets switched
        on, the Enterprise plan and a working Jira connection. A token limited to some inboxes links only on
        tickets whose conversation is in them. Raising a new issue is for members in the dashboard only.
        Tokens need conversations:write.
      operationId: linkIssue
      parameters:
        - $ref: "#/components/parameters/conversationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key]
              properties:
                key: { type: string, description: "The issue key, such as TAB-12, any letter case" }
      responses:
        "201":
          description: Linked
          content:
            application/json:
              schema:
                type: object
                properties:
                  issue: { $ref: "#/components/schemas/LinkedIssue" }
        "200":
          description: Linked already
          content:
            application/json:
              schema:
                type: object
                properties:
                  issue: { $ref: "#/components/schemas/LinkedIssue" }
        "400":
          description: invalid_issue_key
        "403":
          description: The plan does not include tickets, the caller may not edit the ticket, or the token is limited to other inboxes
        "404":
          description: jira_not_found, Jira does not know the issue or the connected account may not see it
        "409":
          description: not_a_ticket, tickets_disabled, jira_not_connected, jira_reauth_required, jira_forbidden
        "502":
          description: jira_unavailable

  /conversations/{conversationId}/linked-issues/{issueId}:
    delete:
      tags: [Tickets]
      summary: Unlink a Jira issue from a ticket
      description: |
        `issueId` is the issue's `id` from listLinkedIssues. Unlinks it with a line in the ticket, removes the
        link back to the ticket in Jira and sends the ticket's ticket.updated with `changes.linkedIssues`. The
        issue stays in Jira. Needs what linking needs. Tokens need conversations:write.
      operationId: unlinkIssue
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: issueId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Unlinked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
        "403":
          description: The plan does not include tickets, the caller may not edit the ticket, or the token is limited to other inboxes
        "404":
          description: not_linked
        "409":
          description: not_a_ticket, tickets_disabled, jira_not_connected, jira_reauth_required

  /conversations/{conversationId}/linked-update:
    post:
      tags: [Tickets]
      summary: Update the customers linked to a tracker
      description: |
        `conversationId` is the tracker. Sends one text update into every conversation linked to it (a
        conversation and its tickets count as one, at most 500), as a reply there with its reply window and
        refusals, and resolves the linked rows and the tracker, as asked. `dryRun` answers what would happen
        and writes nothing. Every other request needs an Idempotency-Key header: a retry sends nothing again
        and answers `replayed` for what the first one sent. A person sends at most 10 updates a minute.
        Tokens need conversations:write; MCP tokens are refused with 403 mcp_not_allowed.
      operationId: updateLinkedCustomers
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                content: { type: string }
                contentType: { type: string, enum: [TEXT, HTML] }
                subject: { type: string }
                resolveLinked: { type: boolean }
                resolveTracker: { type: boolean }
                dryRun: { type: boolean }
      responses:
        "200":
          description: One recipient per linked row the caller may open
          content:
            application/json:
              schema:
                type: object
                properties:
                  recipients:
                    type: array
                    items:
                      type: object
                      properties:
                        conversationId: { type: string }
                        chatId: { type: string, nullable: true }
                        outcome: { type: string, enum: [sent, replayed, ready, same_chat, refused, failed] }
                        code: { type: string }
                        messageId: { type: string }
                  resolved: { type: array, items: { type: string } }
                  hiddenCount: { type: integer }
                  trackerResolved: { type: boolean }
                  unfinished: { type: boolean, description: A send broke off before its message was stored; a retry with the same key finishes it }
                  dryRun: { type: boolean }
        "400":
          description: nothing_to_do, idempotency_key_required
        "403":
          description: Not allowed, a draft only token with content, or mcp_not_allowed
        "409":
          description: not_tracker, idempotency_mismatch
        "413":
          description: too_many_recipients
        "429":
          description: rate_limited

  /conversations/{conversationId}/resolve-as-ticket:
    post:
      tags: [Tickets]
      summary: Resolve a conversation and file its interaction as a resolved ticket
      description: |
        Files the conversation's current interaction (what ticket-context selects by default) as a resolved
        ticket the caller creates and holds, with the conversation's team, the category asked for or else the
        conversation's own, the conversation's priority or else the workspace's default priority for new
        tickets, and a title from the interaction, then resolves the conversation. The ticket
        begins at the interaction's first customer message, so its resolution time is the interaction's.
        Nobody is notified. A refusal leaves the conversation as it was. When nothing was written since the
        last resolve or ticket, `ticket` is null and the conversation is still resolved. An Idempotency-Key
        makes a retry answer with the same ticket. Tokens need conversations:write.
      operationId: resolveAsTicket
      parameters:
        - $ref: "#/components/parameters/conversationId"
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                categoryId: { type: string, nullable: true }
      responses:
        "201":
          description: Filed and resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket: { allOf: [{ $ref: "#/components/schemas/Conversation" }], nullable: true }
                  resolved: { type: boolean }
        "200":
          description: A retry, or nothing to file (ticket null)
        "400":
          description: category_required, invalid_category
        "409":
          description: tickets_disabled, ticket_source, source_spam, category_archived, idempotency_mismatch
        "500":
          description: resolve_failed, with the ticket that was filed

  /conversations/{conversationId}/ticket-draft:
    post:
      tags: [Tickets]
      summary: Draft a ticket's title and summary with Copilot
      description: |
        A title and a summary written from these messages of the conversation only. Nothing is written and
        nothing reaches the customer. Dashboard sessions only; needs the Copilot (plan, AI quota, rate
        limits) and the conversations.notes_summarize permission, and is billed like a Copilot answer.
      operationId: draftTicket
      parameters:
        - $ref: "#/components/parameters/conversationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messageIds]
              additionalProperties: false
              properties:
                messageIds: { type: array, minItems: 1, maxItems: 500, items: { type: string } }
                locale: { type: string, maxLength: 16 }
      responses:
        "200":
          description: The draft
          content:
            application/json:
              schema:
                type: object
                properties:
                  title: { type: string }
                  summary: { type: string }
                  usedCount: { type: integer }
                  omittedCount: { type: integer }

  /accounts/{accountId}/ticket-categories:
    get:
      tags: [Tickets]
      summary: List categories
      description: The workspace's categories in their order, archived ones included. Tokens need conversations:read.
      operationId: listTicketCategories
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: Categories
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/TicketCategory" }
    post:
      tags: [Tickets]
      summary: Add a category
      description: At the end of the order. Session with settings.manage_ticket_categories, Enterprise plan. 409 name_taken when the name exists already (whatever the letter case).
      operationId: createTicketCategory
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 80 }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TicketCategory" }

  /accounts/{accountId}/ticket-categories/{categoryId}:
    patch:
      tags: [Tickets]
      summary: Rename, move or archive a category
      description: Session with settings.manage_ticket_categories, Enterprise plan.
      operationId: updateTicketCategory
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: categoryId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 80 }
                position: { type: integer, minimum: 0, description: The index in the order (archived ones included) it moves to }
                archived: { type: boolean }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TicketCategory" }
    delete:
      tags: [Tickets]
      summary: Delete a category
      description: Session with settings.manage_ticket_categories, Enterprise plan. 409 category_in_use while a conversation or ticket has it; archive it instead.
      operationId: deleteTicketCategory
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: categoryId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Deleted

  /accounts/{accountId}/trackers:
    post:
      tags: [Tickets]
      summary: Create a tracker
      description: |
        A tracker is one problem that many customers have (an outage, a bug): a ticket without a customer, so
        `contactId`, `sourceConversationId` and the inbox are null. Customers' conversations and tickets link
        to it (tracker-links). It is created open, with its summary as the first internal note. Needs tickets
        switched on and the Enterprise plan. A token limited to some inboxes cannot create one, as a tracker
        has no inbox. An Idempotency-Key header makes a repeat answer with the first tracker (`replayed:
        true`). Tokens need conversations:write.
      operationId: createTracker
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              additionalProperties: false
              properties:
                title: { type: string, minLength: 1, maxLength: 200 }
                summary: { type: string, maxLength: 20000, nullable: true, description: Stored as the tracker's first internal note }
                categoryId: { type: string, nullable: true, description: Required when the workspace requires a category; not archived }
                priority:
                  type: string
                  enum: [URGENT, HIGH, MEDIUM, LOW]
                  nullable: true
                  description: Left out, the workspace's default priority for new tickets. null means no priority.
                teamId: { type: string, nullable: true }
                assigneeId: { type: string, nullable: true, description: An active member of the workspace }
      responses:
        "201":
          description: Tracker created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket: { $ref: "#/components/schemas/Conversation" }
                  replayed: { type: boolean, description: Only on a repeat with the same Idempotency-Key }
        "400":
          description: invalid_title, invalid_summary, invalid_category, category_required, invalid_team, invalid_target, invalid_idempotency_key
        "403":
          description: The plan does not include tickets, the caller may not manage conversations, or the token is limited to some inboxes
        "409":
          description: tickets_disabled, category_archived, idempotency_in_progress

  /accounts/{accountId}/ticket-statuses:
    get:
      tags: [Tickets]
      summary: List ticket statuses
      description: The workspace's custom ticket statuses in their order, archived ones included. Tokens need conversations:read.
      operationId: listTicketStatuses
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: Ticket statuses
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/TicketStatus" }
    post:
      tags: [Tickets]
      summary: Add a ticket status
      description: At the end of the order, at most 50 per workspace (409 ticket_status_limit). Session with settings.manage_ticket_categories, Enterprise plan. 409 name_taken when the name exists already (whatever the letter case).
      operationId: createTicketStatus
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, baseStatus]
              properties:
                name: { type: string, minLength: 1, maxLength: 40 }
                baseStatus: { type: string, enum: [OPEN, PENDING, RESOLVED] }
                pausesSla: { type: boolean, description: Only under OPEN }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TicketStatus" }

  /accounts/{accountId}/ticket-statuses/{statusId}:
    patch:
      tags: [Tickets]
      summary: Rename, move or archive a ticket status
      description: Session with settings.manage_ticket_categories, Enterprise plan. A change of pausesSla counts from that moment on. The base status stays.
      operationId: updateTicketStatus
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: statusId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 40 }
                position: { type: integer, minimum: 0, description: The index in the order (archived ones included) it moves to }
                archived: { type: boolean }
                pausesSla: { type: boolean, description: Only under OPEN }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TicketStatus" }
    delete:
      tags: [Tickets]
      summary: Delete a ticket status
      description: Session with settings.manage_ticket_categories, Enterprise plan. 409 ticket_status_in_use while a ticket has it; archive it instead.
      operationId: deleteTicketStatus
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: statusId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Deleted

  /accounts/{accountId}/ticket-settings:
    get:
      tags: [Tickets]
      summary: Get the ticket settings
      description: "The workspace's ticket switches and what the plan allows. Session of any member of the workspace."
      operationId: getTicketSettings
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: Ticket settings
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/TicketSettings"
                  - type: object
                    properties:
                      jiraConnected: { type: boolean, description: "Jira is connected, also while it needs a new sign in" }
                      ticketSystem: { type: boolean, description: The ticket system has reached the workspace }
    patch:
      tags: [Tickets]
      summary: Change the ticket settings
      description: "Session with settings.manage_ticket_categories. Switching either on or setting a default priority needs the Enterprise plan; switching off and clearing the default priority (null) are always allowed."
      operationId: updateTicketSettings
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                ticketsEnabled: { type: boolean }
                ticketCategoryRequired: { type: boolean }
                ticketDefaultPriority: { type: string, enum: [URGENT, HIGH, MEDIUM, LOW], nullable: true, description: The default priority for new tickets; null for none }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TicketSettings" }
        "400":
          $ref: "#/components/responses/BadRequest"

  /accounts/{accountId}/contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      operationId: listContacts
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: identifier
          in: query
          description: Exact lookup by the contact's identifier (or the user id your site signs for Identity Verification). Returns at most one contact and ignores the other parameters.
          schema:
            type: string
        - name: telegramId
          in: query
          description: Exact lookup by Telegram chat id (a person's user id or a group's negative chat id). Returns at most one contact and ignores the other parameters.
          schema:
            type: string
        - name: search
          in: query
          description: Name, email or phone number. A phone number matches however it is written (07700 900000 finds +44 7700 900000).
          schema:
            type: string
        - name: segment
          in: query
          schema:
            type: string
            enum: [active-7d, active-30d, no-email, has-email, has-phone, new]
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/pageSize"
      responses:
        "200":
          description: Paginated list of contacts
          content:
            application/json:
              schema:
                type: object
                properties:
                  contacts:
                    type: array
                    items:
                      $ref: "#/components/schemas/Contact"
                  total:
                    type: integer
                  page:
                    type: integer
                  pageSize:
                    type: integer
    post:
      tags: [Contacts]
      summary: Create a contact
      operationId: createContact
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactCreate"
      responses:
        "201":
          description: Contact created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"

  /accounts/{accountId}/contacts/{contactId}:
    get:
      tags: [Contacts]
      summary: Get a contact
      operationId: getContact
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/contactId"
      responses:
        "200":
          description: Contact details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"
    patch:
      tags: [Contacts]
      summary: Update a contact
      operationId: updateContact
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/contactId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactUpdate"
      responses:
        "200":
          description: Updated contact
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"
    delete:
      tags: [Contacts]
      summary: Delete a contact
      operationId: deleteContact
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/contactId"
      responses:
        "200":
          description: Contact deleted

  /accounts/{accountId}/contacts/merge:
    post:
      tags: [Contacts]
      summary: Merge two contacts
      operationId: mergeContacts
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [primaryContactId, mergeContactIds]
              properties:
                primaryContactId:
                  type: string
                mergeContactIds:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Contacts merged

  /accounts/{accountId}/custom-attributes:
    get:
      tags: [Custom Attributes]
      summary: List custom attribute definitions
      description: Needs a token with conversations:read or contacts:read.
      operationId: listCustomAttributes
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: model
          in: query
          schema:
            type: string
            enum: [CONVERSATION, CONTACT]
      responses:
        "200":
          description: Definitions, oldest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CustomAttributeDefinition"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /accounts/{accountId}/inboxes:
    get:
      tags: [Inboxes]
      summary: List inboxes
      operationId: listInboxes
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of inboxes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Inbox"
    post:
      tags: [Inboxes]
      summary: Create an inbox
      operationId: createInbox
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, channelType]
              properties:
                name:
                  type: string
                channelType:
                  type: string
                  enum: [WEB, EMAIL, WHATSAPP, WHATSAPP_TWILIO, TELEGRAM, TWITTER, LINE, SMS, API]
                emailAddress:
                  type: string
                  format: email
                welcomeTitle:
                  type: string
                greeting:
                  type: string
                csatEnabled:
                  type: boolean
      responses:
        "201":
          description: Inbox created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"

  /accounts/{accountId}/inboxes/{inboxId}:
    get:
      tags: [Inboxes]
      summary: Get an inbox
      operationId: getInbox
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: inboxId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Inbox details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"
    patch:
      tags: [Inboxes]
      summary: Update an inbox
      operationId: updateInbox
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: inboxId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                welcomeTitle:
                  type: string
                greeting:
                  type: string
                csatEnabled:
                  type: boolean
      responses:
        "200":
          description: Updated inbox
    delete:
      tags: [Inboxes]
      summary: Delete an inbox
      operationId: deleteInbox
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: inboxId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Inbox deleted

  /accounts/{accountId}/labels:
    get:
      tags: [Labels]
      summary: List labels
      operationId: listLabels
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of labels
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Label"
    post:
      tags: [Labels]
      summary: Create a label
      operationId: createLabel
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title:
                  type: string
                description:
                  type: string
                color:
                  type: string
                showOnSidebar:
                  type: boolean
      responses:
        "201":
          description: Label created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Label"

  /accounts/{accountId}/labels/{labelId}:
    patch:
      tags: [Labels]
      summary: Update a label
      operationId: updateLabel
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: labelId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                color:
                  type: string
                showOnSidebar:
                  type: boolean
      responses:
        "200":
          description: Updated label
    delete:
      tags: [Labels]
      summary: Delete a label
      operationId: deleteLabel
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: labelId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Label deleted

  /accounts/{accountId}/teams:
    get:
      tags: [Teams]
      summary: List teams
      operationId: listTeams
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of teams
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Team"
    post:
      tags: [Teams]
      summary: Create a team
      operationId: createTeam
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                description:
                  type: string
                allowAutoAssign:
                  type: boolean
      responses:
        "201":
          description: Team created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Team"

  /accounts/{accountId}/agents:
    get:
      tags: [Agents]
      summary: List agents
      operationId: listAgents
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  agents:
                    type: array
                    items:
                      $ref: "#/components/schemas/Agent"
                  maxAgents:
                    type: integer
                  plan:
                    type: string
    post:
      tags: [Agents]
      summary: Invite an agent
      operationId: inviteAgent
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
                role:
                  type: string
                  enum: [ADMINISTRATOR, AGENT]
                  default: AGENT
                customRoleId:
                  type: string
      responses:
        "201":
          description: Agent invited

  /accounts/{accountId}/canned-responses:
    get:
      tags: [Canned Responses]
      summary: List canned responses
      operationId: listCannedResponses
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of canned responses
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CannedResponse"
    post:
      tags: [Canned Responses]
      summary: Create a canned response
      operationId: createCannedResponse
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [shortCode, content]
              properties:
                shortCode:
                  type: string
                content:
                  type: string
      responses:
        "201":
          description: Canned response created

  /accounts/{accountId}/automation-rules:
    get:
      tags: [Automation Rules]
      summary: List automation rules
      operationId: listAutomationRules
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of rules
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AutomationRule"
    post:
      tags: [Automation Rules]
      summary: Create an automation rule
      operationId: createAutomationRule
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, eventName]
              properties:
                name:
                  type: string
                description:
                  type: string
                active:
                  type: boolean
                  default: true
                eventName:
                  type: string
                  enum: [CONVERSATION_CREATED, CONVERSATION_UPDATED, MESSAGE_CREATED, TIME_ELAPSED, LINKED_ISSUE_UPDATED]
                  description: TIME_ELAPSED is a time rule, checked every 5 minutes on open and pending conversations. It needs exactly one timer condition (hours_since_last_activity, hours_since_status_change or hours_since_created, operator is_greater_than, a whole number of hours from 1 to 8760). LINKED_ISSUE_UPDATED runs once per ticket when a Jira issue is raised from it, linked, unlinked or changes its status, never on a resolved ticket; it needs Type is Ticket, and only it has the conditions linked_issue_change, linked_issue_status_category, linked_issue_previous_status_category (the category before a status_changed) and linked_issue_status.
                conditions:
                  type: array
                  items:
                    type: object
                actions:
                  type: array
                  items:
                    type: object
      responses:
        "201":
          description: Rule created

  /accounts/{accountId}/webhooks:
    get:
      tags: [Webhooks]
      summary: List webhooks
      operationId: listWebhooks
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of webhooks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Webhook"
    post:
      tags: [Webhooks]
      summary: Create a webhook
      description: |
        Create a webhook endpoint that receives HMAC-SHA256 signed event payloads.
        The signing secret (64 hex characters) is returned only in this response and
        should be stored securely. Requires the administrator role; API tokens also
        need the `write` scope.
      operationId: createWebhook
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                subscriptions:
                  description: |
                    Events to deliver. Omit or send an empty array to receive every event except
                    the ticket events. Dotted names are canonical; the underscored spelling of each
                    name (for example `conversation_created`) is accepted as an alias. An unknown
                    name is refused with 400.
                  type: array
                  default: []
                  items:
                    $ref: "#/components/schemas/WebhookEventName"
                enabled:
                  type: boolean
                  default: true
      responses:
        "201":
          description: Webhook created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Webhook"
                  - type: object
                    properties:
                      secret:
                        type: string
                        description: HMAC-SHA256 signing secret (only returned on creation)

  /accounts/{accountId}/webhooks/{webhookId}:
    patch:
      tags: [Webhooks]
      summary: Update a webhook
      operationId: updateWebhook
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                subscriptions:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEventName"
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated webhook
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      operationId: deleteWebhook
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webhook deleted

  /accounts/{accountId}/rest-hooks:
    post:
      tags: [REST Hooks]
      summary: Subscribe a REST hook
      operationId: subscribeRestHook
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, trigger]
              properties:
                url:
                  type: string
                  format: uri
                  description: Public HTTPS address. `target_url` is accepted too.
                trigger:
                  type: string
                  enum: [conversation.created, message.incoming, conversation.assigned, conversation.resolved, contact.created, csat.submitted]
                  description: |
                    `message.incoming` is a customer's message only. `conversation.assigned` fires when
                    someone now holds the conversation. `contact.created` fires for contacts added by a
                    teammate or through the API. `event` is accepted for `trigger`.
      responses:
        "201":
          description: Subscribed
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  trigger: { type: string }
                  url: { type: string }
                  createdAt: { type: string, format: date-time }
                  secret:
                    type: string
                    description: |
                      Signs every delivery of this hook: `X-Deskwoot-Signature` is `sha256=` followed by the
                      hex HMAC SHA256 of the raw body with this secret. Returned only here, once; keep it.
        "400": { description: Unknown trigger or a URL that is not public HTTPS }
        "403": { description: Not on Enterprise, not a REST API token, or the token's creator is no administrator }
        "409": { description: The workspace already has 100 REST hooks }
  /accounts/{accountId}/rest-hooks/{hookId}:
    delete:
      tags: [REST Hooks]
      summary: Unsubscribe a REST hook
      operationId: unsubscribeRestHook
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: hookId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "`deleted` is false when the hook was gone already."
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean }
  /accounts/{accountId}/rest-hooks/samples:
    get:
      tags: [REST Hooks]
      summary: Newest objects of a trigger
      description: The newest objects of a trigger, newest first, in the shape its REST hook delivers.
      operationId: restHookSamples
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: trigger
          in: query
          required: true
          schema:
            type: string
            enum: [conversation.created, message.incoming, conversation.assigned, conversation.resolved, contact.created, csat.submitted]
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 10, default: 3 }
      responses:
        "200":
          description: Objects of the trigger
          content:
            application/json:
              schema:
                type: array
                items: { type: object, additionalProperties: true }

  /accounts/{accountId}/campaigns:
    get:
      tags: [Campaigns]
      summary: List campaigns
      operationId: listCampaigns
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: type
          in: query
          schema:
            type: string
            enum: [ONGOING, ONE_OFF]
      responses:
        "200":
          description: List of campaigns
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Campaign"
    post:
      tags: [Campaigns]
      summary: Create a campaign
      operationId: createCampaign
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, message]
              properties:
                title:
                  type: string
                message:
                  type: string
                subject:
                  type: string
                campaignType:
                  type: string
                  enum: [ONGOING, ONE_OFF]
                  default: ONGOING
                enabled:
                  type: boolean
                inboxId:
                  type: string
                audience:
                  type: object
                  properties:
                    type:
                      type: string
                      enum: [all_contacts, segment, contacts]
                    segmentId:
                      type: string
                    contactIds:
                      type: array
                      items:
                        type: string
                scheduledAt:
                  type: string
                  format: date-time
      responses:
        "201":
          description: Campaign created

  /accounts/{accountId}/bots:
    get:
      tags: [Bots]
      summary: List AI bots
      operationId: listBots
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of bots (API keys masked)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Bot"
    post:
      tags: [Bots]
      summary: Create an AI bot
      operationId: createBot
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                description:
                  type: string
                aiProvider:
                  type: string
                  enum: [openai, anthropic]
                aiApiKey:
                  type: string
                aiModel:
                  type: string
                systemPrompt:
                  type: string
                useHelpCenter:
                  type: boolean
                maxTokens:
                  type: integer
                  minimum: 100
                  maximum: 2000
                temperature:
                  type: number
                  minimum: 0
                  maximum: 1
                collectEmailAtStart:
                  type: boolean
      responses:
        "201":
          description: Bot created

  /accounts/{accountId}/integrations:
    get:
      tags: [Integrations]
      summary: List integrations
      operationId: listIntegrations
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of integrations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Integration"
    post:
      tags: [Integrations]
      summary: Connect an integration
      description: |
        A Shopify shop connects only through its own app's consent under Settings,
        Integrations (400 `shopify_oauth_only` here). Every other provider is one
        connection per workspace; a workspace on Enterprise may run several Shopify
        shops.
      operationId: createIntegration
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider]
              properties:
                provider:
                  type: string
                  enum: [SHOPIFY, STRIPE, WOOCOMMERCE, SLACK]
                accessToken:
                  type: string
                apiKey:
                  type: string
                config:
                  type: object
      responses:
        "201":
          description: Integration connected

  /accounts/{accountId}/commerce/orders/{orderId}/refund-preview:
    post:
      tags: [Commerce]
      summary: Preview a refund
      description: |
        What a refund of the order would pay back, read from the store or Stripe now:
        the order's lines with how many of each can still be refunded, the shipping
        left, what was refunded already, what is left in all, and what the named lines
        and shipping pay back (`suggestedMinor`; the whole rest when nothing is named).
        Every amount in the answer is in the smallest unit of `currency` (1999 is
        19.99 EUR). Moves nothing. Needs the `commerce:refund` scope.
      operationId: previewRefund
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/commerceOrderId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefundSelection"
      responses:
        "200":
          description: What the refund would pay back
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview:
                    $ref: "#/components/schemas/RefundPreview"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: "The order's Shopify shop is paused because the plan covers fewer shops (`store_paused`)"
        "422":
          description: "A line the order does not have, more than is left of one (`unknown_line_item`, `quantity_above_refundable`, `shipping_above_refundable`), a restock the store cannot do (`restock_not_supported`), or lines of a Stripe payment or past a Shopify order's first 250 (`line_items_not_supported`)"
        "502":
          description: The store or Stripe could not be read (`provider_unreadable`)

  /accounts/{accountId}/commerce/orders/{orderId}/refund:
    post:
      tags: [Commerce]
      summary: Refund an order
      description: |
        Refunds everything that is left, an amount, or line items and the shipping of
        a Shopify or WooCommerce order, at the order's own provider. Several partial
        refunds may follow each other until nothing is left; the order's status reads
        `partially_refunded` in between and `refunded` at the end. Needs the exact
        `commerce:refund` scope. Moves real money.

        `amount` is a decimal in the order's currency for every provider, Stripe
        included: 20.50 refunds 20.50 EUR, 1000 refunds 1,000 JPY. An amount with more
        decimals than the currency has (4.995 EUR, 10.5 JPY) is refused with 422
        `invalid_amount` before anything is sent. With `lineItems` or `shipping` and no
        amount, the store computes what they pay back (see Preview a refund); with an
        amount as well, the lines are marked refunded and the amount is paid back.

        The refund leaves a team only note in the conversation named by
        `conversationId`, else in the conversation the order is linked to.
      operationId: refundOrder
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/commerceOrderId"
        - name: Idempotency-Key
          in: header
          description: |
            One key per refund you mean (1 to 200 printable characters, no spaces). A
            repeat with the same key answers the first refund with `replayed: true` and
            moves nothing, 409 while the first request still runs. The key also goes to
            the provider. Without a key every call is a new refund, so send one to retry
            safely.
          schema: { type: string, maxLength: 200 }
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/RefundSelection"
                - type: object
                  properties:
                    amount:
                      type: number
                      description: A decimal in the order's currency, the same for Shopify, WooCommerce and Stripe. Omit it to refund everything that is left, or what the named lines and shipping pay back.
                      example: 20.5
                    reason:
                      type: string
                      maxLength: 500
                      description: Goes to the store with the refund, and into the team only note.
                    notify:
                      type: boolean
                      description: Shopify emails the customer about the refund (default true).
                    conversationId:
                      type: string
                      description: The conversation the team only note goes to; one the token may read.
      responses:
        "200":
          description: Refunded, or nothing left to refund
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  refundId: { type: [string, number, "null"] }
                  status:
                    type: string
                    enum: [partially_refunded, refunded]
                  alreadyRefunded:
                    type: boolean
                    description: The order was refunded in full before; nothing was sent.
                  replayed:
                    type: boolean
                    description: The answer of the first request with this Idempotency-Key; nothing was sent now.
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: "A request with this Idempotency-Key still runs (`idempotency_in_progress`), or the order's Shopify shop is paused because the plan covers fewer shops (`store_paused`). Nothing was sent."
        "422":
          description: "`invalid_amount` (with `decimals`), `line_items_not_supported` (a Stripe payment, or a line past a Shopify order's first 250), `unknown_line_item`, `quantity_above_refundable`, `shipping_above_refundable`, `restock_not_supported`, `amount_above_refundable` or `nothing_to_refund`. Nothing was sent."
        "502":
          description: "The provider refused the refund, or gave no definite answer: with `code: outcome_unknown` the money may have moved, so check the order there before trying again."

  /accounts/{accountId}/portals:
    get:
      tags: [Help Center]
      summary: List help center portals
      operationId: listPortals
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of portals
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Portal"
    post:
      tags: [Help Center]
      summary: Create a portal
      operationId: createPortal
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, slug]
              properties:
                name:
                  type: string
                slug:
                  type: string
                headerText:
                  type: string
                pageTitle:
                  type: string
                brandColor:
                  type: string
      responses:
        "201":
          description: Portal created

  /accounts/{accountId}/portals/{portalId}/articles:
    get:
      tags: [Help Center]
      summary: List articles
      operationId: listArticles
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: portalId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: List of articles
    post:
      tags: [Help Center]
      summary: Create an article
      operationId: createArticle
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: portalId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, slug]
              properties:
                title:
                  type: string
                slug:
                  type: string
                content:
                  type: string
                categoryId:
                  type: string
                status:
                  type: string
                  enum: [DRAFT, PUBLISHED]
      responses:
        "201":
          description: Article created

  /accounts/{accountId}/sla-policies:
    get:
      tags: [SLA Policies]
      summary: List SLA policies
      operationId: listSlaPolicies
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: List of SLA policies
    post:
      tags: [SLA Policies]
      summary: Create an SLA policy
      operationId: createSlaPolicy
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                description:
                  type: string
                firstResponseTimeThreshold:
                  type: integer
                  description: Seconds
                nextResponseTimeThreshold:
                  type: integer
                resolutionTimeThreshold:
                  type: integer
                onlyDuringBusinessHours:
                  type: boolean
                priorityTargets:
                  type: object
                  nullable: true
                  description: Own targets for MEDIUM, HIGH or URGENT conversations, each a whole number of minutes from 1 to 43200. A priority or target left out uses the policy's own target, as do LOW and no priority.
                  properties:
                    MEDIUM: { $ref: "#/components/schemas/SlaPriorityTargets" }
                    HIGH: { $ref: "#/components/schemas/SlaPriorityTargets" }
                    URGENT: { $ref: "#/components/schemas/SlaPriorityTargets" }
                businessHoursScheduleId:
                  type: string
                  nullable: true
                  description: A business hours schedule of this workspace that the clocks count in under onlyDuringBusinessHours, after the team's schedule and the inbox's business hours.
      responses:
        "201":
          description: SLA policy created

  /accounts/{accountId}/business-hours-schedules:
    get:
      tags: [SLA Policies]
      summary: List business hours schedules
      description: The named weeks the SLA clocks count in, by name, with the teams and policies that use each. Session with settings.manage_teams.
      operationId: listBusinessHoursSchedules
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: Business hours schedules
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/BusinessHoursSchedule" }
    post:
      tags: [SLA Policies]
      summary: Add a business hours schedule
      description: At most 20 per workspace (409 schedule_limit), each name once (409 schedule_name_taken). A refused time answers 400 with a code and the field's path. Session of an administrator, plan with SLA policies.
      operationId: createBusinessHoursSchedule
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, timezone, schedule]
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                timezone: { type: string, description: A time zone like Europe/Berlin }
                schedule:
                  type: array
                  minItems: 7
                  maxItems: 7
                  items: { $ref: "#/components/schemas/BusinessHoursDay" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BusinessHoursSchedule" }

  /accounts/{accountId}/business-hours-schedules/{scheduleId}:
    patch:
      tags: [SLA Policies]
      summary: Change a business hours schedule
      description: Its name, timezone or days; the fields left out stay. The clocks that count in it follow at their next read. 409 schedule_name_taken when the name exists already. Session of an administrator, plan with SLA policies.
      operationId: updateBusinessHoursSchedule
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: scheduleId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                timezone: { type: string }
                schedule:
                  type: array
                  minItems: 7
                  maxItems: 7
                  items: { $ref: "#/components/schemas/BusinessHoursDay" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BusinessHoursSchedule" }
        "404":
          description: No schedule of this workspace has this id
    delete:
      tags: [SLA Policies]
      summary: Delete a business hours schedule
      description: The teams and policies that used it lose it, and their clocks count in the next schedule of the order. Works without the plan, so a workspace can tidy up after a downgrade. Session of an administrator.
      operationId: deleteBusinessHoursSchedule
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: scheduleId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Deleted
        "404":
          description: No schedule of this workspace has this id

  /accounts/{accountId}/reports:
    get:
      tags: [Reports]
      summary: Get reports
      operationId: getReports
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: days
          in: query
          schema:
            type: integer
            default: 7
        - name: type
          in: query
          description: What every number counts. An unknown type answers 400.
          schema: { type: string, enum: [conversations, tickets, all], default: conversations }
      responses:
        "200":
          description: Report data (the app's Reports tab and the MCP tool reports_get). Times in seconds.
          content:
            application/json:
              schema:
                type: object
                properties:
                  period:
                    type: object
                    properties:
                      days: { type: integer }
                      since: { type: string, format: date-time }
                  totalConversations: { type: integer, description: Conversations created or resolved in the period }
                  open: { type: integer, description: Open now }
                  resolved: { type: integer, description: Resolved in the period }
                  pending: { type: integer, description: Pending now }
                  avgResponseTime:
                    type: integer
                    nullable: true
                    description: "First response. For administrators and tokens (view: workspace) the customer's wait for the first reply, queue time included; for a member who is no administrator (view: own) their own first response, from the moment they had the conversation."
                  avgResolutionTime: { type: integer, nullable: true }
                  dailyVolume:
                    type: array
                    items:
                      type: object
                      properties:
                        date: { type: string, format: date }
                        count: { type: integer }
                  agentPerformance:
                    type: array
                    description: Administrators and tokens only. One row per agent who worked on a conversation in the period.
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        avatarUrl: { type: string, nullable: true }
                        conversationsHandled: { type: integer, description: Conversations the agent worked on in the period }
                        resolved: { type: integer, description: Of those, resolved now }
                        avgResponseTime: { type: integer, nullable: true, description: "The agent's own first response, from the moment they had the conversation" }
                        avgReplyTime: { type: integer, nullable: true }
                        replies: { type: integer }
                        pickedUp: { type: integer, description: Replies from someone who did not hold the conversation or took it with that reply (0 seconds) }
                  channelBreakdown:
                    type: array
                    items:
                      type: object
                      properties:
                        inboxId: { type: string }
                        inboxName: { type: string }
                        channelType: { type: string }
                        conversations: { type: integer }
                        avgResponseTime: { type: integer, nullable: true, description: The same rule as avgResponseTime at the top, per inbox }
                  view:
                    type: string
                    enum: [workspace, own]
                    description: "workspace: every conversation, avgResponseTime is the customer's wait. own: a member who is no administrator, the conversations assigned to them and their own first response."
                  type:
                    type: string
                    enum: [conversations, tickets, all]
                    description: What the numbers count, as asked.

  /accounts/{accountId}/reports/summary:
    get:
      tags: [Reports]
      summary: Report summary
      description: All headline numbers for the period and filters, with the period's reopens, the backlog now, the period's escalations and time in team, the next response and the SLA compliance. Tokens need conversations:read.
      operationId: getReportSummary
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/reportFrom"
        - $ref: "#/components/parameters/reportTo"
        - $ref: "#/components/parameters/reportDays"
        - $ref: "#/components/parameters/reportTimezone"
        - $ref: "#/components/parameters/reportAgent"
        - $ref: "#/components/parameters/reportTeam"
        - $ref: "#/components/parameters/reportInbox"
        - $ref: "#/components/parameters/reportChannel"
        - $ref: "#/components/parameters/reportLabel"
        - $ref: "#/components/parameters/reportStatus"
        - $ref: "#/components/parameters/reportPriority"
        - $ref: "#/components/parameters/reportType"
        - $ref: "#/components/parameters/reportCategory"
        - $ref: "#/components/parameters/reportTicketStatus"
        - $ref: "#/components/parameters/reportHours"
      responses:
        "200":
          description: Summary
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ReportEcho"
                  - type: object
                    properties:
                      metrics: { $ref: "#/components/schemas/ReportMetrics" }
                      allTimeConversations:
                        type: integer
                        description: Every conversation matching the filters, without the period; with agent_id, the conversations the agent ever held.

  /accounts/{accountId}/reports/breakdown:
    get:
      tags: [Reports]
      summary: Report metrics per group
      description: 'The summary metrics per agent, team, inbox, channel, label, status, priority, category, day, week or custom ticket status (ticket_status: with type=tickets the volume per queue; the null group holds tickets in a plain base status and every conversation). Groups without activity are included with zeros. An agent group holds the conversations that agent worked on in the period, so agent groups overlap and do not add up to the totals; the null group, "Unassigned", holds the period''s conversations nobody worked on. With type=tickets an agent group holds the tickets assigned to the agent, so they add up. Agent groups (and every group with agent_id) are agent scoped, so firstResponse is the agent''s own first response and replyTime is set. The CSV keeps its published columns in place and appends reply_time_count, reply_time_avg_s, reply_time_median_s, reply_time_p90_s, reply_time_min_s, reply_time_max_s and reply_time_picked_up, empty when the result is not agent scoped, then reopened, reopen_rate, backlog, backlog_under_1d, backlog_1_to_3d, backlog_3_to_7d, backlog_over_7d and backlog_oldest_since (the backlog cells empty for a day or week), then escalations, escalated_out, time_in_team_count, time_in_team_avg_s, time_in_team_median_s, time_in_team_p90_s, time_in_team_min_s and time_in_team_max_s (empty for a day or week), then next_response_count, next_response_avg_s, next_response_median_s, next_response_p90_s, next_response_min_s and next_response_max_s, then sla_first_response_achieved, sla_first_response_breached, sla_first_response_compliance and the same three for sla_next_response and sla_resolution (conversations and tickets together; compliance empty without an outcome, every sla_* cell empty on a plan without SLA).'
      operationId: getReportBreakdown
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/reportFrom"
        - $ref: "#/components/parameters/reportTo"
        - $ref: "#/components/parameters/reportDays"
        - $ref: "#/components/parameters/reportTimezone"
        - $ref: "#/components/parameters/reportAgent"
        - $ref: "#/components/parameters/reportTeam"
        - $ref: "#/components/parameters/reportInbox"
        - $ref: "#/components/parameters/reportChannel"
        - $ref: "#/components/parameters/reportLabel"
        - $ref: "#/components/parameters/reportStatus"
        - $ref: "#/components/parameters/reportPriority"
        - $ref: "#/components/parameters/reportType"
        - $ref: "#/components/parameters/reportCategory"
        - $ref: "#/components/parameters/reportTicketStatus"
        - $ref: "#/components/parameters/reportHours"
        - name: group_by
          in: query
          required: true
          schema: { type: string, enum: [agent, team, inbox, channel, label, status, priority, category, day, week, ticket_status] }
        - $ref: "#/components/parameters/reportFormat"
      responses:
        "200":
          description: Groups
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ReportEcho"
                  - type: object
                    properties:
                      groupBy: { type: string }
                      groups:
                        type: array
                        items:
                          type: object
                          properties:
                            key: { type: string, nullable: true, description: "Id or value; null = not set" }
                            name: { type: string }
                            meta: { type: object, additionalProperties: { type: string, nullable: true } }
                            metrics: { $ref: "#/components/schemas/ReportMetrics" }

  /accounts/{accountId}/reports/conversations:
    get:
      tags: [Reports]
      summary: Report rows per conversation
      description: One row per conversation of the period with status, priority, subject, agent (the current assignee), team, inbox, channel, labels, first reply and first response, resolution seconds, CSAT, with contacts:read the contact, and each agent's own first response. With agent_id, the conversations the agent worked on in the period. In an agent scoped list (agent_id, or a member who is no administrator) firstReplyAt and firstResponseSeconds are the agent's own; otherwise the conversation's first reply and the customer's wait. Newest first, cursor paged. The CSV keeps its published columns in place and appends agent_first_responses as name=seconds;name=seconds.
      operationId: getReportConversations
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/reportFrom"
        - $ref: "#/components/parameters/reportTo"
        - $ref: "#/components/parameters/reportDays"
        - $ref: "#/components/parameters/reportTimezone"
        - $ref: "#/components/parameters/reportAgent"
        - $ref: "#/components/parameters/reportTeam"
        - $ref: "#/components/parameters/reportInbox"
        - $ref: "#/components/parameters/reportChannel"
        - $ref: "#/components/parameters/reportLabel"
        - $ref: "#/components/parameters/reportStatus"
        - $ref: "#/components/parameters/reportPriority"
        - $ref: "#/components/parameters/reportType"
        - $ref: "#/components/parameters/reportCategory"
        - $ref: "#/components/parameters/reportTicketStatus"
        - $ref: "#/components/parameters/reportLimit"
        - $ref: "#/components/parameters/reportCursor"
        - $ref: "#/components/parameters/reportFormat"
      responses:
        "200":
          description: Conversations
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ReportEcho"
                  - type: object
                    properties:
                      conversations: { type: array, items: { $ref: "#/components/schemas/ReportConversationRow" } }
                      nextCursor: { type: string, nullable: true }
            text/csv:
              schema: { type: string }

  /accounts/{accountId}/reports/replies:
    get:
      tags: [Reports]
      summary: Report rows per agent reply
      description: One row per reply that counts in the agent response times (a person answering a waiting customer), sent in the period, newest first, cursor paged. agent_id picks whose replies are listed; the other filters apply to the conversation. A member who is no administrator gets only their own replies in the conversations assigned to them.
      operationId: getReportReplies
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/reportFrom"
        - $ref: "#/components/parameters/reportTo"
        - $ref: "#/components/parameters/reportDays"
        - $ref: "#/components/parameters/reportTimezone"
        - $ref: "#/components/parameters/reportAgent"
        - $ref: "#/components/parameters/reportTeam"
        - $ref: "#/components/parameters/reportInbox"
        - $ref: "#/components/parameters/reportChannel"
        - $ref: "#/components/parameters/reportLabel"
        - $ref: "#/components/parameters/reportStatus"
        - $ref: "#/components/parameters/reportPriority"
        - $ref: "#/components/parameters/reportType"
        - $ref: "#/components/parameters/reportCategory"
        - $ref: "#/components/parameters/reportTicketStatus"
        - $ref: "#/components/parameters/reportLimit"
        - $ref: "#/components/parameters/reportCursor"
        - $ref: "#/components/parameters/reportFormat"
      responses:
        "200":
          description: Replies
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ReportEcho"
                  - type: object
                    properties:
                      replies: { type: array, items: { $ref: "#/components/schemas/ReportReplyRow" } }
                      nextCursor: { type: string, nullable: true }
            text/csv:
              schema: { type: string }

  /accounts/{accountId}/reports/csat:
    get:
      tags: [Reports]
      summary: CSAT report
      description: CSAT totals, numbers per agent the survey was sent for, and every survey of the period. Cursor paged. A survey that waited for the conversation's open tickets names the ticket whose resolve sent it in `ticketId` (null otherwise).
      operationId: getReportCsat
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/reportFrom"
        - $ref: "#/components/parameters/reportTo"
        - $ref: "#/components/parameters/reportDays"
        - $ref: "#/components/parameters/reportTimezone"
        - $ref: "#/components/parameters/reportAgent"
        - $ref: "#/components/parameters/reportTeam"
        - $ref: "#/components/parameters/reportInbox"
        - $ref: "#/components/parameters/reportChannel"
        - $ref: "#/components/parameters/reportLabel"
        - $ref: "#/components/parameters/reportStatus"
        - $ref: "#/components/parameters/reportPriority"
        - $ref: "#/components/parameters/reportType"
        - $ref: "#/components/parameters/reportCategory"
        - $ref: "#/components/parameters/reportTicketStatus"
        - name: responded
          in: query
          description: "`true` keeps only rated surveys."
          schema: { type: boolean }
        - $ref: "#/components/parameters/reportLimit"
        - $ref: "#/components/parameters/reportCursor"
        - $ref: "#/components/parameters/reportFormat"
      responses:
        "200":
          description: CSAT
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ReportEcho"
                  - type: object
                    properties:
                      csat: { $ref: "#/components/schemas/ReportCsat" }
                      perAgent: { type: array, items: { type: object } }
                      responses: { type: array, items: { type: object } }
                      nextCursor: { type: string, nullable: true }
            text/csv:
              schema: { type: string }

  /accounts/{accountId}/reports/article-feedback:
    get:
      tags: [Reports]
      summary: Help center article feedback
      description: Helpful and not helpful votes per article in the period. Tokens need help_center:read.
      operationId: getReportArticleFeedback
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/reportFrom"
        - $ref: "#/components/parameters/reportTo"
        - $ref: "#/components/parameters/reportDays"
        - $ref: "#/components/parameters/reportTimezone"
      responses:
        "200":
          description: Articles

  /accounts/{accountId}/reports/export:
    get:
      tags: [Reports]
      summary: Reports page CSV download
      operationId: exportReports
      parameters:
        - $ref: "#/components/parameters/accountId"
        - name: type
          in: query
          description: "agents: one row per person. The published columns keep their places: Agent Name, Email, Role, Conversations (worked on in the range), Resolved, Avg Response Time (min) (the agent's own first response in whole minutes, empty when there is none). Then Replies, Picked Up Replies, First Response Avg (s), First Response Median (s), First Response P90 (s), Reply Time Avg (s), Reply Time Median (s), CSAT Score, CSAT Responses."
          schema: { type: string, enum: [conversations, agents, csat], default: conversations }
        - name: kind
          in: query
          description: "For type=conversations, what the file holds, as the Reports page's Type switch. With tickets or all the file gets the columns Type, Title, Category and Source Conversation ID after the published ones, and a ticket's Inbox is empty. An unknown kind answers 400."
          schema: { type: string, enum: [conversations, tickets, all], default: conversations }
        - name: range
          in: query
          schema: { type: string, enum: [7d, 30d, 90d], default: 7d }
        - name: format
          in: query
          description: csv, or json for type=agents (the same fields).
          schema: { type: string, enum: [csv, json], default: csv }
      responses:
        "200":
          description: The CSV file, or with type=agents and format=json the same numbers as JSON.
          content:
            text/csv:
              schema: { type: string }
            application/json:
              schema:
                type: object
                description: type=agents with format=json only.
                properties:
                  period:
                    type: object
                    properties:
                      from: { type: string, format: date-time }
                      to: { type: string, format: date-time }
                  agents:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        email: { type: string }
                        role: { type: string, enum: [ADMINISTRATOR, AGENT] }
                        conversations: { type: integer, description: "Conversations the agent worked on in the range" }
                        resolved: { type: integer }
                        replies: { type: integer, description: "The agent's counted replies to a waiting customer" }
                        pickedUpReplies: { type: integer, description: "Of those, the replies sent without holding the conversation or that took it (0 s)" }
                        firstResponseAvgSeconds: { type: number, nullable: true, description: "The agent's own first response; null when the agent answered no waiting customer" }
                        firstResponseMedianSeconds: { type: number, nullable: true }
                        firstResponseP90Seconds: { type: number, nullable: true }
                        replyTimeAvgSeconds: { type: number, nullable: true }
                        replyTimeMedianSeconds: { type: number, nullable: true }
                        csatScore: { type: number, nullable: true }
                        csatResponses: { type: integer }

  /accounts/{accountId}/settings:
    get:
      tags: [Settings]
      summary: Get account settings
      operationId: getSettings
      parameters:
        - $ref: "#/components/parameters/accountId"
      responses:
        "200":
          description: Account settings
    patch:
      tags: [Settings]
      summary: Update account settings
      operationId: updateSettings
      parameters:
        - $ref: "#/components/parameters/accountId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                locale:
                  type: string
                timezone:
                  type: string
                autoResolveEnabled:
                  type: boolean
                autoResolveDuration:
                  type: integer
                brandColor:
                  type: string
                  pattern: "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
                  description: "Brand color of the chat widget: #rgb or #rrggbb, stored as #rrggbb in lower case. A color with transparency (#rrggbbaa) is refused with 400."
                liveTranslateEnabled:
                  type: boolean
                liveTranslateTargetLang:
                  type: string
                agentSeats:
                  type: integer
      responses:
        "200":
          description: Settings updated


webhooks:
  conversation.created:
    post:
      summary: A new conversation was started
      description: |
        Sent when a conversation is created via the API or the live chat widget.
        `data.conversation` is the full conversation object. API created conversations
        also carry `data.messageId` (the first message); widget created ones carry a
        `data.contact` summary instead.
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: conversation.created
                    data:
                      type: object
                      required: [conversation]
                      properties:
                        conversation:
                          $ref: "#/components/schemas/Conversation"
                        messageId:
                          type: string
                        contact:
                          type: object
                          properties:
                            id: { type: string }
                            name: { type: string, nullable: true }
                            email: { type: string, nullable: true }
            example:
              event: conversation.created
              timestamp: "2026-03-14T12:00:00.000Z"
              accountId: clx_acc1
              data:
                conversation:
                  id: clx_conv1
                  displayId: 1042
                  accountId: clx_acc1
                  inboxId: clx_inbox1
                  contactId: clx_contact1
                  status: OPEN
                  priority: null
                  assigneeId: null
                  teamId: null
                  subject: Order not delivered
                  createdAt: "2026-03-14T12:00:00.000Z"
                messageId: clx_msg1
      responses:
        "2xx":
          description: Acknowledge receipt. Anything else is retried once after 5 seconds.

  conversation.updated:
    post:
      summary: A conversation's attributes changed
      description: "`data.changes` contains exactly the fields sent in the PATCH request (status, assigneeId, teamId, priority, ...)."
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: conversation.updated
                    data:
                      type: object
                      required: [conversationId, changes]
                      properties:
                        conversationId: { type: string }
                        changes: { type: object, additionalProperties: true }
            example:
              event: conversation.updated
              timestamp: "2026-03-14T12:01:00.000Z"
              accountId: clx_acc1
              data:
                conversationId: clx_conv1
                changes: { status: PENDING, priority: HIGH }
      responses:
        "2xx":
          description: Acknowledge receipt.

  conversation.assigned:
    post:
      summary: The assignee of a conversation changed
      description: "`data.assigneeId` is null when the conversation was unassigned."
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: conversation.assigned
                    data:
                      type: object
                      required: [conversationId]
                      properties:
                        conversationId: { type: string }
                        assigneeId: { type: string, nullable: true }
                        previousAssigneeId: { type: string, nullable: true }
            example:
              event: conversation.assigned
              timestamp: "2026-03-14T12:02:00.000Z"
              accountId: clx_acc1
              data:
                conversationId: clx_conv1
                assigneeId: clx_user2
                previousAssigneeId: clx_user1
      responses:
        "2xx":
          description: Acknowledge receipt.

  conversation.resolved:
    post:
      summary: A conversation was resolved
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: conversation.resolved
                    data:
                      type: object
                      required: [conversationId, resolvedBy]
                      properties:
                        conversationId: { type: string }
                        resolvedBy:
                          type: string
                          description: User id of the agent who resolved it
            example:
              event: conversation.resolved
              timestamp: "2026-03-14T12:30:00.000Z"
              accountId: clx_acc1
              data:
                conversationId: clx_conv1
                resolvedBy: clx_user2
      responses:
        "2xx":
          description: Acknowledge receipt.

  message.created:
    post:
      summary: A message was sent or received
      description: Sent for agent replies, incoming customer messages and approved bot drafts.
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: message.created
                    data:
                      type: object
                      required: [conversationId, message]
                      properties:
                        conversationId: { type: string }
                        message:
                          $ref: "#/components/schemas/Message"
            example:
              event: message.created
              timestamp: "2026-03-14T12:05:00.000Z"
              accountId: clx_acc1
              data:
                conversationId: clx_conv1
                message:
                  id: clx_msg2
                  conversationId: clx_conv1
                  senderId: clx_user2
                  content: Your order ships tomorrow.
                  contentType: TEXT
                  messageType: OUTGOING
                  private: false
                  status: SENT
                  createdAt: "2026-03-14T12:05:00.000Z"
      responses:
        "2xx":
          description: Acknowledge receipt.

  contact.created:
    post:
      summary: A contact was created
      description: "`data` is the full contact object itself."
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: contact.created
                    data:
                      $ref: "#/components/schemas/Contact"
            example:
              event: contact.created
              timestamp: "2026-03-14T11:58:00.000Z"
              accountId: clx_acc1
              data:
                id: clx_contact1
                accountId: clx_acc1
                name: Anna Example
                email: anna@example.com
                phone: "+4930123456"
                company: null
                customAttributes: {}
                createdAt: "2026-03-14T11:58:00.000Z"
      responses:
        "2xx":
          description: Acknowledge receipt.

  contact.updated:
    post:
      summary: A contact was updated
      description: "`data.changes` lists the names of the fields that were sent in the update."
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: contact.updated
                    data:
                      type: object
                      required: [contact, changes]
                      properties:
                        contact:
                          $ref: "#/components/schemas/Contact"
                        changes:
                          type: array
                          items: { type: string }
            example:
              event: contact.updated
              timestamp: "2026-03-14T12:10:00.000Z"
              accountId: clx_acc1
              data:
                contact:
                  id: clx_contact1
                  name: Anna Example
                  email: anna@example.com
                  phone: "+4930123456"
                  company: Example GmbH
                changes: [company, phone]
      responses:
        "2xx":
          description: Acknowledge receipt.

  csat.submitted:
    post:
      summary: A customer submitted a satisfaction rating
      description: "`data.feedback` is null when the customer left no comment."
      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: csat.submitted
                    data:
                      type: object
                      required: [conversationId, rating, contactId]
                      properties:
                        conversationId: { type: string }
                        rating: { type: integer, minimum: 1, maximum: 5 }
                        feedback: { type: string, nullable: true }
                        contactId: { type: string }
            example:
              event: csat.submitted
              timestamp: "2026-03-14T13:00:00.000Z"
              accountId: clx_acc1
              data:
                conversationId: clx_conv1
                rating: 5
                feedback: Fast and friendly, thank you!
                contactId: clx_contact1
      responses:
        "2xx":
          description: Acknowledge receipt.

  ticket.created:
    post:
      summary: A ticket was created
      description: |
        Sent when a ticket is created from a conversation, and when an interaction is filed as a resolved
        ticket (which also sends ticket.resolved).
        Sent only to a webhook that names it: an empty `subscriptions` list does not include the ticket
        events. A ticket never sends a conversation.* event. The summary's text is never sent. One change
        can send several ticket events, each delivered on its own, so they can arrive in any order: key on
        `ticketId` and read the state from `ticket`, the ticket when that event was sent.

      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: ticket.created
                    data:
                      type: object
                      required: [ticketId, ticket]
                      properties:
                        ticketId: { type: string }
                        ticket: { $ref: "#/components/schemas/TicketEventTicket" }
            example:
              event: ticket.created
              timestamp: "2026-10-19T09:05:00.000Z"
              accountId: clx_acc1
              data:
                ticketId: clx_ticket1
                ticket:
                  id: clx_ticket1
                  number: 1043
                  title: Charged twice for order 1001
                  status: OPEN
                  priority: HIGH
                  category: { id: clx_cat_billing, name: Billing }
                  team: { id: clx_team_accounts, name: Accounts }
                  assignee: { id: clx_user2, name: Ben }
                  creator: { id: clx_user1, name: Ana }
                  sourceConversationId: clx_conv1
                  contactId: clx_contact1
                  createdAt: "2026-10-19T09:00:00.000Z"
                  resolvedAt: null
                  labels: [{ id: clx_label_vip, title: vip }]
                  customAttributes: { order_id: "1001" }
      responses:
        "2xx":
          description: Acknowledge receipt.

  ticket.updated:
    post:
      summary: A ticket's fields changed
      description: |
        `data.changes` holds what its conversation.updated event would: the fields of the PATCH request, the
        status of a status change (with `source`), or teamId when a transfer moves it. `changes.summary` is
        true when the summary changed. A Jira issue raised from the ticket, linked to it, unlinked from it or
        with a new status in Jira sends `changes.linkedIssues` { change, key, statusName, statusCategory },
        change being raised, linked, unlinked or status_changed; a new status comes with `source` jira.
        Sent only to a webhook that names it: an empty `subscriptions` list does not include the ticket
        events. A ticket never sends a conversation.* event. The summary's text is never sent. One change
        can send several ticket events, each delivered on its own, so they can arrive in any order: key on
        `ticketId` and read the state from `ticket`, the ticket when that event was sent.

      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: ticket.updated
                    data:
                      type: object
                      required: [ticketId, ticket]
                      properties:
                        ticketId: { type: string }
                        ticket: { $ref: "#/components/schemas/TicketEventTicket" }
                        changes: { type: object, additionalProperties: true }
                        source: { type: string }
            example:
              event: ticket.updated
              timestamp: "2026-10-19T09:05:00.000Z"
              accountId: clx_acc1
              data:
                ticketId: clx_ticket1
                ticket:
                  id: clx_ticket1
                  number: 1043
                  title: Charged twice for order 1001
                  status: OPEN
                  priority: HIGH
                  category: { id: clx_cat_billing, name: Billing }
                  team: { id: clx_team_accounts, name: Accounts }
                  assignee: { id: clx_user2, name: Ben }
                  creator: { id: clx_user1, name: Ana }
                  sourceConversationId: clx_conv1
                  contactId: clx_contact1
                  createdAt: "2026-10-19T09:00:00.000Z"
                  resolvedAt: null
                  labels: [{ id: clx_label_vip, title: vip }]
                  customAttributes: { order_id: "1001" }
                changes: { priority: HIGH }
      responses:
        "2xx":
          description: Acknowledge receipt.

  ticket.assigned:
    post:
      summary: The assignee of a ticket changed
      description: |
        `data.assigneeId` is null when the ticket was unassigned.
        Sent only to a webhook that names it: an empty `subscriptions` list does not include the ticket
        events. A ticket never sends a conversation.* event. The summary's text is never sent. One change
        can send several ticket events, each delivered on its own, so they can arrive in any order: key on
        `ticketId` and read the state from `ticket`, the ticket when that event was sent.

      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: ticket.assigned
                    data:
                      type: object
                      required: [ticketId, ticket]
                      properties:
                        ticketId: { type: string }
                        ticket: { $ref: "#/components/schemas/TicketEventTicket" }
                        assigneeId: { type: string, nullable: true }
                        previousAssigneeId: { type: string, nullable: true }
            example:
              event: ticket.assigned
              timestamp: "2026-10-19T09:05:00.000Z"
              accountId: clx_acc1
              data:
                ticketId: clx_ticket1
                ticket:
                  id: clx_ticket1
                  number: 1043
                  title: Charged twice for order 1001
                  status: OPEN
                  priority: HIGH
                  category: { id: clx_cat_billing, name: Billing }
                  team: { id: clx_team_accounts, name: Accounts }
                  assignee: { id: clx_user2, name: Ben }
                  creator: { id: clx_user1, name: Ana }
                  sourceConversationId: clx_conv1
                  contactId: clx_contact1
                  createdAt: "2026-10-19T09:00:00.000Z"
                  resolvedAt: null
                  labels: [{ id: clx_label_vip, title: vip }]
                  customAttributes: { order_id: "1001" }
                assigneeId: clx_user2
                previousAssigneeId: null
      responses:
        "2xx":
          description: Acknowledge receipt.

  ticket.resolved:
    post:
      summary: A ticket was resolved
      description: |
        `data.resolvedBy` is the user id of whoever resolved it, or `contact` or `system` (a ticket filed when its
        chat was resolved names who resolved the chat, as that chat's conversation.resolved does).
        Sent only to a webhook that names it: an empty `subscriptions` list does not include the ticket
        events. A ticket never sends a conversation.* event. The summary's text is never sent. One change
        can send several ticket events, each delivered on its own, so they can arrive in any order: key on
        `ticketId` and read the state from `ticket`, the ticket when that event was sent.

      parameters:
        - $ref: "#/components/parameters/webhookEventHeader"
        - $ref: "#/components/parameters/webhookSignatureHeader"
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    event:
                      const: ticket.resolved
                    data:
                      type: object
                      required: [ticketId, ticket]
                      properties:
                        ticketId: { type: string }
                        ticket: { $ref: "#/components/schemas/TicketEventTicket" }
                        resolvedBy:
                          type: string
                          description: "The user id of whoever resolved it, or contact or system"
                        source: { type: string }
            example:
              event: ticket.resolved
              timestamp: "2026-10-19T09:05:00.000Z"
              accountId: clx_acc1
              data:
                ticketId: clx_ticket1
                ticket:
                  id: clx_ticket1
                  number: 1043
                  title: Charged twice for order 1001
                  status: RESOLVED
                  priority: HIGH
                  category: { id: clx_cat_billing, name: Billing }
                  team: { id: clx_team_accounts, name: Accounts }
                  assignee: { id: clx_user2, name: Ben }
                  creator: { id: clx_user1, name: Ana }
                  sourceConversationId: clx_conv1
                  contactId: clx_contact1
                  createdAt: "2026-10-19T09:00:00.000Z"
                  resolvedAt: "2026-10-19T09:05:00.000Z"
                  labels: [{ id: clx_label_vip, title: vip }]
                  customAttributes: { order_id: "1001" }
                resolvedBy: clx_user2
                source: manual
      responses:
        "2xx":
          description: Acknowledge receipt.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API token (prefix: dw_). Generate in Settings > API Tokens."

  parameters:
    reportFrom:
      name: from
      in: query
      description: Start of the period. A date (YYYY-MM-DD, start of that day in `timezone`) or an ISO 8601 timestamp. Use from/to or days.
      schema: { type: string }
    reportTo:
      name: to
      in: query
      description: End of the period. A date is inclusive (the whole day), a timestamp exclusive. Default now.
      schema: { type: string }
    reportDays:
      name: days
      in: query
      description: Instead of from/to, the last N days up to now.
      schema: { type: integer, minimum: 1, maximum: 366, default: 30 }
    reportTimezone:
      name: timezone
      in: query
      description: IANA timezone name for dates and day/week buckets (offsets like +01:00 are not accepted).
      schema: { type: string, default: UTC }
    reportAgent:
      name: agent_id
      in: query
      description: Comma separated agent ids. Selects the conversations the agent worked on in the period (replied in, was assigned, or held when it was resolved) instead of the current assignee, and makes the result agent scoped (firstResponse is the agent's own, counting only that agent's replies). `unassigned` matches the period's conversations nobody worked on. With type=tickets it selects the tickets assigned to the agent (`unassigned`: the tickets nobody is assigned).
      schema: { type: string }
    reportTeam:
      name: team_id
      in: query
      description: Comma separated team ids; `none` matches conversations without a team.
      schema: { type: string }
    reportInbox:
      name: inbox_id
      in: query
      description: Comma separated inbox ids. A ticket has no inbox of its own; with type=tickets or all it matches by the inbox of the chat it came from. `none` matches the rows without one (a conversation whose inbox was deleted, a ticket from no chat).
      schema: { type: string }
    reportChannel:
      name: channel
      in: query
      description: Comma separated channels (WEBSITE, EMAIL, WHATSAPP, WHATSAPP_TWILIO, TELEGRAM, TWITTER, SMS, LINE, API). With type=tickets or all a ticket matches by the channel of the chat it came from.
      schema: { type: string }
    reportLabel:
      name: label_id
      in: query
      description: Comma separated label ids (any of them); `none` matches conversations without a label.
      schema: { type: string }
    reportStatus:
      name: status
      in: query
      description: Comma separated statuses (OPEN, PENDING, RESOLVED, SNOOZED).
      schema: { type: string }
    reportPriority:
      name: priority
      in: query
      description: Comma separated priorities (urgent, high, medium, low); `none` matches conversations without a priority.
      schema: { type: string }
    reportType:
      name: type
      in: query
      description: conversations (the default, customer conversations only), tickets or all. Read by summary, breakdown, conversations, replies and csat (not by article-feedback, nor by export, which has `kind`). With tickets, group_by=category or team gives tickets by category or by team, group_by=inbox or channel by the inbox or channel of the chat each ticket came from (tickets from no chat in the null group, named "No chat"; with all a ticket counts there too), and group_by=agent by assignee.
      schema: { type: string, enum: [conversations, tickets, all], default: conversations }
    reportCategory:
      name: category_id
      in: query
      description: Comma separated category ids of conversations or tickets (see listTicketCategories); `none` matches the ones without a category.
      schema: { type: string }
    reportTicketStatus:
      name: ticket_status_id
      in: query
      description: Comma separated custom ticket status ids; `none` matches tickets in a plain base status and every conversation.
      schema: { type: string }
    reportHours:
      name: hours
      in: query
      description: "calendar (the default) counts every hour; business counts firstResponse, nextResponse, resolution and replyTime only inside business hours: the team's schedule, else the inbox's business hours (conversations only, when switched on), else the schedule of the SLA policy that applies while it counts business hours only, else every hour. Team and policy schedules count on plans with SLA. Time in team stays calendar time. Read by summary and breakdown; every other reports endpoint answers 400 to business."
      schema: { type: string, enum: [calendar, business], default: calendar }
    reportLimit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
    reportCursor:
      name: cursor
      in: query
      description: nextCursor of the previous page.
      schema: { type: string }
    reportFormat:
      name: format
      in: query
      description: "`csv` returns a CSV file instead of JSON (up to 100,000 rows; a bigger period answers 400). 30 CSV downloads per workspace an hour."
      schema: { type: string, enum: [csv] }
    webhookEventHeader:
      name: X-Deskwoot-Event
      in: header
      required: true
      description: Dotted event name of this delivery.
      schema:
        $ref: "#/components/schemas/WebhookEventName"
    webhookSignatureHeader:
      name: X-Deskwoot-Signature
      in: header
      required: true
      description: "`sha256=` followed by the hex HMAC-SHA256 of the raw request body, keyed with the webhook secret."
      schema:
        type: string
        pattern: "^sha256=[0-9a-f]{64}$"
    accountId:
      name: accountId
      in: path
      required: true
      schema:
        type: string
    commerceOrderId:
      name: orderId
      in: path
      required: true
      description: The Deskwoot order id, the store's own order id, or a Stripe charge id (ch_...).
      schema:
        type: string
    conversationId:
      name: conversationId
      in: path
      required: true
      schema:
        type: string
    contactId:
      name: contactId
      in: path
      required: true
      schema:
        type: string
    page:
      name: page
      in: query
      schema:
        type: integer
        default: 1
    pageSize:
      name: pageSize
      in: query
      schema:
        type: integer
        default: 25
        maximum: 100

  schemas:
    RefundSelection:
      type: object
      description: The line items and shipping a refund names (Shopify and WooCommerce).
      properties:
        lineItems:
          type: array
          description: Each line once, by the store's line item id.
          items:
            type: object
            required: [id, quantity]
            properties:
              id:
                type: string
                description: The store's line item id (`lines[].id` of the preview).
              quantity:
                type: integer
                minimum: 1
              restock:
                type: boolean
                description: "Put the items back in stock. WooCommerce restocks every line of a refund or none, so mixed values answer 422 `restock_not_supported`; so does a Shopify line partly shipped when more than its unshipped units are restocked."
        shipping:
          type: object
          description: "All the shipping that is left (`full: true`) or an amount of it, a decimal in the order's currency."
          properties:
            full: { type: boolean }
            amount: { type: number }
    RefundPreview:
      type: object
      properties:
        provider:
          type: string
          enum: [SHOPIFY, WOOCOMMERCE, STRIPE]
        orderId: { type: string }
        orderNumber: { type: [string, "null"] }
        currency: { type: string, example: EUR }
        decimals:
          type: integer
          description: Decimals of the currency (2 for EUR, 0 for JPY, 3 for KWD). The refund's `amount` may have at most these.
        totalMinor: { type: integer, description: "The order or payment total, in the smallest unit." }
        refundedMinor: { type: integer }
        maxRefundableMinor: { type: integer }
        suggestedMinor:
          type: integer
          description: What the named lines and shipping pay back as the store computes it; the whole rest when nothing is named.
        taxMinor: { type: [integer, "null"] }
        lines:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              title: { type: string }
              variant: { type: [string, "null"] }
              sku: { type: [string, "null"] }
              quantity: { type: integer }
              refundableQuantity: { type: integer }
              unitMinor: { type: [integer, "null"] }
              restockable: { type: boolean }
        shipping:
          type: [object, "null"]
          properties:
            refundableMinor: { type: integer }
        supports:
          type: object
          properties:
            lineItems: { type: boolean }
            shipping: { type: boolean }
            restock: { type: boolean }
            restockPerLine: { type: boolean }
        source:
          type: string
          enum: [provider, local]
    ReportTiming:
      type: object
      properties:
        count: { type: integer }
        avgSeconds: { type: integer, nullable: true }
        medianSeconds: { type: integer, nullable: true }
        p90Seconds: { type: integer, nullable: true }
        minSeconds: { type: integer, nullable: true }
        maxSeconds: { type: integer, nullable: true }
    ReportCsat:
      type: object
      properties:
        sent: { type: integer }
        responded: { type: integer }
        responseRate: { type: number, nullable: true, description: "responded / sent, 0 to 1" }
        avgRating: { type: number, nullable: true, description: Mean star rating 1 to 5 }
        score: { type: integer, nullable: true, description: Mean rating as a percentage (5 stars = 100) }
        satisfiedRate: { type: number, nullable: true, description: Share of 4 and 5 star ratings, 0 to 1 }
        distribution:
          type: object
          properties:
            "1": { type: integer }
            "2": { type: integer }
            "3": { type: integer }
            "4": { type: integer }
            "5": { type: integer }
    ReportMetrics:
      type: object
      properties:
        conversations:
          type: integer
          description: Conversations created or resolved in the period. With agent_id and in an agent group, the conversations the agent worked on in the period (replied in, was assigned, or held when it was resolved), wherever they were created.
        created: { type: integer, description: Of those, created in the period }
        resolved: { type: integer, description: Of those, resolved in the period }
        resolutionRate: { type: number, nullable: true, description: "resolved / conversations, 0 to 1" }
        status:
          type: object
          description: Current status of the conversations counted
          properties:
            open: { type: integer }
            pending: { type: integer }
            snoozed: { type: integer }
            resolved: { type: integer }
        unanswered: { type: integer, description: Created in the period, the customer asked something, not resolved, no first reply yet }
        firstResponse:
          description: "Outside agent scoped results, the customer's wait: from the customer's first message to the first reply a person or an API integration sent, queue time included, over the conversations created in the period (a conversation an agent started counts from the customer's first answer). In agent scoped results (group_by=agent, agent_id, or a member who is no administrator), the agents' own first response: each agent's first reply to a waiting customer per conversation, measured from the oldest unanswered customer message but never from before the agent had the conversation, reported in the period that holds the reply. A member's mail from their own mail client counts as their reply; a reply from an API integration, a mail sent from an address that is no member's, and a resolve end the wait without counting for anyone. The customer's wait is not repeated per agent."
          allOf:
            - $ref: "#/components/schemas/ReportTiming"
            - type: object
              properties:
                over1h: { type: integer }
                over4h: { type: integer }
                over24h: { type: integer }
        resolution: { $ref: "#/components/schemas/ReportTiming" }
        csat: { $ref: "#/components/schemas/ReportCsat" }
        replyTime:
          description: Agent scoped results only, null otherwise. Every reply a person sent to a waiting customer in the period, measured like the agents' first response.
          nullable: true
          allOf:
            - $ref: "#/components/schemas/ReportTiming"
            - type: object
              properties:
                pickedUp: { type: integer, description: Replies from someone who did not hold the conversation or took it with that reply (0 seconds) }
        reopens:
          type: object
          nullable: true
          description: "Summary and breakdown. Reopens in the period: a status change out of RESOLVED (not into spam), counted when it happens and from the status history, which starts with the release that records it. A group chat's conversation has none, its tickets do. An import counts from its first message written here, and the reopen of a resolve made in the other helpdesk is none. Per group, the group the conversation has now."
          properties:
            reopened: { type: integer, description: Reopens in the period; an item reopened twice counts twice }
            resolves: { type: integer, description: "The resolves in the period from the same history, the basis of rate" }
            rate: { type: number, nullable: true, description: "reopened over the resolves in the period from the same history (status changes into RESOLVED from another status than RESOLVED or spam), whatever the status now; a resolve the history did not record (one from before it) counts nowhere, even once it is reopened; null without one" }
        backlog:
          type: object
          nullable: true
          description: "Summary and breakdown, null for a day or week. What is open or pending now, whatever the period; every other filter applies. Age counts from the last reopen, or from the creation without one. Snoozed items and imports nobody has written in since are left out. Per group, the group the conversation has now (per agent, the agent who holds it)."
          properties:
            unresolved: { type: integer, description: Open and pending items now }
            buckets:
              type: object
              properties:
                under1d: { type: integer }
                from1to3d: { type: integer }
                from3to7d: { type: integer }
                over7d: { type: integer, description: 7 days or more }
            oldest:
              type: object
              nullable: true
              description: The item waiting longest
              properties:
                id: { type: string }
                since: { type: string, format: date-time }
                ageSeconds: { type: integer }
        teamMoves:
          type: object
          nullable: true
          description: "Summary and breakdown, null for a day or week. From the team history, which starts with the release that records it. An escalation is a move to another team than the one before (also after a while without a team), dated when it happened; the first team is none. Per team group and with team_id, moves in and time in team follow the team the conversation was in, moves out the team it left; every other filter and group reads the conversation as it is now. An import counts from its first message written here, so imports nobody has written in since are left out."
          properties:
            escalations: { type: integer, description: Moves into a team in the period (per team, into that team) }
            escalatedOut: { type: integer, description: Moves out of a team in the period (per team, out of that team) }
            timeInTeam:
              description: "The time of each stretch in a team inside the period, up to now, less the time the item was resolved (from the status history, which starts with the release that records it). A group chat's conversation has none, its tickets do."
              allOf:
                - $ref: "#/components/schemas/ReportTiming"
        nextResponse:
          nullable: true
          description: "Summary and breakdown. The customer's wait for each reply after the conversation's first response, from the oldest message still unanswered (no matter who held the conversation), over the replies sent in the period. The replies counted are the ones replyTime counts: every person's outside agent scoped results, the agent's own in them; a reply from an API integration or a resolve ends the wait without counting. In a merged conversation the first reply each chat got before the merge was its own first response and is left out, as the SLA leaves it out. A ticket answers in its chat, so it has none: with type=tickets nextResponse is null. Per agent group, that agent's replies; per day or week, the day of the reply."
          allOf:
            - $ref: "#/components/schemas/ReportTiming"
        sla:
          type: object
          nullable: true
          description: "Summary and breakdown. null on a plan without SLA, also after a downgrade. SLA compliance from the stored SLA outcomes: the SLA clocks that stopped in the period (met or breached) and the breaches with no recorded end that started in it; a clock still inside its target counts once it stops. A breach has no recorded end while its clock runs, or when the SLA stopped timing the conversation before its clock stopped (its policy removed, a plan without SLA, or marked as spam). The times are the SLA's own (pauses left out, and the time outside business hours for a policy that counts business hours only), so hours=business changes nothing here. Every filter and group reads the conversation as it is now (per agent, the agent who holds it); per day or week, the day the clock stopped or, without a recorded end, the day it started."
          properties:
            total: { $ref: "#/components/schemas/ReportSlaKind" }
            conversations: { $ref: "#/components/schemas/ReportSlaKind" }
            tickets: { $ref: "#/components/schemas/ReportSlaKind" }
    ReportSlaKind:
      type: object
      description: SLA compliance per metric, of conversations, tickets or both (total)
      properties:
        firstResponse: { $ref: "#/components/schemas/ReportSlaMetric" }
        nextResponse: { $ref: "#/components/schemas/ReportSlaMetric" }
        resolution: { $ref: "#/components/schemas/ReportSlaMetric" }
    ReportSlaMetric:
      type: object
      properties:
        achieved: { type: integer, description: Clocks that stopped inside their target }
        breached: { type: integer, description: "Clocks that ran past their target, the ones with no recorded end included" }
        running: { type: integer, description: "Of breached, the ones with no recorded end: their clock was still running at its last read, or the SLA stopped timing the conversation before the clock stopped" }
        compliance: { type: number, nullable: true, description: "achieved / (achieved + breached), 0 to 1; null without either" }
    ReportConversationRow:
      type: object
      properties:
        id: { type: string }
        displayId: { type: integer }
        status: { type: string }
        priority: { type: string, nullable: true }
        subject: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        firstReplyAt: { type: string, format: date-time, nullable: true, description: "The conversation's first reply; in an agent scoped list, the agent's own first reply to a waiting customer (null when they gave none)" }
        resolvedAt: { type: string, format: date-time, nullable: true }
        lastActivityAt: { type: string, format: date-time }
        firstResponseSeconds: { type: integer, nullable: true, description: "The customer's wait (from their first message to the first reply, queue time included); in an agent scoped list, the agent's own first response, from the moment they had the conversation" }
        resolutionSeconds: { type: integer, nullable: true }
        agent:
          type: object
          nullable: true
          description: The current assignee
          properties:
            id: { type: string }
            name: { type: string }
        team:
          type: object
          nullable: true
          properties:
            id: { type: string }
            name: { type: string }
        inbox:
          type: object
          nullable: true
          properties:
            id: { type: string }
            name: { type: string }
            channel: { type: string }
        labels:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              title: { type: string }
        csat:
          type: object
          nullable: true
          properties:
            rating: { type: integer, nullable: true }
            feedback: { type: string, nullable: true }
            sentAt: { type: string, format: date-time }
        contact:
          type: object
          description: Only with contacts:read (or a dashboard session)
          properties:
            id: { type: string }
            name: { type: string, nullable: true }
            email: { type: string, nullable: true }
        country: { type: string, nullable: true }
        botHandedOff: { type: boolean }
        agentFirstResponses:
          type: array
          description: Each agent's first reply to a waiting customer in this conversation (all time), oldest first
          items:
            type: object
            properties:
              agent:
                type: object
                properties:
                  id: { type: string }
                  name: { type: string }
              seconds: { type: integer }
              repliedAt: { type: string, format: date-time }
              assignedAt: { type: string, format: date-time, nullable: true, description: When the agent got the conversation; null when they took it by answering }
    ReportReplyRow:
      type: object
      properties:
        messageId: { type: string }
        conversation:
          type: object
          properties:
            id: { type: string }
            displayId: { type: integer }
        agent:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
        repliedAt: { type: string, format: date-time }
        waitingSince: { type: string, format: date-time, description: The oldest customer message still unanswered at the reply }
        assignedAt: { type: string, format: date-time, nullable: true, description: Start of the agent's assignment that held the reply; null when the agent took the conversation by answering }
        agentWaitSeconds: { type: integer, description: From waitingSince but never from before assignedAt; 0 when assignedAt is null }
        customerWaitSeconds: { type: integer, description: From waitingSince, queue time included }
        firstForAgent: { type: boolean, description: The agent's first counted reply in this conversation }
        inbox:
          type: object
          nullable: true
          properties:
            id: { type: string }
            name: { type: string }
            channel: { type: string }
        team:
          type: object
          nullable: true
          properties:
            id: { type: string }
            name: { type: string }
    ReportEcho:
      type: object
      properties:
        period:
          type: object
          properties:
            from: { type: string, format: date-time }
            to: { type: string, format: date-time }
            timezone: { type: string }
        filters:
          type: object
          additionalProperties: { type: array, items: { type: string } }
    Conversation:
      type: object
      properties:
        id:
          type: string
        displayId:
          type: integer
        status:
          type: string
          enum: [OPEN, PENDING, RESOLVED, SNOOZED]
        priority:
          type: string
          enum: [NONE, LOW, MEDIUM, HIGH, URGENT]
        channel:
          type: string
        subject:
          type: string
        inboxId:
          type: string
        contactId:
          type: string
        assigneeId:
          type: string
          nullable: true
        teamId:
          type: string
          nullable: true
        kind:
          type: string
          enum: [CONVERSATION, TICKET]
          description: TICKET for a ticket (see Tickets), which has no inbox.
        sourceConversationId:
          type: string
          nullable: true
          description: A ticket's conversation it came from.
        categoryId:
          type: string
          nullable: true
        category:
          type: object
          nullable: true
          description: "List items only: the category's id and name, also an archived one."
          properties:
            id: { type: string }
            name: { type: string }
        createdById:
          type: string
          nullable: true
          description: Who created the ticket.
        ticketStatus:
          type: object
          nullable: true
          description: A ticket's custom status (see listTicketStatuses); null on a conversation and in a plain base status.
          properties:
            id: { type: string }
            name: { type: string }
            baseStatus: { type: string, enum: [OPEN, PENDING, RESOLVED] }
            pausesSla: { type: boolean }
        customAttributes:
          type: object
          additionalProperties: true
          description: Custom attribute values keyed by attribute key.
        labels:
          type: array
          items:
            $ref: "#/components/schemas/Label"
        messages:
          type: array
          items:
            $ref: "#/components/schemas/Message"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    Message:
      type: object
      properties:
        id:
          type: string
        content:
          type: string
        messageType:
          type: string
          enum: [INCOMING, OUTGOING, ACTIVITY]
        private:
          type: boolean
        contentType:
          type: string
        senderId:
          type: string
          nullable: true
        senderType:
          type: string
          enum: [USER, CONTACT, BOT]
        createdAt:
          type: string
          format: date-time

    HistoryPerson:
      type: object
      nullable: true
      properties:
        id: { type: string }
        name: { type: string }

    HistoryMergedFrom:
      type: object
      nullable: true
      description: The conversation a merge moved the row in from; it no longer exists
      properties:
        conversationId: { type: string }
        displayId: { type: integer, nullable: true }

    HistoryPageMeta:
      type: object
      properties:
        hasMore: { type: boolean }
        nextCursor: { type: string, nullable: true }
        limit: { type: integer }

    Contact:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        company:
          type: string
        city:
          type: string
        state:
          type: string
        country:
          type: string
        description:
          type: string
        telegramId:
          type: string
          nullable: true
        blocked:
          type: boolean
        customAttributes:
          type: object
        conversationCount:
          type: integer
        lastActivityAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time

    ContactTelegramId:
      type: string
      description: |
        The Telegram chat the contact is: a person's user id, or a group's negative chat id (the contact
        then counts as a group chat). Set it before the chat's first message and that message joins this
        contact. Only a contact without one takes it (400 `locked` otherwise); another contact of the
        workspace with the same id answers 409 telegram_id_taken with the holder's contactId.
      example: "-1001234567890"
    ContactCreate:
      type: object
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        company:
          type: string
        city:
          type: string
        state:
          type: string
        country:
          type: string
        countryCode:
          type: string
        description:
          type: string
        identifier:
          type: string
          description: Your own id for the contact, unique in the workspace. A second contact with it answers 409 identifier_taken with the holder's contactId.
        organizationId:
          type: string
          description: An organization of this workspace.
        telegramId:
          $ref: "#/components/schemas/ContactTelegramId"
        customAttributes:
          $ref: "#/components/schemas/CustomAttributesWrite"

    ContactUpdate:
      type: object
      properties:
        firstName:
          type: string
        lastName:
          type: string
        name:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        company:
          type: string
        city:
          type: string
        state:
          type: string
        country:
          type: string
        locale:
          type: string
          description: The customer's language as a language code, for example de or pt-BR. Replaces the stored one.
          example: pt-BR
        description:
          type: string
        blocked:
          type: boolean
        tags:
          type: array
          items:
            type: string
        organizationId:
          type: string
          nullable: true
          description: An organization of this workspace, or null to remove it.
        telegramId:
          $ref: "#/components/schemas/ContactTelegramId"
        customAttributes:
          $ref: "#/components/schemas/CustomAttributesWrite"

    CustomAttributesWrite:
      type: object
      additionalProperties: true
      description: >-
        Custom attribute values keyed by attribute key. Merged into the stored
        values; a key set to null is removed. A key with a definition in
        Settings > Custom Attributes must match its type: TEXT a string,
        NUMBER a number, LINK a full http(s) URL, DATE a YYYY-MM-DD string,
        LIST one of the definition's attributeValues, CHECKBOX true or false.
        Keys without a definition are stored as sent. Deskwoot's own keys
        (for example source) cannot be written.
      example:
        order_id: A-1042
        order_total: 129.9
        vip: true
        old_field: null

    CustomAttributeDefinition:
      type: object
      properties:
        id:
          type: string
        attributeDisplayName:
          type: string
        attributeKey:
          type: string
        attributeDisplayType:
          type: string
          enum: [TEXT, NUMBER, LINK, DATE, LIST, CHECKBOX]
        attributeModel:
          type: string
          enum: [CONVERSATION, CONTACT]
        attributeValues:
          type: array
          nullable: true
          items:
            type: string
          description: Options of a LIST attribute.
        description:
          type: string
          nullable: true

    Inbox:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        channelType:
          type: string
          enum: [WEB, EMAIL, WHATSAPP, WHATSAPP_TWILIO, TELEGRAM, TWITTER, LINE, SMS, API]
        welcomeTitle:
          type: string
        greeting:
          type: string
        csatEnabled:
          type: boolean
        createdAt:
          type: string
          format: date-time

    Label:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        color:
          type: string
        showOnSidebar:
          type: boolean

    Team:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        allowAutoAssign:
          type: boolean

    Agent:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          enum: [ADMINISTRATOR, AGENT]
        availability:
          type: string
          enum: [ONLINE, BUSY, OFFLINE]

    CannedResponse:
      type: object
      properties:
        id:
          type: string
        shortCode:
          type: string
        content:
          type: string

    AutomationRule:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        active:
          type: boolean
        eventName:
          type: string
        conditions:
          type: array
          items:
            type: object
        actions:
          type: array
          items:
            type: object

    Webhook:
      type: object
      properties:
        id:
          type: string
        accountId:
          type: string
        url:
          type: string
          format: uri
        subscriptions:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEventName"
        enabled:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    WebhookEventName:
      type: string
      description: |
        Canonical dotted event name. The underscored spelling of each value is accepted
        as an alias when subscribing.
      enum:
        - conversation.created
        - conversation.updated
        - conversation.assigned
        - conversation.resolved
        - message.created
        - contact.created
        - contact.updated
        - csat.submitted
        - ticket.created
        - ticket.updated
        - ticket.assigned
        - ticket.resolved

    TicketCategory:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        position: { type: integer }
        archived: { type: boolean, description: Kept on the conversations and tickets that have it, not offered for new ones }
        archivedAt: { type: string, format: date-time, nullable: true }

    TicketStatus:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        baseStatus: { type: string, enum: [OPEN, PENDING, RESOLVED], description: The status a ticket in it has }
        pausesSla: { type: boolean, description: Its tickets' SLA clocks stand still }
        position: { type: integer }
        archived: { type: boolean, description: Kept on the tickets that have it, not offered for new ones }
        archivedAt: { type: string, format: date-time, nullable: true }

    SlaPriorityTargets:
      type: object
      description: Targets in minutes for one priority; a target left out uses the policy's own.
      properties:
        firstResponse: { type: integer, minimum: 1, maximum: 43200, nullable: true }
        nextResponse: { type: integer, minimum: 1, maximum: 43200, nullable: true }
        resolution: { type: integer, minimum: 1, maximum: 43200, nullable: true }

    BusinessHoursDay:
      type: object
      required: [day, enabled, allDay, openTime, closeTime]
      properties:
        day: { type: string, enum: [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday] }
        enabled: { type: boolean, description: Open on this day }
        allDay: { type: boolean }
        openTime: { type: string, description: "Like 09:00; may be empty on a day that is off or open all day" }
        closeTime: { type: string, description: "Like 17:00, or 24:00 for midnight; may be empty on a day that is off or open all day" }

    BusinessHoursSchedule:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        timezone: { type: string }
        schedule:
          type: array
          items: { $ref: "#/components/schemas/BusinessHoursDay" }
        _count:
          type: object
          properties:
            teams: { type: integer }
            slaPolicies: { type: integer }

    TicketSettings:
      type: object
      properties:
        ticketsEnabled: { type: boolean }
        ticketCategoryRequired: { type: boolean, description: A new ticket needs a category }
        ticketDefaultPriority: { type: string, enum: [URGENT, HIGH, MEDIUM, LOW], nullable: true, description: The default priority for new tickets; null for none }
        planIncludesTickets: { type: boolean }
        active: { type: boolean, description: Switched on and included in the plan }

    TicketEventTicket:
      type: object
      description: The ticket as the ticket.* webhooks carry it, read when the event is sent.
      properties:
        id: { type: string }
        number: { type: integer }
        title: { type: string }
        status: { type: string, enum: [OPEN, PENDING, RESOLVED, SNOOZED] }
        priority: { type: string, enum: [URGENT, HIGH, MEDIUM, LOW], nullable: true }
        category: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
        team: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
        assignee: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
        creator: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
        sourceConversationId: { type: string, nullable: true, description: The conversation the ticket came from }
        contactId: { type: string }
        createdAt: { type: string, format: date-time }
        resolvedAt: { type: string, format: date-time, nullable: true }
        labels:
          type: array
          description: The ticket's labels, oldest first.
          items: { type: object, properties: { id: { type: string }, title: { type: string } } }
        customAttributes: { type: object, additionalProperties: true, description: "The ticket's custom attributes, without Deskwoot's own bookkeeping keys" }
        linkedIssues:
          type: array
          description: The ticket's linked Jira issues, the first linked first, with their last known status.
          items:
            type: object
            properties:
              key: { type: string }
              url: { type: string }
              title: { type: string }
              statusName: { type: string }
              statusCategory: { type: string, enum: [TODO, IN_PROGRESS, DONE] }

    LinkedIssue:
      type: object
      description: A Jira issue linked to a ticket, with its last known state.
      properties:
        id: { type: string, description: The id the unlink takes }
        provider: { type: string, enum: [JIRA] }
        siteId: { type: string, description: The Jira site (cloud id) }
        externalId: { type: string, description: "Jira's id of the issue, which stays when its key changes" }
        key: { type: string }
        url: { type: string }
        title: { type: string }
        statusName: { type: string }
        statusCategory: { type: string, enum: [TODO, IN_PROGRESS, DONE] }
        deleted: { type: boolean }
        notFound: { type: boolean }
        syncedAt: { type: string, format: date-time }
        raised: { type: boolean, description: The issue was raised from this ticket }
        linkedAt: { type: string, format: date-time }
        linkedBy: { type: object, nullable: true, description: "Null for a link made with an API token", properties: { id: { type: string }, name: { type: string } } }

    WebhookEnvelope:
      type: object
      description: Body of every webhook delivery.
      required: [event, data, timestamp, accountId]
      properties:
        event:
          $ref: "#/components/schemas/WebhookEventName"
        data:
          type: object
          description: Event specific payload, see the `webhooks` section.
        timestamp:
          type: string
          format: date-time
        accountId:
          type: string

    Campaign:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        message:
          type: string
        campaignType:
          type: string
          enum: [ONGOING, ONE_OFF]
        enabled:
          type: boolean
        scheduledAt:
          type: string
          format: date-time
          nullable: true

    Bot:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        aiProvider:
          type: string
          enum: [openai, anthropic]
        aiModel:
          type: string
        useHelpCenter:
          type: boolean
        maxTokens:
          type: integer
        temperature:
          type: number
        collectEmailAtStart:
          type: boolean

    Integration:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
          enum: [SHOPIFY, STRIPE, WOOCOMMERCE, SLACK]
        active:
          type: boolean
        config:
          type: object
        createdAt:
          type: string
          format: date-time

    Portal:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        headerText:
          type: string
        brandColor:
          type: string

    PaginationMeta:
      type: object
      properties:
        page:
          type: integer
        pageSize:
          type: integer
        total:
          type: integer

  responses:
    BadRequest:
      description: The request body is invalid
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: "customAttributes.order_total: must be a number"
    Unauthorized:
      description: Missing or invalid API token
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Unauthorized
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Not found
