openapi: 3.1.0
info:
  title: Waveline Integrations API
  version: 1.0.0
  description: >-
    Integration API for contacts, phone activity, billed outbound SMS/MMS, media, OAuth, and signed webhooks. API keys cover the entire workspace or exactly one inbox; OAuth also supports selected shared inboxes (1–100). Only workspace Owners can grant entire-workspace access, which includes all current and future shared and personal inboxes for calls and messages. Owners and Admins can connect one shared inbox. Workspace Members can connect one shared inbox where they have the Manager role. Any teammate can connect their own personal inbox. Existing grants retain their recorded access and are not upgraded automatically. Unknown fields are rejected. JSON requests are limited to 64 KiB, binary uploads to 5 MiB, and JSON responses to 4 MiB.
servers:
  - url: https://{apiHost}
    variables:
      apiHost:
        default: api.waveline.tel
security:
  - WorkspaceApiKey: []
  - OAuthAccessToken: []
paths:
  /v1/connection:
    get:
      operationId: getConnection
      summary: Test API credentials and inspect their scope
      responses:
        '200':
          description: Current connection; no API key or signing secret is returned.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Connection'
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
  /v1/contacts:
    get:
      operationId: listContacts
      summary: List or find accessible contacts
      description: |
        Requires contacts.read. Phone and case-insensitive email filters use exact AND matching
        against all saved channels, not only the primary channel. Multiple matches are returned;
        callers must not silently select one. Deleted and inaccessible contacts are excluded.
        The query filter searches normalized contact names, company, role and primary channels
        as a literal case-insensitive substring. Filters combine with AND and remain cursor-bound.
        Workspace contact access includes only workspace-visible contacts. Contacts restricted to shared or
        personal inboxes retain their audience and are excluded from Workspace contact sync. Personal-inbox connections follow
        the member's contact ownership and shares through their authorized personal inbox.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/CreatedAfter'
        - $ref: '#/components/parameters/CreatedBefore'
        - name: phone
          in: query
          schema:
            type: string
            pattern: ^\+[1-9][0-9]{1,14}$
        - name: email
          in: query
          schema:
            type: string
        - name: query
          in: query
          description: Literal substring of the contact search text; whitespace is trimmed. Wildcards are not interpreted.
          schema:
            type: string
            minLength: 3
            maxLength: 100
        - $ref: '#/components/parameters/Order'
      responses:
        '200':
          description: Contacts in the requested order (ascending stable ID by default).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Contact'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createContact
      summary: Create a contact
      description: >-
        Requires contacts.read and contacts.write. Workspace connections create workspace contacts.
        Selected-inbox OAuth connections create contacts shared only with those inboxes. Single-inbox connections create contacts shared only with that inbox; personal-inbox contacts
        are owned by the subject. Clients cannot override the audience. Request ID deduplication lasts seven days.
        A nonnull valid E.164 phone is required; skip source records without a phone. Missing or null phone returns CONTACT_PHONE_REQUIRED.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactCreate'
      responses:
        '201':
          $ref: '#/components/responses/Contact'
        default:
          $ref: '#/components/responses/Error'
  /v1/contacts/upsert:
    post:
      operationId: upsertContact
      summary: Create or update a contact by external identity
      description: >-
        Requires contacts.read and contacts.write. The exact case-sensitive external_source and external_id
        pair is saved within this authorized connection. Use a stable source app/account identifier.
        A new pair creates an ordinary editable contact in the connection scope; a known pair updates
        that contact using the same field and audience rules as PATCH. Omitted display_name is derived automatically.
        A new pair requires a nonnull valid E.164 phone. For a known pair, phone may be omitted when the mapped
        contact already has a valid saved phone. Setting phone to null follows PATCH promotion rules; it never
        removes the contact's last usable phone. Missing phone on creation returns CONTACT_PHONE_REQUIRED.
        No phone/email matching or automatic linking to existing contacts occurs. The mapping persists beyond
        the seven-day request replay cache. Use a different Idempotency-Key for each source change event.
        A deleted, merged-away, or inaccessible mapped contact returns 404; source-managed contacts reject writes.
        A new connection has separate mappings. Source fields never grant additional access.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactUpsert'
      responses:
        '200':
          $ref: '#/components/responses/Contact'
        default:
          $ref: '#/components/responses/Error'
  /v1/contacts/{id}:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getContact
      summary: Get one accessible contact
      description: Requires contacts.read. Missing and inaccessible resources return 404.
      responses:
        '200':
          $ref: '#/components/responses/Contact'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateContact
      summary: Update selected fields of an accessible contact
      description: |
        Requires contacts.read and contacts.write. Omitted fields are unchanged. Nullable fields
        accept null to clear the value, subject to the phone requirement. Phone/email changes retain other
        saved channels. The resulting contact must have a valid saved phone: omitted phone preserves it;
        phone=null promotes another saved phone to primary and retains the previous phone as a secondary
        number. If no other valid phone remains, the update fails with CONTACT_PHONE_REQUIRED without changing
        the contact. Email=null clears the primary email designation. display_name cannot be cleared explicitly.
        When omitted, name or primary-channel edits recompute it from the resulting first/last name,
        company, phone, or email, falling back to Unknown Contact. Unrelated edits preserve it.
        Source-managed read-only contacts reject changes. No resource_version or updated_at write is accepted.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactWrite'
      responses:
        '200':
          $ref: '#/components/responses/Contact'
        default:
          $ref: '#/components/responses/Error'
  /v1/calls:
    get:
      operationId: listCalls
      summary: List accessible external calls
      description: |
        Requires calls.read. Workspace returns one call with its complete journey. A single-inbox
        connection receives only that inbox's interactions, grouped into one call entry with the same
        parent ID, including transfers to and from other inboxes. Other inboxes' interaction details
        are excluded. Legacy multi-inbox grants still require every associated inbox to be allowed
        and current. Internal calls are excluded.
        The date filters apply to the view's start_time. This endpoint includes ongoing calls. An inbox's
        end_time can be set while the parent call continues in another inbox; call.completed events and
        setup samples wait for the entire parent call to end.
      parameters:
        - $ref: '#/components/parameters/ActivityInbox'
        - $ref: '#/components/parameters/ActivityDirection'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/CreatedAfter'
        - $ref: '#/components/parameters/CreatedBefore'
        - $ref: '#/components/parameters/Order'
        - $ref: '#/components/parameters/IncludeLabels'
        - name: external_number
          in: query
          description: Exact external party phone number. Combines with the date filters.
          schema:
            type: string
            pattern: '^\+[1-9][0-9]{1,14}$'
        - name: outcome
          in: query
          description: >-
            Only calls whose reported outcome (the outcome field of each returned call, for this connection's view)
            equals this value. unknown includes calls still in progress.
          schema:
            type: string
            enum: [completed, missed, voicemail, transferred, forwarded, failed, unknown]
        - name: employee_id
          in: query
          description: Only calls with a visible interaction this employee answered.
          schema:
            $ref: '#/components/schemas/RecordId'
        - name: sentiment
          in: query
          description: >-
            Only calls with at least one accessible recording carrying this saved sentiment. Calls without saved
            sentiment never match; null is not neutral.
          schema:
            type: string
            enum: [positive, negative, neutral, mixed]
        - name: unhandled_only
          in: query
          description: >-
            true returns calls currently marked unhandled and needing follow-up in an accessible inbox, or in inbox_id when provided. This filter does not expand the connection's access.
          schema:
            type: string
            enum: ['true', 'false']
      responses:
        '200':
          description: Calls in the requested order (ascending stable ID by default).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Call'
        default:
          $ref: '#/components/responses/Error'
  /v1/calls/{id}:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getCall
      summary: Get one accessible external call
      description: >-
        Requires calls.read. The ID is the parent Waveline record ID, not a provider call SID.
        The response uses the same connection-specific projection as listCalls and call.completed events.
      parameters:
        - $ref: '#/components/parameters/IncludeLabels'
      responses:
        '200':
          description: >-
            Call metadata and outcome derived from persisted call facts. Recordings, summaries, and transcripts are not
            included.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Call'
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
  /v1/messages:
    get:
      operationId: listMessages
      summary: List accessible external conversation messages
      description: |
        Requires messages.read. Every inbox in the conversation must be an allowed, current inbox.
        Internal-only conversations are excluded. Returns individual existing Waveline message records,
        not the logical recipient-group aggregation proposed in the full plan.
        external_number matches a participant in the conversation, not a claimed per-message recipient.
      parameters:
        - $ref: '#/components/parameters/ActivityInbox'
        - $ref: '#/components/parameters/ActivityDirection'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/CreatedAfter'
        - $ref: '#/components/parameters/CreatedBefore'
        - name: external_number
          in: query
          description: Exact external conversation participant phone number.
          schema:
            type: string
            pattern: '^\+[1-9][0-9]{1,14}$'
        - name: conversation_id
          in: query
          description: Only messages from this authorized conversation. All filters combine with AND.
          schema:
            $ref: '#/components/schemas/RecordId'
        - $ref: '#/components/parameters/Order'
        - $ref: '#/components/parameters/IncludeLabels'
      responses:
        '200':
          description: Messages in the requested order (ascending stable ID by default).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Message'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: sendMessage
      summary: Send one billed external recipient message
      description: >-
        Requires messages.write and messages.read, a selected inbox with a registered sending number, and an invoice-backed subscription. Each accepted recipient send costs US$0.01 before tax, including sends that later fail delivery. Segments, attachment parts, and retries under the same Idempotency-Key do not add integration sending fees. Send deduplication is permanent. Each request supports one E.164 external recipient and must include text or attachments. Messaging registration, opt-out, and recipient-blocking requirements still apply. Acceptance does not confirm delivery.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - inbox_id
                - to
              properties:
                inbox_id:
                  $ref: '#/components/schemas/RecordId'
                to:
                  type: string
                  pattern: ^\+[1-9][0-9]{1,14}$
                body:
                  type: string
                  maxLength: 1600
                from:
                  type: string
                attachment_ids:
                  type: array
                  maxItems: 10
                  items:
                    $ref: '#/components/schemas/RecordId'
                  description: >-
                    Finalized upload attachment IDs for this connection/inbox or attachments on currently accessible
                    messages. Total<=5MiB. Each upload is consumed by one logical send.
              additionalProperties: false
      responses:
        '202':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/MessageSend'
        default:
          $ref: '#/components/responses/Error'
  /v1/messages/{id}:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getMessage
      summary: Get one accessible message
      description: Requires messages.read. Includes completed attachment metadata; download URLs require a separate authorized request.
      parameters:
        - $ref: '#/components/parameters/IncludeLabels'
      responses:
        '200':
          description: Message record.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Message'
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
  /v1/phone-numbers:
    get:
      operationId: listPhoneNumbers
      summary: List accessible Waveline phone numbers
      description: >-
        Requires phone_numbers.read. Lists the tenant's own inventory within the granted inboxes.
        Explicit Workspace grants also include unassigned numbers and personal inbox numbers.
        Deleted numbers and numbers assigned to deleted inboxes are excluded. Reading numbers does
        not grant sending or number-management permissions. Missing and inaccessible inbox filters return 404.
      parameters:
        - name: limit
          in: query
          description: Number of phone numbers per page; defaults to 20.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - $ref: '#/components/parameters/Cursor'
        - name: inbox_id
          in: query
          description: Narrow to one inbox within this connection's current access.
          schema:
            $ref: '#/components/schemas/RecordId'
      responses:
        '200':
          description: Numbers ordered by ascending stable ID. Page size defaults to 20, maximum 100.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/PhoneNumber'
        default:
          $ref: '#/components/responses/Error'
  /v1/analytics/calls:
    get:
      operationId: getCallAnalytics
      summary: Read scoped external call analytics
      description: >-
        Requires analytics.read. Returns external-call metrics for the connection's currently authorized inboxes. unique_calls counts distinct calls within the filtered interval; interaction_count and all *_interactions fields count inbox interactions. A transferred call can have multiple interactions and appear in multiple groups, so do not add group unique-call counts to calculate the report total. Day and hour buckets use UTC; empty buckets are omitted. Ranges are limited to 93 elapsed days, or 31 days for hourly reports. More than 1000 groups returns 413 RESPONSE_TOO_LARGE; narrow the period or inbox. The response contains aggregate metrics, not raw call content or external participant phone numbers.
      parameters:
        - name: start_date
          in: query
          required: true
          description: Inclusive start timestamp with an explicit time zone.
          schema: { type: string, format: date-time }
        - name: end_date
          in: query
          required: true
          description: Exclusive end timestamp with an explicit time zone; must follow start_date.
          schema: { type: string, format: date-time }
        - name: inbox_id
          in: query
          description: Narrow to one authorized inbox. Missing and inaccessible inboxes both return 404.
          schema:
            $ref: '#/components/schemas/RecordId'
        - name: group_by
          in: query
          schema:
            type: string
            enum: [day, hour, inbox, phone_number, employee]
            default: day
        - name: compare_to
          in: query
          description: >-
            previous_period adds a comparison for the equal-length window ending at start_date, with its
            summary, groups and precomputed summary changes.
          schema:
            type: string
            enum: [previous_period]
      responses:
        '200':
          description: Complete summary and groups for the filtered period. Recent activity may take time to appear.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/CallAnalytics'
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
  /v1/conversations:
    get:
      operationId: listConversations
      summary: List accessible conversations by recent activity
      description: |
        Requires messages.read. Returns one row per readable conversation, most recent activity first, using the
        same visibility rules as listMessages. Each row carries a preview of the last message and awaiting_reply
        (the external party sent the last message). Personal assistant (OAuth) connections also receive the
        member's own unread state; API keys receive null and cannot use unread_only. The date filters apply to
        the last activity. The cursor is anchored to the last returned conversation's current activity, so a
        conversation that receives a new message while you page can appear again; none are skipped.
      parameters:
        - $ref: '#/components/parameters/ActivityInbox'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/CreatedAfter'
        - $ref: '#/components/parameters/CreatedBefore'
        - $ref: '#/components/parameters/IncludeLabels'
        - name: external_number
          in: query
          description: Exact external conversation participant phone number.
          schema:
            type: string
            pattern: '^\+[1-9][0-9]{1,14}$'
        - name: unread_only
          in: query
          description: true returns only conversations unread by the authenticated member. Personal assistant connections only.
          schema:
            type: string
            enum: ['true', 'false']
      responses:
        '200':
          description: Conversations ordered by most recent activity.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Conversation'
        default:
          $ref: '#/components/responses/Error'
  /v1/members:
    get:
      operationId: listMembers
      summary: List teammates in accessible inboxes
      description: >-
        Not available to REST API keys or service connections; these receive 403. Teammate lookup is available through Waveline's supported personal assistant connection with inboxes.read. Do not send assistant credentials to the REST API. Within that supported connection, the operation lists active teammates who participate in accessible inboxes, ordered by employee ID. It returns names and inbox memberships, not contact details. employee_id identifies a teammate in call records; user_id identifies them in ring groups.
      parameters:
        - $ref: '#/components/parameters/ActivityInbox'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Teammates ordered by employee ID.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Member'
        default:
          $ref: '#/components/responses/Error'
  /v1/inboxes:
    get:
      operationId: listInboxes
      summary: List the connection's allowed inboxes
      description: >-
        Requires inboxes.read. Returns the complete allowed inbox list. Membership fields describe the member
        behind a personal assistant (OAuth) connection and are null for API keys.
      responses:
        '200':
          description: Allowed current inboxes.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Inbox'
        default:
          $ref: '#/components/responses/Error'
  /v1/webhook-subscriptions:
    get:
      operationId: listWebhookSubscriptions
      summary: List this connection's webhook subscriptions
      description: >-
        Requires webhooks.read. Returns all non-deleted subscriptions for this connection, with no pagination or query
        filters. Signing secrets are never included.
      responses:
        '200':
          description: Connection subscriptions.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Subscription'
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createWebhookSubscription
      summary: Subscribe to one or more event types
      description: |
        Requires webhooks.write plus each selected event's resource read scope. Destinations must use
        an exact operator-approved HTTPS origin. The default is https://hooks.zapier.com. No redirects
        are followed. Successful creation and retries return the current subscription and signing secret.
        A matching active connection/callback/event-set returns its existing subscription and secret.
        A matching disabled subscription returns 409; deleted tombstones do not block fresh creation.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - target_url
                - event_types
              properties:
                target_url:
                  type: string
                  format: uri
                  maxLength: 2048
                event_types:
                  type: array
                  minItems: 1
                  maxItems: 6
                  items:
                    $ref: '#/components/schemas/EventType'
      responses:
        '201':
          description: Subscription and its private signing secret. Store the secret immediately.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    required:
                      - subscription
                      - signing_secret
                    properties:
                      subscription:
                        $ref: '#/components/schemas/Subscription'
                      signing_secret:
                        type: string
                        pattern: ^whsec_
                        description: whsec_ followed by base64-encoded 32 random bytes.
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
  /v1/webhook-subscriptions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      operationId: updateWebhookSubscription
      summary: Enable or disable a subscription
      description: Requires webhooks.write. Event types and destination cannot be changed by this operation.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - status
              properties:
                status:
                  type: string
                  enum:
                    - active
                    - disabled
      responses:
        '200':
          $ref: '#/components/responses/Subscription'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteWebhookSubscription
      summary: Delete a subscription and cancel outstanding deliveries
      description: Requires webhooks.write. Retains a tombstone and delivery history. A deleted subscription cannot be reactivated.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          $ref: '#/components/responses/Subscription'
        default:
          $ref: '#/components/responses/Error'
  /v1/webhook-samples/{event_type}:
    parameters:
      - name: event_type
        in: path
        required: true
        schema:
          $ref: '#/components/schemas/EventType'
    get:
      operationId: getWebhookSamples
      summary: Get setup samples shaped like live webhook events
      description: |
        Requires the event's resource read scope. Uses accessible real resource data and does not
        send a webhook or create an event. Sample IDs are prefixed sample_. At most three samples
        are selected from the 100 most recent accessible resources, newest first. Returns an empty array when no
        suitable sample is available in that bounded lookup. Static Zapier examples are illustrative only.
      parameters:
        - $ref: '#/components/parameters/ActivityInbox'
        - $ref: '#/components/parameters/ActivityDirection'
        - name: recording_type
          in: query
          description: Only for call.recording.ready, call.transcript.ready, or call.summary.ready samples.
          schema:
            type: string
            enum: [call, voicemail]
      responses:
        '200':
          description: Sample event envelopes.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EventEnvelope'
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
  /v1/webhook-deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: Inspect recent webhook delivery outcomes
      description: >-
        Requires webhooks.read. Returns the newest limit records without event bodies or secrets. Only limit is
        supported; no cursor or date filters. Replay is an owner/admin action in the Waveline UI.
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Recent delivery metadata, ordered newest first.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Delivery'
                  rate_limit:
                    $ref: '#/components/schemas/RateLimit'
        default:
          $ref: '#/components/responses/Error'
  /v1/contacts/{id}/notes:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: listContactNotes
      summary: List contact notes
      description: Requires contacts.read. Missing and inaccessible parent resources return 404.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Page ordered by ascending ID.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Note'
                  next_cursor:
                    type:
                      - string
                      - 'null'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createContactNote
      summary: Add a contact note
      description: >-
        Requires contacts.read and contacts.write. Idempotency lasts seven days. Source-managed read-only contacts
        reject writes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  minLength: 1
                  maxLength: 10000
              additionalProperties: false
      responses:
        '201':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Note'
        default:
          $ref: '#/components/responses/Error'
  /v1/contacts/{id}/notes/{note_id}:
    parameters:
      - $ref: '#/components/parameters/RecordId'
      - name: note_id
        in: path
        required: true
        schema:
          $ref: '#/components/schemas/RecordId'
    patch:
      operationId: updateContactNote
      summary: Update a contact note
      description: >-
        Requires contacts.read and contacts.write. Idempotency lasts seven days. Source-managed read-only contacts
        reject writes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  minLength: 1
                  maxLength: 10000
              additionalProperties: false
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Note'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteContactNote
      summary: Soft-delete a contact note
      description: >-
        Requires contacts.read and contacts.write. Idempotency lasts seven days. Source-managed read-only contacts
        reject writes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Note'
        default:
          $ref: '#/components/responses/Error'
  /v1/contact-properties:
    get:
      operationId: listContactPropertyDefinitions
      summary: List existing workspace contact property definitions
      description: Requires contacts.read. Missing and inaccessible parent resources return 404.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Page ordered by ascending ID.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ContactPropertyDefinition'
                  next_cursor:
                    type:
                      - string
                      - 'null'
        default:
          $ref: '#/components/responses/Error'
  /v1/contacts/{id}/properties:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: listContactProperties
      summary: List contact property values
      description: Requires contacts.read. Missing and inaccessible parent resources return 404.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Page ordered by ascending ID.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ContactPropertyValue'
                  next_cursor:
                    type:
                      - string
                      - 'null'
        default:
          $ref: '#/components/responses/Error'
  /v1/contacts/{id}/properties/{property_id}:
    parameters:
      - $ref: '#/components/parameters/RecordId'
      - name: property_id
        in: path
        required: true
        schema:
          $ref: '#/components/schemas/RecordId'
    patch:
      operationId: setContactProperty
      summary: Set a typed value on an existing contact property
      description: >-
        Requires contacts.read and contacts.write. Idempotency lasts seven days. Source-managed read-only contacts
        reject writes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - value
              properties:
                value:
                  description: >-
                    Must match the existing definition type. Null clears. Dates use YYYY-MM-DD, phone uses E.164,
                    multi-select uses defined choices.
              additionalProperties: false
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/ContactPropertyValue'
        default:
          $ref: '#/components/responses/Error'
  /v1/calls/{id}/recordings:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: listCallRecordings
      summary: List recording metadata for a call
      description: Requires calls.read. Missing and inaccessible parent resources return 404.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Page ordered by ascending ID.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Recording'
                  next_cursor:
                    type:
                      - string
                      - 'null'
        default:
          $ref: '#/components/responses/Error'
  /v1/recordings/{id}:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getRecording
      summary: Get recording metadata
      description: >-
        Requires calls.read. The parent call must be accessible, and every inbox on the recording must
        be currently authorized. A partial inbox call view does not grant access to recordings covering other inboxes.
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Recording'
        default:
          $ref: '#/components/responses/Error'
  /v1/recordings/{id}/transcript:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getRecordingTranscript
      summary: Get recording transcript
      description: >-
        Requires calls.read. Returns saved text only; does not start or charge for transcription/summary generation.
        Text may be null while unavailable.
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/MediaText'
        default:
          $ref: '#/components/responses/Error'
  /v1/recordings/{id}/summary:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getRecordingSummary
      summary: Get recording summary
      description: >-
        Requires calls.read. Returns saved text only; does not start or charge for transcription/summary generation.
        Text may be null while unavailable.
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/MediaText'
        default:
          $ref: '#/components/responses/Error'
  /v1/recordings/{id}/download:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getRecordingDownload
      summary: Get recording download
      description: >-
        Requires calls.read. The private URL expires after five minutes; revocation blocks new URLs but cannot
        invalidate an already issued URL before expiry.
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/MediaDownload'
        default:
          $ref: '#/components/responses/Error'
  /v1/messages/{id}/attachments:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: listMessageAttachments
      summary: List completed message attachments
      description: Requires messages.read. Missing and inaccessible parent resources return 404.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Page ordered by ascending ID.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MessageAttachment'
                  next_cursor:
                    type:
                      - string
                      - 'null'
        default:
          $ref: '#/components/responses/Error'
  /v1/attachments/{id}/download:
    parameters:
      - $ref: '#/components/parameters/RecordId'
    get:
      operationId: getAttachmentDownload
      summary: Get a private message attachment download URL
      description: Requires messages.read and current access to the associated message. The URL expires after five minutes.
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/MediaDownload'
        default:
          $ref: '#/components/responses/Error'
  /v1/message-uploads:
    post:
      operationId: createMessageUpload
      summary: Reserve a message media upload
      description: >-
        Requires messages.read+messages.write and an allowed inbox. Expires in 24 hours; at most 25 unused
        unexpired reservations/connection. Uploading does not incur the sending fee. Only ready media can be sent, once
        per upload.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - inbox_id
                - file_name
                - mime_type
                - size_bytes
                - sha256
              properties:
                inbox_id:
                  $ref: '#/components/schemas/RecordId'
                file_name:
                  type: string
                  pattern: ^[A-Za-z0-9][A-Za-z0-9._ -]{0,119}$
                mime_type:
                  type: string
                  enum:
                    - image/jpeg
                    - image/png
                    - image/gif
                    - image/webp
                    - application/pdf
                    - audio/mpeg
                    - audio/mp4
                    - video/mp4
                size_bytes:
                  type: integer
                  minimum: 1
                  maximum: 5242880
                sha256:
                  type: string
                  pattern: ^[a-f0-9]{64}$
              additionalProperties: false
      responses:
        '201':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/MessageUpload'
        default:
          $ref: '#/components/responses/Error'
  /v1/message-uploads/{upload_id}/content:
    parameters:
      - name: upload_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    put:
      operationId: uploadMessageContent
      summary: Upload exactly the reserved media bytes
      description: >-
        Use reserved Content-Type, same Bearer credentials, exact size and SHA-256. Content-Encoding is rejected. Retry
        identical bytes at this URL; existing objects are never overwritten. Returns ready attachment_id. The upload ID
        provides idempotency; no separate Idempotency-Key is needed.
      requestBody:
        required: true
        content:
          image/jpeg:
            schema:
              type: string
              format: binary
          image/png:
            schema:
              type: string
              format: binary
          image/gif:
            schema:
              type: string
              format: binary
          image/webp:
            schema:
              type: string
              format: binary
          application/pdf:
            schema:
              type: string
              format: binary
          audio/mpeg:
            schema:
              type: string
              format: binary
          audio/mp4:
            schema:
              type: string
              format: binary
          video/mp4:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: Authorized result.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/MessageUpload'
        default:
          $ref: '#/components/responses/Error'
components:
  securitySchemes:
    WorkspaceApiKey:
      type: http
      scheme: bearer
      bearerFormat: Waveline API key
      description: wl_live_ or wl_test_ followed by 64 lowercase hex characters. Use separate keys per integration and environment.
    OAuthAccessToken:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: >-
        15-minute OAuth access token obtained using authorization code + S256 PKCE. Discover endpoints at
        /.well-known/oauth-authorization-server. New connections and rotating refresh tokens have no scheduled
        expiration. Access tokens still expire after 15 minutes and must be refreshed. Revocation and current
        connection access remain enforced. Tokens are bound to the resource authorized during consent.
  parameters:
    ActivityInbox:
      name: inbox_id
      in: query
      description: Filter to activity involving this accessible inbox. Does not change connection access or the returned resource projection.
      schema:
        $ref: '#/components/schemas/RecordId'
    ActivityDirection:
      name: direction
      in: query
      description: Filter by external call or message direction.
      schema:
        type: string
        enum: [inbound, outbound]
    Order:
      name: order
      in: query
      description: >-
        oldest (the default) pages forward by ascending stable ID. newest returns the most recent records first and
        next_cursor continues with older records. The order is bound to the cursor.
      schema:
        type: string
        enum: [oldest, newest]
        default: oldest
    IncludeLabels:
      name: include_labels
      in: query
      description: >-
        true adds display labels to each record: inbox_labels for accessible inboxes, contacts matched to external
        numbers (requires contacts.read; omitted otherwise) and, for messages, sender_name. The default record shape
        and webhook payloads are unchanged.
      schema:
        type: string
        enum: ['true', 'false']
        default: 'false'
    RecordId:
      name: id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/RecordId'
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
      description: >-
        Opaque cursor from next_cursor. Reuse the same credentials, endpoint, filters, ordering, and limit. Results may change between requests; pagination does not freeze the dataset.
    CreatedAfter:
      name: created_after
      in: query
      schema:
        type: string
        format: date-time
      description: >-
        Inclusive lower time bound. Applies to start_time for calls and created_at for contacts/messages. Include a time
        zone.
    CreatedBefore:
      name: created_before
      in: query
      schema:
        type: string
        format: date-time
      description: Exclusive upper time bound; must be later than created_after. Include a time zone.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: ^[!-~]+$
      description: |
        Use a stable request identifier. Contact writes replay the saved response for seven days,
        subject to current resource access. Subscription creation retains its identity for seven days
        but returns current metadata and signing secret; a disabled/deleted subscription rejects replay.
        Reusing those keys with a different operation, path or exact JSON body returns 409.
        Subscription status and delete operations are repeatable state changes.
  responses:
    Error:
      description: |
        400 invalid input, 401 expired/revoked/missing key, 403 scope or access denied, 404 missing/inaccessible,
        409 request/state conflict, 413 oversized request or response, 415 wrong content type, 429 rate limited,
        or 503 unavailable. 429 includes Retry-After. All responses include X-Request-Id.
      content:
        application/json:
          schema:
            type: object
            required:
              - error
            properties:
              error:
                type: object
                required:
                  - code
                  - message
                  - request_id
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  request_id:
                    type: string
    Contact:
      description: Contact record.
      content:
        application/json:
          schema:
            type: object
            required:
              - data
            properties:
              data:
                $ref: '#/components/schemas/Contact'
              rate_limit:
                $ref: '#/components/schemas/RateLimit'
    Subscription:
      description: Subscription metadata, without its secret.
      content:
        application/json:
          schema:
            type: object
            required:
              - data
            properties:
              data:
                $ref: '#/components/schemas/Subscription'
              rate_limit:
                $ref: '#/components/schemas/RateLimit'
  schemas:
    RecordId:
      type: string
      pattern: ^[1-9][0-9]*$
      maxLength: 19
      description: Stable existing Waveline record ID. Keep it as a string; it may exceed JavaScript's safe integer range.
    RateLimit:
      type: object
      properties:
        limit:
          type: integer
        remaining:
          type: integer
        reset_at:
          type: string
          format: date-time
    Page:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            type: object
        next_cursor:
          type:
            - string
            - 'null'
        rate_limit:
          $ref: '#/components/schemas/RateLimit'
    Connection:
      type: object
      required:
        - id
        - tenant_id
        - name
        - scopes
        - inbox_ids
        - status
        - connection_mode
        - subject_user_id
        - access_scope
        - workspace_access
      properties:
        id:
          type: string
          format: uuid
        tenant_id:
          $ref: '#/components/schemas/RecordId'
        name:
          type: string
        workspace_name:
          type: [string, 'null']
          description: The workspace's display name. Use it, not tenant_id, when labeling a connection for people.
        connection_mode:
          type: string
          enum: [member, workspace]
          description: Internal ownership field. Personal-inbox connections use member; Workspace and shared-inbox connections use workspace and are independent of the installer. This is not a creation input.
        access_scope:
          type: string
          enum: [workspace, inbox, inboxes, legacy, user]
          description: Workspace covers calls and messages across all current and future shared and personal inboxes and syncs workspace-visible contacts; inbox covers one recorded inbox and inboxes covers an explicitly selected shared-inbox set. Legacy grants retain their original restricted selections. user is a personal assistant (OAuth) connection that follows the member's live app access.
        workspace_access:
          type: boolean
          description: True only for an explicitly authorized Workspace grant. Existing grants default to false.
        subject_user_id:
          type: [string, 'null']
          format: uuid
          description: The authenticated member bound to a Personal connection; null for Workspace connections. Clients cannot select or change this subject.
        key_prefix:
          type: string
        scopes:
          type: array
          items:
            type: string
            enum:
              - contacts.read
              - contacts.write
              - calls.read
              - messages.read
              - inboxes.read
              - phone_numbers.read
              - analytics.read
              - webhooks.read
              - webhooks.write
              - messages.write
        inbox_ids:
          type: array
          description: Recorded inbox allowlist. Empty for Workspace scope; GET /v1/inboxes returns the currently authorized inboxes.
          items:
            $ref: '#/components/schemas/RecordId'
        status:
          type: string
          enum:
            - active
            - revoked
        created_at:
          type: string
          format: date-time
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        revoked_at:
          type:
            - string
            - 'null'
          format: date-time
        default_inbox_id:
          type: [string, 'null']
          description: Personal assistant (OAuth) connections only. The member's default inbox.
        timezone:
          type: [string, 'null']
          description: >-
            Personal assistant (OAuth) connections only. IANA timezone from the member's default inbox business
            hours, otherwise from another accessible inbox; null when none is configured.
        timezone_source:
          type: [string, 'null']
          enum: [default_inbox, accessible_inbox, null]
    ContactCreate:
      type: object
      additionalProperties: false
      required: [phone]
      properties:
        display_name:
          $ref: '#/components/schemas/ContactWrite/properties/display_name'
        first_name:
          $ref: '#/components/schemas/ContactWrite/properties/first_name'
        last_name:
          $ref: '#/components/schemas/ContactWrite/properties/last_name'
        company:
          $ref: '#/components/schemas/ContactWrite/properties/company'
        role:
          $ref: '#/components/schemas/ContactWrite/properties/role'
        phone:
          type: string
          pattern: '^\+[1-9][0-9]{1,14}$'
          description: Required valid E.164 phone. Skip source records without a usable phone.
        email:
          $ref: '#/components/schemas/ContactWrite/properties/email'
    ContactWrite:
      type: object
      additionalProperties: false
      properties:
        display_name:
          type: string
          minLength: 1
          maxLength: 160
          description: Optional explicit label. Otherwise derived from first/last name, company, phone, or email, falling back to Unknown Contact.
        first_name:
          type:
            - string
            - 'null'
          maxLength: 320
        last_name:
          type:
            - string
            - 'null'
          maxLength: 320
        company:
          type:
            - string
            - 'null'
          maxLength: 320
        role:
          type:
            - string
            - 'null'
          maxLength: 320
        phone:
          type:
            - string
            - 'null'
          pattern: ^\+[1-9][0-9]{1,14}$
          description: Omit to preserve the saved phone. Null promotes another valid saved phone to primary while retaining the previous number, or returns CONTACT_PHONE_REQUIRED if no other phone remains.
        email:
          type:
            - string
            - 'null'
          maxLength: 320
    ContactUpsert:
      type: object
      additionalProperties: false
      required: [external_source, external_id]
      description: A new external identity requires a nonnull valid phone. A known identity may omit phone when its contact already has a valid saved phone; null follows ContactWrite promotion rules. This depends on the saved mapping and is checked by the server.
      properties:
        external_source:
          type: string
          minLength: 1
          maxLength: 160
          description: Stable source app/account identifier, such as hubspot:portal-123. Case-sensitive; no surrounding whitespace or control characters.
        external_id:
          type: string
          minLength: 1
          maxLength: 200
          description: Source contact record ID, preserved exactly as a string. Case-sensitive; no surrounding whitespace or control characters.
        display_name:
          $ref: '#/components/schemas/ContactWrite/properties/display_name'
        first_name:
          $ref: '#/components/schemas/ContactWrite/properties/first_name'
        last_name:
          $ref: '#/components/schemas/ContactWrite/properties/last_name'
        company:
          $ref: '#/components/schemas/ContactWrite/properties/company'
        role:
          $ref: '#/components/schemas/ContactWrite/properties/role'
        phone:
          $ref: '#/components/schemas/ContactWrite/properties/phone'
        email:
          $ref: '#/components/schemas/ContactWrite/properties/email'
    Contact:
      type: object
      required:
        - id
        - display_name
        - first_name
        - last_name
        - company
        - role
        - phone
        - email
        - created_at
        - updated_at
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        display_name:
          type: string
        first_name:
          type:
            - string
            - 'null'
        last_name:
          type:
            - string
            - 'null'
        company:
          type:
            - string
            - 'null'
        role:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Call:
      type: object
      required:
        - id
        - session_call_sid
        - direction
        - start_time
        - end_time
        - duration_seconds
        - outcome
        - external_number
        - called_via_number
        - inbox_ids
        - view_scope
        - unhandled_inbox_ids
        - interactions
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        session_call_sid:
          type:
            - string
            - 'null'
        direction:
          type: string
          enum:
            - inbound
            - outbound
        start_time:
          type:
            - string
            - 'null'
          format: date-time
        end_time:
          type:
            - string
            - 'null'
          format: date-time
        duration_seconds:
          type:
            - integer
            - 'null'
          description: >-
            Workspace and legacy views use elapsed end_time minus start_time, rounded down to seconds.
            An inbox view uses the union of that inbox's completed interaction intervals: overlapping
            interactions are counted once and time in other inboxes is excluded. Neither measure is talk
            time. For inbox views this is null while any included interaction is in progress; Workspace
            and legacy duration is null until the parent call ends. Historical single-inbox calls
            without interaction facts retain their aggregate elapsed duration.
        outcome:
          type: string
          enum:
            - completed
            - voicemail
            - forwarded
            - transferred
            - missed
            - failed
            - unknown
          description: >-
            Workspace and legacy views derive this in the listed precedence from persisted interactions.
            An inbox view uses its final interaction's terminal disposition: transfers, voicemail and
            forwarding take precedence over an earlier answer. Missed includes abandoned/no-answer.
            Failed requires explicit carrier-root failure and is not inferred from another inbox.
            Unknown means insufficient facts or in progress.
        external_number:
          type:
            - string
            - 'null'
        called_via_number:
          type:
            - string
            - 'null'
          description: >-
            Waveline phone number used for the full call. Null for a partial single-inbox view of a
            multi-inbox call because the original number may belong to another inbox.
        inbox_ids:
          type: array
          items:
            $ref: '#/components/schemas/RecordId'
          description: Inboxes included in this view; an inbox view contains only its selected inbox.
        view_scope:
          type: string
          enum: [workspace, inbox, inboxes, legacy]
          description: Determines whether timing, outcome and interactions cover the whole call or one inbox.
        unhandled_inbox_ids:
          type: array
          items:
            $ref: '#/components/schemas/RecordId'
          description: >-
            Accessible inboxes where this call still needs follow-up, as in Waveline's unhandled calls. Empty once
            it is answered or returned later, cleared by a teammate, or never needed follow-up.
        interactions:
          type: array
          items:
            $ref: '#/components/schemas/CallInteraction'
          description: >-
            Permitted external interactions ordered by entry time and ID. Multiple visits to the same
            inbox remain part of this one call entry. Historical calls can have no interaction facts.
        inbox_labels:
          type: object
          additionalProperties:
            type: string
          description: Only with include_labels=true. Labels of the accessible inboxes in this record, keyed by inbox ID.
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/ContactMatch'
          description: >-
            Only with include_labels=true and contacts.read. Visible contacts saved with the external number. Several
            matches are alternatives, not additional participants.
    CallInteraction:
      type: object
      required:
        - id
        - inbox_id
        - employee_id
        - employee_name
        - entry_mode
        - exit_mode
        - entry_at
        - ringing_at
        - bridged_at
        - resolved_at
        - bridged
        - duration_seconds
        - handoff_source_inbox_id
        - handoff_target_inbox_id
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        inbox_id:
          $ref: '#/components/schemas/RecordId'
        employee_id:
          type: [string, 'null']
          pattern: '^[0-9]+$'
          description: Actual answering employee ID, when this interaction was bridged; never a ringing candidate.
        employee_name:
          type: [string, 'null']
          description: Display name of the actual answering employee, when available.
        entry_mode:
          type: string
          enum: [inbound, outbound, forward, blind_transfer, attended_transfer]
        exit_mode:
          type: [string, 'null']
          enum: [abandoned, missed, no_answer, bridged, voicemail, forwarded, blind_transfer, attended_transfer, null]
        entry_at:
          type: string
          format: date-time
        ringing_at:
          type: [string, 'null']
          format: date-time
        bridged_at:
          type: [string, 'null']
          format: date-time
        resolved_at:
          type: [string, 'null']
          format: date-time
        bridged:
          type: boolean
        duration_seconds:
          type: [integer, 'null']
          description: Elapsed entry-to-resolution time rounded down to seconds; not talk time. Null while unresolved.
        handoff_source_inbox_id:
          type: [string, 'null']
          pattern: '^[0-9]+$'
          description: Source inbox only when it belongs to the current authorized scope; otherwise null.
        handoff_target_inbox_id:
          type: [string, 'null']
          pattern: '^[0-9]+$'
          description: Target inbox only when it belongs to the current authorized scope; otherwise null.
    Message:
      type: object
      required:
        - id
        - conversation_id
        - body
        - delivery_status
        - direction
        - from
        - external_numbers
        - inbox_ids
        - created_at
        - updated_at
        - origin_connection_id
        - origin_application_name
        - attachments
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        conversation_id:
          $ref: '#/components/schemas/RecordId'
        body:
          type:
            - string
            - 'null'
        delivery_status:
          type:
            - string
            - 'null'
          description: Existing Waveline transport status; consumers must tolerate additional values.
        direction:
          type: string
          enum:
            - inbound
            - outbound
        from:
          type:
            - string
            - 'null'
        external_numbers:
          type: array
          items:
            type: string
          description: >-
            Sorted distinct external phone participants in this authorized conversation. These identify conversation
            parties, not exact delivery recipients for a transport part.
        inbox_ids:
          type: array
          items:
            $ref: '#/components/schemas/RecordId'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        origin_connection_id:
          type:
            - string
            - 'null'
          format: uuid
        origin_application_name:
          type:
            - string
            - 'null'
        attachments:
          type: array
          description: Completed attachments for this message. Storage paths and signed URLs are never included.
          items:
            $ref: '#/components/schemas/MessageAttachment'
        inbox_labels:
          type: object
          additionalProperties:
            type: string
          description: Only with include_labels=true. Labels of the accessible inboxes in this conversation, keyed by inbox ID.
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/ContactMatch'
          description: Only with include_labels=true and contacts.read. Visible contacts saved with each external number.
        sender_name:
          type: [string, 'null']
          description: Only with include_labels=true. The teammate who sent the message; null for inbound or automated messages.
    ContactMatch:
      type: object
      required: [phone, contact_id, name]
      description: >-
        A visible contact saved with an external number, at most five per number. The same check as getContact
        applies; inaccessible contacts are never returned.
      properties:
        phone:
          type: string
        contact_id:
          $ref: '#/components/schemas/RecordId'
        name:
          type: [string, 'null']
          description: Display name, else first and last name; null when no name is saved.
    Conversation:
      type: object
      required: [id, name, transport, last_message_at, external_numbers, inbox_ids, last_message, awaiting_reply, unread]
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        name:
          type: [string, 'null']
          description: Group conversation name, when set.
        transport:
          type: string
          description: internal, direct_sms or twilio_conversations_group; consumers must tolerate additional values.
        last_message_at:
          type: string
          format: date-time
        external_numbers:
          type: array
          items:
            type: string
        inbox_ids:
          type: array
          items:
            $ref: '#/components/schemas/RecordId'
        last_message:
          type: [object, 'null']
          required: [id, body_preview, direction, sender_name, sent_by_me, attachment_count, created_at]
          properties:
            id:
              $ref: '#/components/schemas/RecordId'
            body_preview:
              type: [string, 'null']
              description: First 280 characters of the body.
            direction:
              type: string
              enum: [inbound, outbound]
            sender_name:
              type: [string, 'null']
            sent_by_me:
              type: [boolean, 'null']
              description: Personal assistant connections only; null for API keys.
            attachment_count:
              type: integer
            created_at:
              type: string
              format: date-time
        awaiting_reply:
          type: [boolean, 'null']
          description: True when the external party sent the last message.
        unread:
          type: [boolean, 'null']
          description: The authenticated member's own read state; null for API keys.
        inbox_labels:
          type: object
          additionalProperties:
            type: string
          description: Only with include_labels=true.
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/ContactMatch'
          description: Only with include_labels=true and contacts.read.
    Member:
      type: object
      required: [employee_id, user_id, name, is_me, inboxes]
      properties:
        employee_id:
          $ref: '#/components/schemas/RecordId'
        user_id:
          type: string
          format: uuid
          description: The ring-group user ID.
        name:
          type: [string, 'null']
        is_me:
          type: boolean
        inboxes:
          type: array
          description: Current memberships in accessible inboxes.
          items:
            type: object
            required: [id, label, availability]
            properties:
              id:
                $ref: '#/components/schemas/RecordId'
              label:
                type: string
              availability:
                type: string
                enum: [available, unavailable]
    PhoneNumber:
      type: object
      additionalProperties: false
      required: [id, phone_number, label, description, inbox_id, is_primary, registered, registration_status, release_started_at]
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        phone_number:
          type: string
          description: Number as stored in Waveline inventory; legacy values may use local formatting.
        label: { type: string }
        description: { type: [string, 'null'] }
        inbox_id:
          type: [string, 'null']
          pattern: '^[1-9][0-9]{0,18}$'
        is_primary: { type: boolean }
        registered: { type: boolean }
        registration_status: { type: [string, 'null'] }
        release_started_at:
          type: [string, 'null']
          format: date-time
          description: Non-null while a number release is in progress. Never a release token.
    CallAnalytics:
      type: object
      required: [start_date, end_date, timezone, group_by, inbox_id, call_type, counting_unit, summary, groups]
      properties:
        start_date: { type: string, format: date-time }
        end_date: { type: string, format: date-time }
        timezone: { type: string, const: UTC }
        group_by: { type: string, enum: [day, hour, inbox, phone_number, employee] }
        inbox_id: { type: [string, 'null'] }
        call_type: { type: string, const: external }
        counting_unit: { type: string, const: inbox_interaction }
        summary:
          $ref: '#/components/schemas/CallAnalyticsMetrics'
        groups:
          type: array
          maxItems: 1000
          items:
            type: object
            required: [key, label, metrics]
            properties:
              key:
                type: [string, 'null']
                description: UTC date/hour, inbox/employee ID, or historical called-via number. Null represents unattributed interactions.
              label: { type: [string, 'null'] }
              metrics:
                $ref: '#/components/schemas/CallAnalyticsMetrics'
        comparison:
          description: Present only when compare_to is supplied.
          type: object
          required: [compare_to, start_date, end_date, summary, groups, change, percent_change]
          properties:
            compare_to: { type: string, const: previous_period }
            start_date: { type: string, format: date-time }
            end_date:
              type: string
              format: date-time
              description: Exclusive; equals the report start_date.
            summary:
              $ref: '#/components/schemas/CallAnalyticsMetrics'
            groups:
              type: array
              maxItems: 1000
              description: The previous window grouped the same way. Day and hour keys are that window's own buckets.
              items:
                type: object
                required: [key, label, metrics]
                properties:
                  key: { type: [string, 'null'] }
                  label: { type: [string, 'null'] }
                  metrics:
                    $ref: '#/components/schemas/CallAnalyticsMetrics'
            change:
              type: object
              description: >-
                Current summary minus previous summary per metric (answer_rate in percentage points).
                Null when either side has no value.
              additionalProperties: { type: [number, 'null'] }
            percent_change:
              type: object
              description: >-
                Relative change in percent per summary metric, rounded to one decimal. Null when the previous
                value is 0 or missing. answer_rate is omitted; use change for its percentage-point delta.
              additionalProperties: { type: [number, 'null'] }
    CallAnalyticsMetrics:
      type: object
      description: >-
        Counts use inbox interactions except unique_calls. Answer rate is the percentage of direct
        inbound interactions that reached ringing and were answered. Null rates and averages mean
        there are no eligible samples. Durations are seconds; totals sum visible interaction time.
      properties:
        unique_calls: { type: integer, minimum: 0 }
        interaction_count: { type: integer, minimum: 0 }
        inbound_interactions: { type: integer, minimum: 0 }
        outbound_interactions: { type: integer, minimum: 0 }
        answered_interactions: { type: integer, minimum: 0 }
        missed_interactions: { type: integer, minimum: 0 }
        abandoned_interactions: { type: integer, minimum: 0 }
        voicemail_interactions: { type: integer, minimum: 0 }
        forwarded_interactions: { type: integer, minimum: 0 }
        transferred_interactions: { type: integer, minimum: 0 }
        transfer_in_interactions: { type: integer, minimum: 0 }
        transfer_in_answered_interactions: { type: integer, minimum: 0 }
        after_hours_interactions: { type: integer, minimum: 0 }
        answer_rate: { type: [number, 'null'], minimum: 0, maximum: 100 }
        total_talk_seconds: { type: number }
        avg_talk_seconds: { type: [number, 'null'] }
        avg_time_to_first_ring_seconds: { type: [number, 'null'] }
        avg_ring_to_answer_seconds: { type: [number, 'null'] }
        avg_entry_to_answer_seconds: { type: [number, 'null'] }
        avg_time_to_abandon_seconds: { type: [number, 'null'] }
    Inbox:
      type: object
      required:
        - id
        - label
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        label:
          type: string
        inbox_type:
          type: string
          enum: [personal, shared]
        owner_employee_id:
          type: [string, 'null']
          format: uuid
        timezone:
          type: [string, 'null']
          description: IANA timezone of the inbox business hours; null when no schedule is configured.
        primary_phone_number:
          type: [string, 'null']
        is_member:
          type: [boolean, 'null']
          description: Personal assistant connections only; null for API keys.
        is_my_default:
          type: [boolean, 'null']
          description: Personal assistant connections only; null for API keys.
        my_availability:
          type: [string, 'null']
          enum: [available, unavailable, null]
          description: Personal assistant connections only; null for API keys or non-members.
    EventType:
      type: string
      enum:
        - call.completed
        - call.recording.ready
        - call.transcript.ready
        - call.summary.ready
        - message.received
        - message.delivered
        - message.failed
        - contact.created
        - contact.updated
    EventEnvelope:
      type: object
      required:
        - id
        - type
        - created_at
        - connection_id
        - data
      properties:
        origin:
          type: object
          description: Contact change provenance captured at write time. Absent on older deliveries and synthetic samples. The envelope connection_id is the destination; origin.connection_id is the writer.
          properties:
            type:
              type: string
              enum: [integration, api, employee, system]
            provider:
              type: string
              example: hubspot
            connection_id:
              type: string
              format: uuid
            application_name:
              type: string
            initial_import:
              type: boolean
        id:
          type: string
          description: Logical event ID, stable across delivery retries and replay. Sample IDs begin sample_.
        type:
          $ref: '#/components/schemas/EventType'
        created_at:
          type: string
          format: date-time
        connection_id:
          type: string
          format: uuid
        data:
          oneOf:
            - $ref: '#/components/schemas/Contact'
            - $ref: '#/components/schemas/Call'
            - $ref: '#/components/schemas/Message'
            - $ref: '#/components/schemas/RecordingEvent'
    Subscription:
      type: object
      required:
        - id
        - connection_id
        - target_url
        - event_types
        - status
      properties:
        id:
          type: string
          format: uuid
        connection_id:
          type: string
          format: uuid
        target_url:
          type: string
          format: uri
        event_types:
          type: array
          items:
            $ref: '#/components/schemas/EventType'
        status:
          type: string
          enum:
            - active
            - disabled
            - deleted
        created_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - 'null'
          format: date-time
        activated_at:
          type: string
          format: date-time
    Delivery:
      type: object
      required:
        - id
        - event_id
        - subscription_id
        - status
        - attempt_count
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        subscription_id:
          type: string
          format: uuid
        connection_id:
          type: string
          format: uuid
        event_type:
          $ref: '#/components/schemas/EventType'
        replay_of:
          type:
            - string
            - 'null'
          format: uuid
          description: Original delivery ID when this delivery is a manual replay.
        status:
          type: string
          enum:
            - pending
            - delivering
            - delivered
            - failed
            - canceled
        attempt_count:
          type: integer
        http_status:
          type:
            - integer
            - 'null'
        error_code:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        delivered_at:
          type:
            - string
            - 'null'
          format: date-time
        next_attempt_at:
          type: string
          format: date-time
    MessageFee:
      type: object
      required:
        - amount_cents
        - currency
        - basis
        - status
      properties:
        amount_cents:
          type: integer
          const: 1
        currency:
          type: string
          const: usd
        basis:
          type: string
          const: accepted_recipient_send
        status:
          type: string
          enum:
            - pending_invoice
            - invoiced
    MessageSend:
      type: object
      required:
        - message
        - message_ids
        - fee
      properties:
        message:
          $ref: '#/components/schemas/Message'
        message_ids:
          type: array
          items:
            $ref: '#/components/schemas/RecordId'
        fee:
          $ref: '#/components/schemas/MessageFee'
    Note:
      type: object
      required:
        - id
        - contact_id
        - body
        - created_at
        - updated_at
        - origin_connection_id
        - origin_application_name
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        contact_id:
          $ref: '#/components/schemas/RecordId'
        body:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - 'null'
        origin_connection_id:
          type: [string, 'null']
          format: uuid
        origin_application_name:
          type: [string, 'null']
          description: Integration name saved when this note was created; null for employee-created or legacy notes.
    ContactPropertyDefinition:
      type: object
      required:
        - id
        - key
        - label
        - data_type
        - options
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        key:
          type: string
        label:
          type: string
        data_type:
          type: string
          enum:
            - text
            - number
            - boolean
            - date
            - url
            - email
            - phone
            - address
            - multi_select
        options:
          type: object
    ContactPropertyValue:
      type: object
      required:
        - id
        - key
        - data_type
        - value
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        key:
          type: string
        data_type:
          type: string
        value: {}
        updated_at:
          type:
            - string
            - 'null'
    Recording:
      type: object
      required:
        - id
        - session_call_sid
        - recording_type
        - duration_seconds
        - recorded_at
        - created_at
        - has_transcript
        - has_summary
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        session_call_sid:
          type: string
        recording_type:
          type:
            - string
            - 'null'
        duration_seconds:
          type:
            - number
            - string
            - 'null'
        recorded_at:
          type:
            - string
            - 'null'
        created_at:
          type: string
        has_transcript:
          type: boolean
        has_summary:
          type: boolean
        sentiment:
          type: [string, 'null']
          enum: [positive, negative, neutral, mixed, null]
        sentiment_status:
          type: [string, 'null']
    RecordingEvent:
      description: >-
        Metadata for ready recording, transcript, or summary content. Includes voicemail when content
        exists. data.id is the recording ID; call_id is the parent call ID. No media URL or transcript
        text is included. Fetch content through the recording endpoints, which recheck current access.
      allOf:
        - $ref: '#/components/schemas/Recording'
        - type: object
          required: [call_id, direction, external_number, inbox_ids, has_audio]
          properties:
            call_id:
              $ref: '#/components/schemas/RecordId'
            direction:
              type: string
              enum: [inbound, outbound]
            external_number:
              type: [string, 'null']
            inbox_ids:
              type: array
              items:
                $ref: '#/components/schemas/RecordId'
            has_audio:
              type: boolean
    MediaText:
      type: object
      required:
        - id
        - text
        - status
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        text:
          type:
            - string
            - 'null'
        status:
          type:
            - string
            - 'null'
    MediaDownload:
      type: object
      required:
        - download_url
        - expires_at
      properties:
        download_url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
    MessageAttachment:
      type: object
      required:
        - id
        - file_name
        - mime_type
        - size_bytes
        - created_at
      properties:
        id:
          $ref: '#/components/schemas/RecordId'
        file_name:
          type:
            - string
            - 'null'
        mime_type:
          type:
            - string
            - 'null'
        size_bytes:
          type:
            - integer
            - 'null'
        created_at:
          type: string
    MessageUpload:
      type: object
      required:
        - id
        - inbox_id
        - file_name
        - mime_type
        - size_bytes
        - attachment_id
        - expires_at
        - status
        - upload_path
      properties:
        id:
          type: string
          format: uuid
        inbox_id:
          $ref: '#/components/schemas/RecordId'
        file_name:
          type: string
        mime_type:
          type: string
        size_bytes:
          type: integer
        attachment_id:
          type:
            - string
            - 'null'
        expires_at:
          type: string
        status:
          type: string
          enum:
            - awaiting_upload
            - ready
        upload_path:
          type: string
