openapi: 3.1.0
info:
  title: Protodesk API
  version: 0.1.0
  description: >
    The Protodesk Platform API: sync your customers in, attach your own
    business objects (orders, bookings, listings) to conversations, post and
    read messages across channels, and receive every workspace event via
    signed webhooks or a resumable SSE stream.


    Authenticate every request with a workspace-scoped API key:
    `Authorization: Bearer pk_live_...`. Create endpoints accept an
    `Idempotency-Key` header so retries are always safe. Lists use opaque
    cursor pagination (`data` / `hasMore` / `nextCursor`).


    Task-oriented guides: [protodesk.io/docs](https://protodesk.io/docs).
servers:
  - url: https://api.protodesk.io/v1
    description: Production
  - url: http://localhost:8080/v1
    description: Local development
tags:
  - name: Customers
    description: People or companies contacting a workspace across channels.
  - name: Conversations
    description: Customer conversations attached to channels and external business objects.
  - name: Messages
    description: Customer, agent, AI, system, and internal messages inside conversations.
  - name: Channel identities
    description: The same customer across WhatsApp, email, chat, and more — linked identities per channel.
  - name: Statuses & priorities
    description: The workspace's conversation statuses and priority levels.
  - name: Assignments
    description: Who a conversation has been assigned to, over time. Assign via POST /conversations/{conversationId}/assign.
  - name: Webhooks
    description: Webhook subscriptions for receiving workspace events.
  - name: Realtime
    description: Server-sent event stream of workspace events.
  - name: API Keys
    description: Create, list, and revoke workspace API keys.
  - name: Account
    description: The workspace and scopes behind your API key.
  - name: System
    description: Service metadata and health endpoints.
paths:
  /:
    get:
      tags:
        - System
      summary: API index
      operationId: getApiIndex
      responses:
        "200":
          description: API metadata.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                type: object
                required:
                  - name
                  - version
                  - status
                properties:
                  name:
                    type: string
                    example: Protodesk API
                  version:
                    type: string
                    example: v1
                  status:
                    type: string
                    example: ready
        default:
          $ref: "#/components/responses/Error"
  /healthz:
    get:
      tags:
        - System
      summary: Health check
      operationId: getHealth
      servers:
        - url: https://api.protodesk.io
        - url: http://localhost:8080
      responses:
        "200":
          description: Service health.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                type: object
                required:
                  - service
                  - status
                  - version
                properties:
                  service:
                    type: string
                    example: protodesk-backend
                  status:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: dev
        default:
          $ref: "#/components/responses/Error"
  /me:
    get:
      tags:
        - Account
      summary: Get the current workspace and key scopes
      operationId: getMe
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Authenticated API key context.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        default:
          $ref: "#/components/responses/Error"
  /realtime/events:
    get:
      tags:
        - Realtime
      summary: Stream workspace events (SSE)
      description: >
        A Server-Sent Events stream of the workspace event outbox — the same
        events and payloads delivered to webhooks. Each message carries the
        event id in the SSE `id:` field; reconnect with `Last-Event-ID` (or the
        `cursor` query param) to replay events missed while disconnected.
        Without a cursor, only events created after the connection opens are
        sent. A `: heartbeat` comment is sent during idle periods, and the
        server closes long-lived connections (~15m) with a `: reconnect`
        comment so clients resume with a fresh cursor.
      operationId: streamEvents
      security:
        - ApiKeyAuth: []
      parameters:
        - name: cursor
          in: query
          required: false
          description: Event id to resume after. Overridden by the Last-Event-ID header if both are absent here.
          schema:
            type: string
        - name: conversationId
          in: query
          required: false
          description: Only stream events for this conversation.
          schema:
            type: string
        - name: eventTypes
          in: query
          required: false
          description: Comma-separated event types to include (defaults to all).
          schema:
            type: string
          example: message.created,conversation.updated
        - name: Last-Event-ID
          in: header
          required: false
          description: Standard SSE reconnect header; the last event id the client received.
          schema:
            type: string
      responses:
        "200":
          description: An event stream.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            text/event-stream:
              schema:
                type: string
              example: |
                id: evt_x6q5vl75f5d53m7oet2k4r5w6a
                event: message.created
                data: {"id":"evt_...","type":"message.created","apiVersion":"2026-07-09","createdAt":"2026-07-09T12:00:00Z","data":{}}
        default:
          $ref: "#/components/responses/Error"
  /keys:
    get:
      tags:
        - API Keys
      summary: List API keys
      description: Lists the workspace's API keys. Secrets are never returned.
      operationId: listApiKeys
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: API keys.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListApiKeysResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      tags:
        - API Keys
      summary: Create an API key
      description: >
        Mints a new API key. The raw secret is returned once in the response
        and is never retrievable afterward. Requires the keys:manage scope.
      operationId: createApiKey
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiKeyRequest"
      responses:
        "201":
          description: Created API key with its raw secret (shown once).
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedApiKey"
        default:
          $ref: "#/components/responses/Error"
  /keys/{keyId}:
    delete:
      tags:
        - API Keys
      summary: Revoke an API key
      description: >
        Revokes an API key immediately. Subsequent requests using it are
        rejected with 401. Revoking is idempotent.
      operationId: revokeApiKey
      security:
        - ApiKeyAuth: []
      parameters:
        - name: keyId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The revoked API key.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKey"
        default:
          $ref: "#/components/responses/Error"
  /customers:
    get:
      tags:
        - Customers
      summary: List customers
      operationId: listCustomers
      security:
        - ApiKeyAuth: []
      parameters:
        - name: externalId
          in: query
          schema:
            type: string
          description: Filter by your external customer id.
        - name: search
          in: query
          schema:
            type: string
          description: Case-insensitive substring match over name, email, and company.
        - name: identityChannel
          in: query
          schema:
            type: string
          description: >
            With identityIdentifier, resolve the customer owning a channel
            identity ("who is +62812… on WhatsApp?"). Both must be provided
            together; the result is a 0-or-1 item list.
          example: whatsapp
        - name: identityIdentifier
          in: query
          schema:
            type: string
          description: The identity value to look up (e.g. a phone number).
          example: "+628123456789"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Customers.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListCustomersResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      tags:
        - Customers
      summary: Create a customer
      operationId: createCustomer
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCustomerRequest"
      responses:
        "201":
          description: Created customer.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        default:
          $ref: "#/components/responses/Error"
    put:
      tags:
        - Customers
      summary: Upsert a customer by externalId
      description: >
        Creates a customer when no customer with the given externalId exists
        (201), or patches the existing one (200). Only the provided fields are
        written on update. externalId is the natural key and is required.
      operationId: upsertCustomer
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpsertCustomerRequest"
      responses:
        "200":
          description: Existing customer patched.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        "201":
          description: New customer created.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        default:
          $ref: "#/components/responses/Error"
  /customers/{customerId}:
    get:
      tags:
        - Customers
      summary: Get a customer
      operationId: getCustomer
      security:
        - ApiKeyAuth: []
      parameters:
        - name: customerId
          in: path
          required: true
          schema:
            type: string
            example: cus_x6q5vl75f5d53m7oet2k4r5w6a
      responses:
        "200":
          description: Customer.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        default:
          $ref: "#/components/responses/Error"
    delete:
      tags:
        - Customers
      summary: Delete a customer
      description: >
        Soft-deletes the customer: it disappears from lists but is retained so
        conversation history still resolves. Idempotent.
      operationId: deleteCustomer
      security:
        - ApiKeyAuth: []
      parameters:
        - name: customerId
          in: path
          required: true
          schema:
            type: string
            example: cus_x6q5vl75f5d53m7oet2k4r5w6a
      responses:
        "204":
          description: Customer deleted.
        default:
          $ref: "#/components/responses/Error"
    patch:
      tags:
        - Customers
      summary: Update a customer
      operationId: updateCustomer
      security:
        - ApiKeyAuth: []
      parameters:
        - name: customerId
          in: path
          required: true
          schema:
            type: string
            example: cus_x6q5vl75f5d53m7oet2k4r5w6a
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateCustomerRequest"
      responses:
        "200":
          description: Updated customer.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        default:
          $ref: "#/components/responses/Error"
  /customers/{customerId}/identities:
    parameters:
      - name: customerId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Channel identities
      summary: List a customer's channel identities
      operationId: listIdentities
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Channel identities.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListIdentitiesResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      tags:
        - Channel identities
      summary: Link a channel identity to a customer
      description: >
        API-created identities default verified=false. Passing verified=true
        asserts your system authenticated the identity (recorded with
        verifiedAt) and allows it to auto-merge. A (channel, identifier)
        already claimed by another customer returns 409 with the owning
        customerId.
      operationId: createIdentity
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateIdentityRequest"
      responses:
        "201":
          description: Created channel identity.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChannelIdentity"
        "409":
          description: The identity is already claimed by another customer.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentityConflict"
        default:
          $ref: "#/components/responses/Error"
  /customers/{customerId}/identities/{identityId}:
    delete:
      tags:
        - Channel identities
      summary: Remove a channel identity
      operationId: deleteIdentity
      security:
        - ApiKeyAuth: []
      parameters:
        - name: customerId
          in: path
          required: true
          schema:
            type: string
        - name: identityId
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Identity removed.
        default:
          $ref: "#/components/responses/Error"
  /conversations:
    get:
      tags:
        - Conversations
      summary: List conversations
      operationId: listConversations
      security:
        - ApiKeyAuth: []
      parameters:
        - name: customerId
          in: query
          schema:
            type: string
          description: Filter by Protodesk customer id.
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/ConversationStatus"
        - name: channel
          in: query
          schema:
            type: string
          example: whatsapp
        - name: externalObjectType
          in: query
          schema:
            type: string
          description: Filter by the attached external object's type.
          example: villa_listing
        - name: externalObjectId
          in: query
          schema:
            type: string
          description: Filter by the attached external object's id.
          example: listing_123
        - name: updatedAfter
          in: query
          description: >
            Return only conversations updated strictly after this RFC3339
            timestamp. Useful for polling changes since a last sync.
          schema:
            type: string
            format: date-time
          example: 2026-07-09T12:00:00Z
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Conversations ordered by most recently updated.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListConversationsResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      tags:
        - Conversations
      summary: Create a conversation
      operationId: createConversation
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateConversationRequest"
      responses:
        "201":
          description: Created conversation.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Conversation"
        default:
          $ref: "#/components/responses/Error"
  /conversations/{conversationId}:
    get:
      tags:
        - Conversations
      summary: Get a conversation
      operationId: getConversation
      security:
        - ApiKeyAuth: []
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
            example: conv_x6q5vl75f5d53m7oet2k4r5w6a
      responses:
        "200":
          description: Conversation.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Conversation"
        default:
          $ref: "#/components/responses/Error"
    patch:
      tags:
        - Conversations
      summary: Update a conversation
      operationId: updateConversation
      security:
        - ApiKeyAuth: []
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
            example: conv_x6q5vl75f5d53m7oet2k4r5w6a
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateConversationRequest"
      responses:
        "200":
          description: Updated conversation.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Conversation"
        default:
          $ref: "#/components/responses/Error"
  /conversations/{conversationId}/assign:
    post:
      tags:
        - Conversations
      summary: Assign a conversation
      operationId: assignConversation
      security:
        - ApiKeyAuth: []
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
            example: conv_x6q5vl75f5d53m7oet2k4r5w6a
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssignConversationRequest"
      responses:
        "200":
          description: Assigned conversation.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Conversation"
        default:
          $ref: "#/components/responses/Error"
  /conversations/{conversationId}/resolve:
    post:
      tags:
        - Conversations
      summary: Resolve a conversation
      operationId: resolveConversation
      security:
        - ApiKeyAuth: []
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
            example: conv_x6q5vl75f5d53m7oet2k4r5w6a
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ResolveConversationRequest"
      responses:
        "200":
          description: Resolved conversation.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Conversation"
        default:
          $ref: "#/components/responses/Error"
  /statuses:
    get:
      tags:
        - Statuses & priorities
      summary: List workspace statuses
      operationId: listStatuses
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Workspace-defined conversation statuses ordered by position.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListStatusesResponse"
        default:
          $ref: "#/components/responses/Error"
  /priorities:
    get:
      tags:
        - Statuses & priorities
      summary: List workspace priorities
      operationId: listPriorities
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Workspace-defined priority levels ordered by position.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListPrioritiesResponse"
        default:
          $ref: "#/components/responses/Error"
  /conversations/{conversationId}/assignments:
    parameters:
      - name: conversationId
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Assignments
      summary: List a conversation's assignments
      operationId: listAssignments
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Assignments (all states), newest first.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListAssignmentsResponse"
        default:
          $ref: "#/components/responses/Error"
  /conversations/{conversationId}/messages:
    get:
      tags:
        - Messages
      summary: List messages
      operationId: listMessages
      security:
        - ApiKeyAuth: []
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
            example: conv_x6q5vl75f5d53m7oet2k4r5w6a
        - name: visibility
          in: query
          description: >
            Filter by the internal flag. Use public for a customer-facing view
            that excludes internal notes.
          schema:
            type: string
            enum:
              - all
              - public
              - internal
            default: all
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Messages in chronological order.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListMessagesResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      tags:
        - Messages
      summary: Create a message
      operationId: createMessage
      security:
        - ApiKeyAuth: []
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
            example: conv_x6q5vl75f5d53m7oet2k4r5w6a
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMessageRequest"
      responses:
        "201":
          description: Created message.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        default:
          $ref: "#/components/responses/Error"
  /webhooks:
    get:
      tags:
        - Webhooks
      summary: List webhook subscriptions
      operationId: listWebhooks
      security:
        - ApiKeyAuth: []
      parameters:
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/WebhookStatus"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Webhook subscriptions.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListWebhooksResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      tags:
        - Webhooks
      summary: Create a webhook subscription
      operationId: createWebhook
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookRequest"
      responses:
        "201":
          description: Created webhook subscription. The signing secret is returned only in this response.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscription"
        default:
          $ref: "#/components/responses/Error"
  /webhooks/{webhookId}:
    patch:
      tags:
        - Webhooks
      summary: Update a webhook subscription
      operationId: updateWebhook
      security:
        - ApiKeyAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            example: wh_x6q5vl75f5d53m7oet2k4r5w6a
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWebhookRequest"
      responses:
        "200":
          description: Updated webhook subscription.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscription"
        default:
          $ref: "#/components/responses/Error"
    delete:
      tags:
        - Webhooks
      summary: Delete a webhook subscription
      operationId: deleteWebhook
      security:
        - ApiKeyAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            example: wh_x6q5vl75f5d53m7oet2k4r5w6a
      responses:
        "204":
          description: Deleted webhook subscription.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
        default:
          $ref: "#/components/responses/Error"
  /webhooks/{webhookId}/test:
    post:
      tags:
        - Webhooks
      summary: Send a test delivery
      description: >
        Sends a synthetic `webhook.test` event through the real delivery
        path (same headers and HMAC signature) and returns the result
        synchronously. Use this to verify your endpoint before real traffic.
      operationId: testWebhook
      security:
        - ApiKeyAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            example: wh_x6q5vl75f5d53m7oet2k4r5w6a
      responses:
        "200":
          description: Test delivery result.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookTestResult"
        default:
          $ref: "#/components/responses/Error"
  /webhooks/{webhookId}/deliveries:
    get:
      tags:
        - Webhooks
      summary: List recent deliveries
      description: Recent delivery attempts for one subscription, newest first.
      operationId: listWebhookDeliveries
      security:
        - ApiKeyAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            example: wh_x6q5vl75f5d53m7oet2k4r5w6a
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Delivery attempts ordered by most recent.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListWebhookDeliveriesResponse"
        default:
          $ref: "#/components/responses/Error"
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: Protodesk API key
      description: >
        Use a workspace API key in the Authorization header. Keys use
        pk_live_<key_id>_<secret> for production and pk_test_<key_id>_<secret>
        for test mode.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >
        Optional retry key for safely replaying create requests. Reusing the
        same key with a different request body returns 409.
      schema:
        type: string
        minLength: 8
        maxLength: 255
        example: 8db8e596-7c1a-4fd5-a728-4d6c99f4e66b
    Cursor:
      name: cursor
      in: query
      required: false
      description: >
        Opaque keyset cursor from a prior response's nextCursor. Omit for the
        first page. Malformed or tampered cursors return 400.
      schema:
        type: string
  headers:
    RequestId:
      description: Request id for tracing this request in logs and support.
      schema:
        type: string
        example: req_x6q5vl75f5d53m7oet2k4r5w6a
    IdempotencyReplayed:
      description: Present with true when a response was replayed from an idempotency key.
      schema:
        type: string
        enum:
          - "true"
    RateLimitLimit:
      description: Request budget for the route class (read or write) applied to this request.
      schema:
        type: integer
        example: 40
    RateLimitRemaining:
      description: Requests remaining in the current budget after this request.
      schema:
        type: integer
        example: 39
    RateLimitReset:
      description: Seconds until the budget refills to its full limit.
      schema:
        type: integer
        example: 1
    RetryAfter:
      description: Seconds to wait before retrying, sent with 429 responses.
      schema:
        type: integer
        example: 1
  responses:
    Error:
      description: >-
        Error response. All /v1 responses carry RateLimit-Limit,
        RateLimit-Remaining, and RateLimit-Reset headers. A 429
        (rate_limit.exceeded) additionally carries Retry-After. Request bodies
        exceeding the route-class limit (1KB reads, 64KB writes) return 413
        (request.body_too_large).
      headers:
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
        RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
  schemas:
    Me:
      type: object
      required:
        - apiKeyId
        - workspaceId
        - scopes
      properties:
        apiKeyId:
          type: string
          example: key_o5mj4q7beg32vkjcd76a37t5zy
        workspaceId:
          type: string
          example: wsp_f46kbkltuw6uzkj7y2j4bbxlbe
        scopes:
          type: array
          items:
            $ref: "#/components/schemas/APIKeyScope"
    CreateApiKeyRequest:
      type: object
      required:
        - name
        - scopes
      properties:
        name:
          type: string
          description: Human-readable label for the key.
          example: CI deploy
        environment:
          type: string
          description: Defaults to the calling key's environment.
          enum:
            - live
            - test
        scopes:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/APIKeyScope"
    CreatedApiKey:
      type: object
      description: A newly created key. The secret is shown once and never again.
      required:
        - id
        - name
        - environment
        - scopes
        - display
        - secret
      properties:
        id:
          type: string
          example: key_o5mj4q7beg32vkjcd76a37t5zy
        name:
          type: string
        environment:
          type: string
          enum:
            - live
            - test
        scopes:
          type: array
          items:
            $ref: "#/components/schemas/APIKeyScope"
        display:
          type: string
          description: Masked key preview safe to display.
          example: pk_test_key...cret
        secret:
          type: string
          description: The raw API key. Store it now; it cannot be retrieved later.
          example: pk_test_key_o5mj4q7beg32vkjcd76a37t5zy_abcdefghijklmnopqrstuvwxyz234567
    ApiKey:
      type: object
      description: API key metadata without secret material.
      required:
        - id
        - name
        - environment
        - scopes
        - display
        - createdAt
      properties:
        id:
          type: string
        name:
          type: string
        environment:
          type: string
          enum:
            - live
            - test
        scopes:
          type: array
          items:
            $ref: "#/components/schemas/APIKeyScope"
        display:
          type: string
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        revokedAt:
          type: string
          format: date-time
          nullable: true
    ListApiKeysResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ApiKey"
    APIKeyScope:
      type: string
      description: Workspace API key permission scope.
      enum:
        - customers:read
        - customers:write
        - conversations:read
        - conversations:write
        - messages:write
        - attachments:read
        - attachments:write
        - webhooks:manage
        - realtime:read
        - ai:use
        - keys:manage
    CreateCustomerRequest:
      type: object
      properties:
        externalId:
          type: string
          description: Your stable id for this customer.
          example: renter_123
        name:
          type: string
          example: Maya
        email:
          type: string
          format: email
          example: maya@example.com
        phone:
          type: string
          example: "+628123456789"
        company:
          type: string
          example: Bali Villa Hub
        language:
          type: string
          example: id
        timezone:
          type: string
          example: Asia/Jakarta
        metadata:
          type: object
          additionalProperties: true
          example:
            tier: vip
            source: villa_owner_app
    UpdateCustomerRequest:
      type: object
      description: Omitted fields are left unchanged. Empty externalId clears the external id.
      minProperties: 1
      properties:
        externalId:
          type: string
          description: Your stable id for this customer. Send an empty string to clear it.
          example: renter_123
        name:
          type: string
          example: Maya
        email:
          type: string
          format: email
          example: maya@example.com
        phone:
          type: string
          example: "+628123456789"
        company:
          type: string
          example: Bali Villa Hub
        language:
          type: string
          example: id
        timezone:
          type: string
          example: Asia/Jakarta
        avatarUrl:
          type: string
        claimedEmail:
          type: string
          description: An email the customer claimed but that is not verified.
        emailStatus:
          type: string
          description: Email deliverability/suppression state.
        lastActiveChannel:
          type: string
          example: whatsapp
        firstContactAt:
          type: string
          format: date-time
        lastSeenAt:
          type: string
          format: date-time
        tags:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
          description: Backdates the record when it is first created (import semantics); ignored if the customer already exists.
        metadata:
          type: object
          additionalProperties: true
          description: Replaces the full customer metadata object.
          example:
            tier: gold
    UpsertCustomerRequest:
      type: object
      description: >
        externalId is the natural key and is required. On update, only the
        provided fields are written; omitted fields are left unchanged.
      required:
        - externalId
      properties:
        externalId:
          type: string
          description: Your stable id for this customer; the upsert key.
          example: renter_123
        name:
          type: string
          example: Maya
        email:
          type: string
          format: email
          example: maya@example.com
        phone:
          type: string
          example: "+628123456789"
        company:
          type: string
          example: Bali Villa Hub
        language:
          type: string
          example: id
        timezone:
          type: string
          example: Asia/Jakarta
        metadata:
          type: object
          additionalProperties: true
          description: Replaces the full customer metadata object.
          example:
            tier: gold
    CreateIdentityRequest:
      type: object
      required:
        - channel
        - identifier
      properties:
        channel:
          type: string
          description: Channel the identity belongs to (open string, e.g. whatsapp, slack, email).
          example: whatsapp
        identifier:
          type: string
          description: The identity value on that channel.
          example: "+628123456789"
        verified:
          type: boolean
          default: false
          description: >
            True asserts your system authenticated this identity, allowing
            auto-merge. Recorded with verifiedAt.
        claimedEmail:
          type: string
          description: A claimed (unverified) email; display and merge-suggestion input only.
        displayName:
          type: string
        avatarUrl:
          type: string
        primary:
          type: boolean
          default: false
    ChannelIdentity:
      type: object
      required:
        - id
        - customerId
        - channel
        - identifier
        - verified
        - primary
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: cid_x6q5vl75f5d53m7oet2k4r5w6a
        customerId:
          type: string
        channel:
          type: string
          example: whatsapp
        identifier:
          type: string
          example: "+628123456789"
        verified:
          type: boolean
        verifiedAt:
          type: string
          format: date-time
          nullable: true
        claimedEmail:
          type: [string, "null"]
        displayName:
          type: [string, "null"]
        avatarUrl:
          type: [string, "null"]
        lastActiveAt:
          type: string
          format: date-time
          nullable: true
        primary:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ListIdentitiesResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ChannelIdentity"
    IdentityConflict:
      type: object
      description: >
        Returned with 409 when a (channel, identifier) is already claimed. The
        owning customerId is workspace-scoped, so it is safe to return.
      required:
        - error
        - customerId
      properties:
        error:
          $ref: "#/components/schemas/Error"
        customerId:
          type: string
          example: cus_x6q5vl75f5d53m7oet2k4r5w6a
    Customer:
      type: object
      description: >
        Every key is always present; optional fields are null when unset
        (never empty strings).
      required:
        - id
        - externalId
        - name
        - email
        - phone
        - company
        - language
        - timezone
        - avatarUrl
        - claimedEmail
        - emailStatus
        - lastActiveChannel
        - firstContactAt
        - lastSeenAt
        - tags
        - metadata
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: cus_x6q5vl75f5d53m7oet2k4r5w6a
        externalId:
          type: [string, "null"]
          example: renter_123
        name:
          type: string
          example: Maya
        email:
          type: [string, "null"]
          example: maya@example.com
        phone:
          type: [string, "null"]
          example: "+628123456789"
        company:
          type: [string, "null"]
          example: Bali Villa Hub
        language:
          type: [string, "null"]
          example: id
        timezone:
          type: [string, "null"]
          example: Asia/Jakarta
        avatarUrl:
          type: [string, "null"]
          example: https://cdn.example.com/avatars/maya.png
        claimedEmail:
          type: [string, "null"]
          description: An email the customer claimed but that is not verified.
        emailStatus:
          type: [string, "null"]
          description: Email deliverability/suppression state (e.g. active, unsubscribed, bounced).
        lastActiveChannel:
          type: [string, "null"]
          example: whatsapp
        firstContactAt:
          type: [string, "null"]
          format: date-time
        lastSeenAt:
          type: [string, "null"]
          format: date-time
        tags:
          type: array
          items:
            type: string
        conversationCount:
          type: integer
          description: Number of conversations for this customer. Present on list responses only.
        metadata:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ConversationStatus:
      type: string
      description: >
        Workspace-defined status key. Every workspace is seeded with `open`,
        `pending`, `resolved`, and `closed`; workspaces can define their own.
        Use `GET /v1/statuses` to list the keys valid for your workspace.
        Unknown keys are rejected with `conversations.unknown_status`.
      pattern: "^[a-z0-9][a-z0-9_-]{0,62}$"
      example: open
    ConversationPriority:
      type: string
      description: >
        Workspace-defined priority key. Every workspace is seeded with `low`,
        `normal`, `high`, and `urgent`; workspaces can define their own. Use
        `GET /v1/priorities` to list the keys valid for your workspace.
        Unknown keys are rejected with `conversations.unknown_priority`.
      pattern: "^[a-z0-9][a-z0-9_-]{0,62}$"
      example: normal
    Assignee:
      type: object
      description: >
        Who the conversation is assigned to. Set `id` to an empty string to
        clear the assignment.
      required:
        - id
      properties:
        type:
          type: string
          enum:
            - user
            - ai-agent
          default: user
        id:
          type: string
          description: External assignee id, such as an owner, agent, or AI agent id.
          example: owner_456
    Status:
      type: object
      description: Workspace-defined conversation status.
      required:
        - key
        - label
        - color
        - icon
        - type
        - position
        - isDefault
      properties:
        key:
          type: string
          description: Immutable key referenced by conversation `status`.
          example: open
        label:
          type: string
          example: Open
        color:
          type: string
          example: "#22c55e"
        icon:
          type: string
          example: ""
        type:
          type: string
          description: >
            `active` statuses are in progress; `completed` statuses end the
            conversation and are valid resolution targets.
          enum:
            - active
            - completed
        position:
          type: integer
        isDefault:
          type: boolean
          description: Default status for its type.
    Priority:
      type: object
      description: Workspace-defined conversation priority level.
      required:
        - key
        - label
        - color
        - position
        - isDefault
      properties:
        key:
          type: string
          description: Immutable key referenced by conversation `priority`.
          example: normal
        label:
          type: string
          example: Normal
        color:
          type: string
          example: ""
        position:
          type: integer
        isDefault:
          type: boolean
    ListStatusesResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Status"
    ListPrioritiesResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Priority"
    ExternalObject:
      type: object
      description: Snapshot of your business object attached to the conversation.
      required:
        - type
        - id
        - title
        - url
        - metadata
      properties:
        type:
          type: string
          example: villa_listing
        id:
          type: string
          example: listing_123
        title:
          type: [string, "null"]
          example: 2BR Villa in Canggu
        url:
          type: [string, "null"]
          format: uri
          example: https://example.com/listings/listing_123
        metadata:
          type: object
          additionalProperties: true
          example:
            ownerId: owner_456
            agencyId: agency_789
    CreateConversationRequest:
      type: object
      required:
        - customerId
        - subject
      properties:
        externalId:
          type: string
          description: Your stable id for this conversation.
          example: inquiry_123
        subject:
          type: string
          minLength: 1
          example: Inquiry for 2BR Villa in Canggu
        status:
          $ref: "#/components/schemas/ConversationStatus"
          default: open
        priority:
          $ref: "#/components/schemas/ConversationPriority"
          default: normal
        channel:
          type: string
          default: api
          example: whatsapp
        customerId:
          type: string
          example: cus_x6q5vl75f5d53m7oet2k4r5w6a
        assignee:
          $ref: "#/components/schemas/Assignee"
        team:
          type: string
          description: External team or routing hint.
          example: canggu-rentals
        tags:
          type: array
          items:
            type: string
          example:
            - villa
            - lead
        externalObject:
          $ref: "#/components/schemas/ExternalObject"
        metadata:
          type: object
          additionalProperties: true
          example:
            source: owner_app
        channelIdentityId:
          type: string
          description: The channel identity that opened this conversation (provenance).
          example: cid_x6q5vl75f5d53m7oet2k4r5w6a
        routing:
          $ref: "#/components/schemas/ConversationRouting"
    ConversationRouting:
      type: object
      description: >
        Routing hints resolved at create time. Unresolved hints never fail the
        create — they leave the conversation unassigned and are recorded in the
        response's routingNotes for debugging.
      properties:
        teamKey:
          type: string
          description: Team key to route to. Falls back to the workspace fallback team if unknown.
          example: canggu-rentals
        assigneeId:
          type: string
          description: Assign directly to this assignee id.
        assigneeType:
          type: string
          enum:
            - user
            - ai-agent
          default: user
        assigneeExternalId:
          type: string
          description: Your external id for an assignee (recorded as a suggestion; not yet resolved).
        priority:
          type: string
        tags:
          type: array
          items:
            type: string
        strategy:
          type: string
          description: Routing strategy hint (e.g. round_robin). Recorded for future use.
    UpdateConversationRequest:
      type: object
      description: >
        Omitted fields are left unchanged. Tags, externalObject, and metadata
        replace their full stored values when provided.
      minProperties: 1
      properties:
        subject:
          type: string
          minLength: 1
          example: Inquiry for 2BR Villa in Canggu
        status:
          $ref: "#/components/schemas/ConversationStatus"
        priority:
          $ref: "#/components/schemas/ConversationPriority"
        channel:
          type: string
          minLength: 1
          example: whatsapp
        assignee:
          $ref: "#/components/schemas/Assignee"
        team:
          type: string
          description: External team or routing hint.
          example: canggu-rentals
        tags:
          type: array
          items:
            type: string
          example:
            - urgent
            - villa
        externalObject:
          $ref: "#/components/schemas/ExternalObject"
        metadata:
          type: object
          additionalProperties: true
          example:
            source: triage
    AssignConversationRequest:
      type: object
      description: >
        Requires assignee or team. Send an assignee with an empty id, or an
        empty team string, to clear that field.
      minProperties: 1
      properties:
        assignee:
          $ref: "#/components/schemas/Assignee"
        team:
          type: string
          description: External team or routing hint.
          example: canggu-rentals
    ResolveConversationRequest:
      type: object
      properties:
        status:
          type: string
          description: >
            Workspace-defined completed-type status key. Defaults to the
            workspace's default completed status (`resolved` for seeded
            workspaces). Active-type keys are rejected with
            `conversations.invalid_resolution_status`.
          example: resolved
    Conversation:
      type: object
      description: >
        Every key is always present; optional fields are null when unset.
      required:
        - id
        - externalId
        - subject
        - status
        - priority
        - channel
        - customerId
        - assignee
        - team
        - tags
        - externalObject
        - metadata
        - channelIdentityId
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: conv_x6q5vl75f5d53m7oet2k4r5w6a
        externalId:
          type: [string, "null"]
          example: inquiry_123
        subject:
          type: string
          example: Inquiry for 2BR Villa in Canggu
        status:
          $ref: "#/components/schemas/ConversationStatus"
        priority:
          $ref: "#/components/schemas/ConversationPriority"
        channel:
          type: string
          example: whatsapp
        customerId:
          type: string
          example: cus_x6q5vl75f5d53m7oet2k4r5w6a
        assignee:
          oneOf:
            - $ref: "#/components/schemas/Assignee"
            - type: "null"
          description: Null when the conversation is unassigned.
        team:
          type: [string, "null"]
          example: canggu-rentals
        tags:
          type: array
          items:
            type: string
        externalObject:
          oneOf:
            - $ref: "#/components/schemas/ExternalObject"
            - type: "null"
          description: Null when no external object is attached.
        metadata:
          type: object
          additionalProperties: true
        channelIdentityId:
          type: [string, "null"]
          description: The channel identity that opened this conversation, if any.
          example: cid_x6q5vl75f5d53m7oet2k4r5w6a
        routingNotes:
          type: object
          additionalProperties: true
          nullable: true
          description: Routing hints and resolution reasons recorded at create time (debugging).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ListConversationsResponse:
      type: object
      required:
        - data
        - hasMore
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Conversation"
        hasMore:
          $ref: "#/components/schemas/HasMore"
        nextCursor:
          $ref: "#/components/schemas/NextCursor"
    MessageSender:
      type: string
      enum:
        - customer
        - agent
        - ai
        - system
    Attachment:
      type: object
      description: >
        A file on a message, referenced by URL (and/or your own id). Every key
        is always present; unset fields are null. Provide at least id or url.
      required:
        - id
        - url
        - filename
        - contentType
        - sizeBytes
        - metadata
      properties:
        id:
          type: [string, "null"]
          description: Your external attachment id, if any.
          example: file_123
        url:
          type: [string, "null"]
          format: uri
          example: https://example.com/files/inquiry.png
        filename:
          type: [string, "null"]
          example: inquiry.png
        contentType:
          type: [string, "null"]
          example: image/png
        sizeBytes:
          type: integer
          format: int64
          minimum: 0
          example: 12345
        metadata:
          type: object
          additionalProperties: true
          example:
            kind: screenshot
    WebhookStatus:
      type: string
      enum:
        - active
        - disabled
    Assignment:
      type: object
      required:
        - id
        - conversationId
        - assignee
        - role
        - status
        - offeredAt
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: asg_x6q5vl75f5d53m7oet2k4r5w6a
        conversationId:
          type: string
        assignee:
          $ref: "#/components/schemas/Assignee"
        role:
          type: string
          example: owner
        status:
          type: string
          enum:
            - offered
            - accepted
            - declined
            - active
            - revoked
            - completed
            - expired
        offeredAt:
          type: string
          format: date-time
        respondedAt:
          type: string
          format: date-time
          nullable: true
        resolvedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ListAssignmentsResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Assignment"
    WebhookEvent:
      type: string
      enum:
        - customer.created
        - customer.updated
        - conversation.created
        - conversation.updated
        - conversation.assigned
        - conversation.resolved
        - message.created
        - message.delivered
        - message.failed
    WebhookTestResult:
      type: object
      required:
        - eventId
        - delivered
      properties:
        eventId:
          type: string
          description: Synthetic test event id included in the delivery headers.
          example: evt_x6q5vl75f5d53m7oet2k4r5w6a
        delivered:
          type: boolean
        responseStatus:
          type: [integer, "null"]
          description: HTTP status returned by your endpoint. Null when the request never completed.
          example: 200
        responseSnippet:
          type: [string, "null"]
          description: First 512 bytes of the response body or failure reason.
    WebhookDelivery:
      type: object
      description: One delivery attempt series for one event and subscription.
      required:
        - id
        - eventId
        - eventType
        - attempt
        - status
        - createdAt
        - completedAt
      properties:
        id:
          type: string
          example: whd_x6q5vl75f5d53m7oet2k4r5w6a
        eventId:
          type: string
          example: evt_x6q5vl75f5d53m7oet2k4r5w6a
        eventType:
          $ref: "#/components/schemas/WebhookEvent"
        attempt:
          type: integer
          description: Attempts made so far (max 6, with 30s/2m/10m/1h/6h backoff).
        status:
          type: string
          enum:
            - pending
            - sending
            - delivered
            - failed
        responseStatus:
          type: [integer, "null"]
          description: Last HTTP status from your endpoint. Null when the request never completed.
        responseSnippet:
          type: [string, "null"]
          description: First 512 bytes of the last response body or failure reason.
        createdAt:
          type: string
          format: date-time
        completedAt:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
          description: Null while retries are still scheduled.
    ListWebhookDeliveriesResponse:
      type: object
      required:
        - data
        - hasMore
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDelivery"
        hasMore:
          $ref: "#/components/schemas/HasMore"
        nextCursor:
          $ref: "#/components/schemas/NextCursor"
    CreateWebhookRequest:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          format: uri
          description: HTTPS endpoint that will receive signed webhook deliveries.
          example: https://example.com/protodesk/webhooks
        events:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/WebhookEvent"
          example:
            - message.created
            - conversation.resolved
        status:
          $ref: "#/components/schemas/WebhookStatus"
          default: active
        metadata:
          type: object
          additionalProperties: true
          example:
            source: owner_app
    UpdateWebhookRequest:
      type: object
      minProperties: 1
      properties:
        url:
          type: string
          format: uri
          description: HTTPS endpoint that will receive signed webhook deliveries.
          example: https://example.com/protodesk/webhooks
        events:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/WebhookEvent"
        status:
          $ref: "#/components/schemas/WebhookStatus"
        metadata:
          type: object
          additionalProperties: true
          description: Replaces the full webhook metadata object.
          example:
            source: owner_app
    WebhookSubscription:
      type: object
      required:
        - id
        - url
        - events
        - status
        - metadata
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: wh_x6q5vl75f5d53m7oet2k4r5w6a
        url:
          type: string
          format: uri
          example: https://example.com/protodesk/webhooks
        events:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEvent"
        status:
          $ref: "#/components/schemas/WebhookStatus"
        secret:
          type: [string, "null"]
          description: Signing secret; returned only when the webhook is created, null on reads.
          example: whsec_pz6kyr2ftwsn6c6q3a4oky2d35txmgaj3t35u2quy4ielzqro3za
        metadata:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ListWebhooksResponse:
      type: object
      required:
        - data
        - hasMore
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookSubscription"
        hasMore:
          $ref: "#/components/schemas/HasMore"
        nextCursor:
          $ref: "#/components/schemas/NextCursor"
    CreateMessageRequest:
      type: object
      description: Requires body or at least one attachment.
      properties:
        externalId:
          type: string
          description: Your stable id for this message.
          example: msg_123
        sender:
          $ref: "#/components/schemas/MessageSender"
          default: customer
        body:
          type: string
          example: Hi, is this villa available for yearly rental?
        channel:
          type: string
          default: api
          example: whatsapp
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/Attachment"
        metadata:
          type: object
          additionalProperties: true
          example:
            source: owner_app
        internal:
          type: boolean
          default: false
          description: True for internal notes that should not be sent to the customer.
    Message:
      type: object
      description: >
        Every key is always present; optional fields are null when unset.
      required:
        - id
        - externalId
        - conversationId
        - sender
        - body
        - channel
        - attachments
        - metadata
        - internal
        - createdAt
      properties:
        id:
          type: string
          example: msg_x6q5vl75f5d53m7oet2k4r5w6a
        externalId:
          type: [string, "null"]
          example: msg_123
        conversationId:
          type: string
          example: conv_x6q5vl75f5d53m7oet2k4r5w6a
        sender:
          $ref: "#/components/schemas/MessageSender"
        body:
          type: string
          example: Hi, is this villa available for yearly rental?
        channel:
          type: string
          example: whatsapp
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/Attachment"
        metadata:
          type: object
          additionalProperties: true
        internal:
          type: boolean
        createdAt:
          type: string
          format: date-time
    ListMessagesResponse:
      type: object
      required:
        - data
        - hasMore
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Message"
        hasMore:
          $ref: "#/components/schemas/HasMore"
        nextCursor:
          $ref: "#/components/schemas/NextCursor"
    ListCustomersResponse:
      type: object
      required:
        - data
        - hasMore
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Customer"
        hasMore:
          $ref: "#/components/schemas/HasMore"
        nextCursor:
          $ref: "#/components/schemas/NextCursor"
    HasMore:
      type: boolean
      description: True when more rows exist beyond this page.
    NextCursor:
      type: [string, "null"]
      description: >-
        Opaque cursor for the next page. Pass it as the cursor query param.
        Null when hasMore is false.
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          $ref: "#/components/schemas/Error"
    Error:
      type: object
      required:
        - code
        - message
        - requestId
      properties:
        code:
          type: string
          example: route.not_found
        message:
          type: string
          example: Not found
        requestId:
          type: string
          example: req_x6q5vl75f5d53m7oet2k4r5w6a
