openapi: 3.0.3
info:
  title: Vocatech API
  version: 1.0.0
  description: |
    The Vocatech API provides access to detailed call and message reports, contact management, real-time webhook subscriptions, and messaging.

    ### Notes
    - Call **durations** are measured in **seconds**.
    - All dates are based on **America/New_York** (Eastern Time) by default.
    - If **start_date**/**end_date** are omitted, today's date in Eastern Time is used (00:00:00 → 23:59:59).
    - Use the **timezone** parameter to override (e.g., `America/Chicago`).
    - All endpoints require authentication via **Bearer Token (JWT)**.
    - Test webhooks interactively at [https://api.vocatech.com/test](https://api.vocatech.com/test).

servers:
  - url: https://api.vocatech.com/v1
    description: Vocatech Production API

tags:
  - name: Calls
    description: Call history and reporting.
  - name: Messages
    description: Send messages and view message history.
  - name: Media
    description: Authenticated access to SMS/MMS attachments, WhatsApp media, and call recordings.
  - name: Contacts
    description: Manage integration contacts — list, create/update (upsert), and delete.
  - name: Webhooks
    description: Real-time event subscriptions for call and message events.

paths:
  /messages:
    post:
      tags: [Messages]
      summary: Send a new message
      description: |
        Sends a new SMS message. The messaging mode is auto-detected from the number's configuration:

        **Webex mode** (`messaging_service = 'webex'`): Creates a Webex Space, adds members, and sends the initial message through Webex. The Webex webhook then delivers the SMS via Bandwidth. Requires the `members` field.

        **Email mode** (`messaging_service = 'email'`): Sends the SMS directly via Bandwidth and sends an email to the number's registered email address to establish the message thread. The `members` field is not required — the email recipient is determined by the number's configuration.

        If a Webex message thread between the numbers already exists, the message is sent into the existing space (returns 200 with `existing_space: true`).
        Messages are limited to 1600 characters.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [platform, from, to, message]
              properties:
                platform:
                  type: string
                  enum: [text]
                  description: "The messaging platform to use. Only SMS (text) is supported."
                from:
                  type: string
                  description: "The sender phone number (must belong to the authenticated company)."
                  example: "1234567890"
                to:
                  type: string
                  description: "The recipient phone number."
                  example: "0987654321"
                message:
                  type: string
                  maxLength: 1600
                  description: "The initial text message to send (max 1600 characters)."
                  example: "Hello, this is a new message."
                name:
                  type: string
                  description: "Optional name of the contact. Defaults to 'Vocatech Text'."
                  example: "John Doe"
                members:
                  type: array
                  items:
                    type: string
                    format: email
                  description: "Array of user emails to add to the Webex Space. Optional for Webex mode — if omitted, all authorized members for the number are auto-loaded. Ignored for email mode."
                  example: ["user1@example.com", "user2@example.com"]
                test:
                  type: boolean
                  default: false
                  description: "When true, validates the request without sending any messages or creating resources. Returns 200 with validation result instead of 201."
      responses:
        '200':
          description: "Returned in two cases: (1) Dry-run validation (when `test` is true) — no messages sent, no resources created. (2) Message sent into an existing Webex space — returns `existing_space: true` with the room UUID."
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: "test"
                  message:
                    type: object
                    properties:
                      mode:
                        type: string
                        enum: [webex, email]
                      platform:
                        type: string
                      from:
                        type: string
                      to:
                        type: string
                      valid:
                        type: boolean
                        example: true
                      members:
                        type: array
                        items:
                          type: string
                        description: "Webex mode only. Shows auto-loaded members if none were provided."
                      name:
                        type: string
                        description: "Email mode only."
                      email_recipient:
                        type: string
                        description: "Email mode only."
        '201':
          description: Message sent successfully. Response varies by mode.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: "created"
                  message:
                    type: object
                    properties:
                      mode:
                        type: string
                        enum: [webex, email]
                        description: "The messaging mode used (auto-detected from number config)."
                      platform:
                        type: string
                      from:
                        type: string
                      to:
                        type: string
                      name:
                        type: string
                      room_title:
                        type: string
                        description: "Webex mode only."
                      room_uuid:
                        type: string
                        description: "Webex mode only."
                      members:
                        type: array
                        items:
                          type: string
                        description: "Webex mode only."
                      email_recipient:
                        type: string
                        description: "Email mode only. The email address that received the message thread."
                      message_sent:
                        type: boolean
                      email_sent:
                        type: boolean
                        description: "Email mode only. Whether the thread-establishing email was sent."
                      created_at:
                        type: string
                        format: date-time
        '400':
          description: Bad Request (missing or invalid fields).
        '401':
          description: Unauthorized.
        '403':
          description: Forbidden (invalid from number, unauthorized platform, or unauthorized members).
        '429':
          description: Rate limit exceeded.
        '500':
          description: Server error.
    get:
      tags: [Messages]
      summary: Fetch message reports
      description: |
        Returns text and WhatsApp message records for the specified company and date range.

        - Combines **text (sms_log)** and **WhatsApp (messages)** data.
        - Supports filtering by **direction**, **status**, and **channel**.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: start_date
          required: false
          description: Start date (YYYY-MM-DD). Defaults to today.
          schema:
            type: string
            format: date
          example: "2026-04-21"

        - in: query
          name: end_date
          required: false
          description: End date (YYYY-MM-DD). Defaults to today.
          schema:
            type: string
            format: date
          example: "2026-04-21"

        - in: query
          name: direction
          required: false
          description: Message direction.
          schema:
            type: string
            enum: [incoming, outgoing, any]
            default: any

        - in: query
          name: status
          required: false
          description: Message delivery status.
          schema:
            type: string
            enum: [delivered, failed, read, any]
            default: any

        - in: query
          name: channel
          required: false
          description: Filter by message channel.
          schema:
            type: string
            enum: [text, whatsapp, any]
            default: any

        - in: query
          name: search
          required: false
          description: Keyword search applied to numbers.
          schema:
            type: string

        - in: query
          name: page
          required: false
          schema:
            type: integer
            minimum: 1

        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500

      responses:
        '200':
          description: Successful response containing query echo, messages array, and meta information.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: object
                    properties:
                      company_id:
                        type: integer
                      start_date:
                        type: string
                        format: date
                      end_date:
                        type: string
                        format: date
                      direction:
                        type: string
                        enum: [inbound, outbound, any]
                      status:
                        type: string
                        enum: [delivered, failed, read, any]
                      channel:
                        type: string
                        enum: [text, whatsapp, any]
                      search:
                        type: string
                      timezone:
                        type: string
                    required:
                      [company_id, start_date, end_date, direction, status, channel, search, timezone]
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        message_id:
                          type: string
                        direction:
                          type: string
                          enum: [inbound, outbound]
                        status:
                          type: string
                          nullable: true
                        remote_name:
                          type: string
                          nullable: true
                        remote_number:
                          type: string
                        group_number:
                          type: string
                        sent_time:
                          type: string
                          format: date-time
                        channel:
                          type: string
                          enum: [text, whatsapp]
                        type:
                          type: string
                          enum: [text, media]
                        body:
                          type: string
                          nullable: true
                        attachments:
                          type: array
                          description: "Media attachments (MMS images, WhatsApp media). Empty array if text-only."
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                                description: "Attachment ID (use with GET /media/{id})"
                                example: "att_123"
                              content_type:
                                type: string
                                description: "MIME type"
                                example: "image/jpeg"
                              filename:
                                type: string
                                description: "Original or generated filename"
                                example: "photo1.jpg"
                              size:
                                type: integer
                                nullable: true
                                description: "File size in bytes"
                                example: 245760
                      required:
                        [message_id, direction, remote_number, group_number, sent_time, channel, type]
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total_pages:
                        type: integer
                      total_messages:
                        type: integer
                    required: [page, limit, total_pages, total_messages]
              examples:
                sample:
                  value:
                    query:
                      company_id: 2223256392
                      start_date: "2025-10-10"
                      end_date: "2025-10-11"
                      direction: "any"
                      status: "any"
                      channel: "any"
                      search: ""
                      timezone: "America/New_York"
                    messages:
                      - message_id: "1760150514087tqaibnofcvhja"
                        direction: "inbound"
                        status: "delivered"
                        remote_name: null
                        remote_number: "8667340007"
                        group_number: "8622007078"
                        sent_time: "2025-10-11T02:41:54Z"
                        channel: "text"
                        type: "media"
                        body: "Check out this photo!"
                        attachments:
                          - id: "att_456"
                            content_type: "image/jpeg"
                            filename: "photo1.jpg"
                            size: 245760
                      - message_id: "17601494650416iqi44ewy5ql3"
                        direction: "inbound"
                        status: "delivered"
                        remote_name: null
                        remote_number: "8457064292"
                        group_number: "8632250207"
                        sent_time: "2025-10-11T02:25:45Z"
                        channel: "whatsapp"
                        type: "text"
                        body: "I'm so sorry for replying so late."
                        attachments: []
                    meta:
                      page: 1
                      limit: 2
                      total_pages: 10
                      total_messages: 901

  /media/{id}:
    get:
      tags: [Media]
      summary: Get media (attachments and recordings)
      description: |
        Unified media endpoint for SMS/MMS attachments, WhatsApp media, and call recordings.
        The ID prefix determines the media type:

        | Prefix | Type | Example | Source |
        |--------|------|---------|--------|
        | `att_` | Message attachment | `att_123` | SMS/MMS images, WhatsApp photos, documents, voice notes |
        | `rec_` | Call recording | `rec_84729` | Call recording audio from call journey segments |

        **Attachments** (`att_` prefix): Returns metadata and a direct download URL. Attachment IDs appear in `GET /messages` results and `message.received`/`message.sent` webhook payloads.

        **Recordings** (`rec_` prefix): Returns a time-limited (30-minute) signed URL for downloading the MP3 audio file. Recording segment IDs appear in the `recording_url` field of `GET /calls` journey segments.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          description: "Media ID with prefix — `att_123` for message attachments, `rec_84729` for call recordings."
          schema:
            type: string
          examples:
            attachment:
              summary: Message attachment
              value: "att_123"
            recording:
              summary: Call recording
              value: "rec_84729"
      responses:
        '200':
          description: Media metadata and download URL.
          content:
            application/json:
              examples:
                attachment:
                  summary: Message attachment response
                  value:
                    id: "att_123"
                    type: "attachment"
                    source: "whatsapp"
                    content_type: "image/jpeg"
                    filename: "photo1.jpg"
                    size: 245760
                    url: "https://whatsapp.vocatech.com/media/wa_vonage_abc123.jpg"
                recording:
                  summary: Call recording response
                  value:
                    id: "rec_84729"
                    type: "recording"
                    content_type: "audio/mp3"
                    format: "mp3"
                    duration: 185
                    url: "https://storage.googleapis.com/vocatech_call_recordings/...signed..."
                    expires_at: "2026-03-25T19:30:00.000Z"
        '404':
          description: Media not found or not owned by your company.
        '410':
          description: Media file no longer available (expired or deleted).

  /contacts/fields:
    get:
      tags: [Contacts]
      summary: List contact fields
      description: |
        Returns the company's contact field definitions (schema). Use this to understand which fields are available
        before pushing or searching contacts.

        Each field has flags:
        - **is_phone** — values are auto-formatted as phone numbers (digits only, US 11→10 digit handling). Phone fields support multiple numbers separated by semicolons (e.g., `"5551234567;5559876543"`).
        - **is_match** — used for dedup matching when creating/updating contacts (AND logic across all match fields)
        - **is_integration** — if true, this field is a clickable integration connector (e.g., Portal, QuickBooks) with associated initials, URLs, and icon data
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of field definitions for the company.
          content:
            application/json:
              schema:
                type: object
                properties:
                  fields:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        order:
                          type: integer
                        is_phone:
                          type: boolean
                          description: "If true, values are auto-formatted as phone numbers."
                        is_match:
                          type: boolean
                          description: "If true, used for dedup matching on upsert."
                        reports_enabled:
                          type: boolean
                        hide_no_data:
                          type: boolean
                        show_in_fields:
                          type: boolean
                        is_integration:
                          type: boolean
                          description: "If true, this field is a clickable integration connector."
                        integration:
                          type: object
                          nullable: true
                          description: "Integration details (only present for integration fields)."
                          properties:
                            id:
                              type: integer
                            initials:
                              type: string
                              description: "Two-character icon text (e.g., 'AP' for Admin Portal)."
                            image_url:
                              type: string
                            fields:
                              type: array
                              description: "Integration configuration fields (URLs, templates, etc.)."
                              items:
                                type: object
                                properties:
                                  key:
                                    type: string
                                  display_name:
                                    type: string
                                  value:
                                    type: string
                                  fills:
                                    type: array
                                    items:
                                      type: object
                                      properties:
                                        contact_field:
                                          type: string
                                        placeholder:
                                          type: string
              examples:
                sample:
                  value:
                    fields:
                      - id: 12
                        name: "Company"
                        order: 0
                        is_phone: false
                        is_match: true
                        reports_enabled: true
                        hide_no_data: false
                        show_in_fields: true
                        is_integration: false
                      - id: 18
                        name: "Phone"
                        order: 4
                        is_phone: true
                        is_match: false
                        reports_enabled: true
                        hide_no_data: true
                        show_in_fields: true
                        is_integration: false
                      - id: 20
                        name: "Portal"
                        order: 5
                        is_phone: false
                        is_match: false
                        reports_enabled: true
                        hide_no_data: false
                        show_in_fields: true
                        is_integration: true
                        integration:
                          id: 1
                          initials: "AP"
                          image_url: ""
                          fields:
                            - key: "contact_url"
                              display_name: "Contact URL"
                              value: "https://admin.vocatech.com/company/?company_id={CONTACT_ID}"
                              fills:
                                - contact_field: "id"
                                  placeholder: "{CONTACT_ID}"
                            - key: "default_url"
                              display_name: "Default URL"
                              value: "https://admin.vocatech.com"
                              fills: []

  /contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      description: |
        Returns a paginated list of contacts for the company. Each contact includes all field values keyed by field name.

        Use the `search` parameter for a general text search across all field values (LIKE match).
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: page
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
        - in: query
          name: search
          required: false
          description: "Search term — matches against any field value (partial match)."
          schema:
            type: string
      responses:
        '200':
          description: Paginated list of contacts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: object
                    properties:
                      company_id:
                        type: integer
                      search:
                        type: string
                      page:
                        type: integer
                      limit:
                        type: integer
                  contacts:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        fields:
                          type: object
                          additionalProperties:
                            type: string
                          description: "Key-value pairs where keys are field names and values are the contact's data."
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total_pages:
                        type: integer
                      total_contacts:
                        type: integer
              examples:
                sample:
                  value:
                    query:
                      page: 1
                      limit: 100
                      search: null
                    contacts:
                      - id: 181119
                        fields:
                          Company: "ABC Mortgage"
                          Contact: "John Smith"
                          Email: "john@abcmortgage.com"
                          Phone: "2125551234;2125559999;2125550100"
                          Portal: "9105222634"
                      - id: 181120
                        fields:
                          Company: "XYZ Corp"
                          Contact: "Jane Doe"
                          Email: "jane@xyzcorp.com"
                          Phone: "7185558888"
                          Portal: "9105333745"
                    meta:
                      page: 1
                      limit: 100
                      total_pages: 19
                      total_contacts: 1858
    post:
      tags: [Contacts]
      summary: Create or update contacts
      description: |
        Creates or updates contacts using upsert logic. Supports both single and batch operations.

        **Single contact:**
        ```json
        { "fields": { "First Name": "Jane", "Phone": "9175559999" } }
        ```

        **Batch (up to 500):**
        ```json
        {
          "contacts": [
            { "fields": { "First Name": "Jane", "Phone": "9175559999" } },
            { "fields": { "First Name": "Bob", "Phone": "9175558888" } }
          ]
        }
        ```

        ### Upsert Logic
        - Phone fields (where `is_phone = true`) are auto-formatted: stripped to digits, US 11-digit numbers have leading 1 removed.
        - **Multi-number phone fields**: Phone fields support multiple numbers separated by semicolons (e.g., `"5551234567;5559876543;5554441111"`). Each number is formatted individually. Duplicates within the same field are removed.
        - Integration fields (like Portal) accept a value that becomes the integration connector's reference ID (e.g., a company_id for URL generation).
        - If all `is_match` fields match an existing contact (AND logic), the existing contact is **updated** instead of creating a duplicate.
        - At least one non-empty match field value must actually match to trigger an update.
        - If no match is found, a new contact is **created**.

        Use `GET /contacts/fields` first to see which fields are available and which are match/phone fields.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  description: "Single contact"
                  required: [fields]
                  properties:
                    fields:
                      type: object
                      additionalProperties:
                        type: string
                      description: "Field name → value pairs."
                - type: object
                  description: "Batch of contacts (max 500)"
                  required: [contacts]
                  properties:
                    contacts:
                      type: array
                      maxItems: 500
                      items:
                        type: object
                        required: [fields]
                        properties:
                          fields:
                            type: object
                            additionalProperties:
                              type: string
            examples:
              single:
                summary: Single contact
                value:
                  fields:
                    Company: "ABC Mortgage"
                    Contact: "John Smith"
                    Phone: "2125551234;2125559999;2125550100"
                    Portal: "9105222634"
              batch:
                summary: Batch of contacts
                value:
                  contacts:
                    - fields:
                        Company: "ABC Mortgage"
                        Phone: "2125551234;2125559999"
                        Portal: "9105222634"
                    - fields:
                        Company: "XYZ Corp"
                        Phone: "7185558888"
                        Portal: "9105333745"
      responses:
        '200':
          description: Contacts created or updated successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      total:
                        type: integer
                      created:
                        type: integer
                      updated:
                        type: integer
                      errors:
                        type: integer
                  contacts:
                    type: array
                    description: "Successfully processed contacts."
                    items:
                      type: object
                      properties:
                        contact:
                          type: object
                          properties:
                            id:
                              type: integer
                            fields:
                              type: object
                              additionalProperties:
                                type: string
                        action:
                          type: string
                          enum: [created, updated]
                  errors:
                    type: array
                    description: "Errors for contacts that failed."
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                        message:
                          type: string
              examples:
                single_created:
                  summary: Single contact created
                  value:
                    summary:
                      total: 1
                      created: 1
                      updated: 0
                      errors: 0
                    contacts:
                      - contact:
                          id: 12347
                          fields:
                            Company: "ABC Mortgage"
                            Contact: "John Smith"
                            Phone: "2125551234;2125559999;2125550100"
                            Portal: "9105222634"
                        action: "created"
                    errors: []
                batch_mixed:
                  summary: Batch with mixed results
                  value:
                    summary:
                      total: 2
                      created: 1
                      updated: 1
                      errors: 0
                    contacts:
                      - contact:
                          id: 12347
                          fields:
                            Company: "ABC Mortgage"
                            Phone: "2125551234;2125559999"
                            Portal: "9105222634"
                        action: "created"
                      - contact:
                          id: 12345
                          fields:
                            Company: "XYZ Corp"
                            Phone: "7185558888"
                            Portal: "9105333745"
                        action: "updated"
                    errors: []
        '400':
          description: "Bad request — missing `fields` key or batch exceeds 500."
        '401':
          description: Unauthorized.
        '429':
          description: Rate limit exceeded.
    delete:
      tags: [Contacts]
      summary: Delete contacts
      description: |
        Deletes one or more contacts by ID. Supports single and batch operations.

        **Single contact:**
        ```json
        { "id": 12345 }
        ```

        **Batch (up to 500):**
        ```json
        { "ids": [12345, 12346, 12347] }
        ```

        Only contacts belonging to the authenticated company can be deleted. Contacts not found or not owned are reported as errors.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  description: "Single contact"
                  required: [id]
                  properties:
                    id:
                      type: integer
                - type: object
                  description: "Batch delete (max 500)"
                  required: [ids]
                  properties:
                    ids:
                      type: array
                      maxItems: 500
                      items:
                        type: integer
            examples:
              single:
                summary: Delete one contact
                value:
                  id: 12345
              batch:
                summary: Delete multiple contacts
                value:
                  ids: [12345, 12346, 12347]
      responses:
        '200':
          description: Delete results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      total:
                        type: integer
                      deleted:
                        type: integer
                      errors:
                        type: integer
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        message:
                          type: string
              examples:
                success:
                  value:
                    summary:
                      total: 3
                      deleted: 3
                      errors: 0
                    errors: []
                partial:
                  value:
                    summary:
                      total: 3
                      deleted: 2
                      errors: 1
                    errors:
                      - id: 99999
                        message: "Contact not found"
        '400':
          description: "Bad request — missing `id` or `ids`, or batch exceeds 500."
        '401':
          description: Unauthorized.
        '429':
          description: Rate limit exceeded.

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Array of webhook endpoints (secret_key never included).
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    company_id:
                      type: integer
                    name:
                      type: string
                      nullable: true
                    description:
                      type: string
                      nullable: true
                    url:
                      type: string
                      format: uri
                    event_filters:
                      type: array
                      items:
                        type: string
                        enum: [call.started, call.answered, call.ended, call.transcription, message.sent, message.received, message.status_updated]
                    extension_filters:
                      type: array
                      items:
                        type: string
                    enabled:
                      type: boolean
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
    post:
      tags: [Webhooks]
      summary: Create a webhook endpoint
      description: |
        Creates a new webhook endpoint for the authenticated company.

        ### Supported Event Types

        | Public type | When it fires |
        |---|---|
        | `call.started` | A call leg begins (dialled or received) |
        | `call.answered` | A call leg is answered |
        | `call.ended` | A call leg finishes |
        | `call.transcription` | AI processing completes (approx 5-30m after call finishes) |
        | `message.sent` | An outgoing SMS or WhatsApp message is sent |
        | `message.received` | An inbound SMS or WhatsApp message arrives |
        | `message.status_updated` | Delivery status changes (sent → delivered → read) |

        Subscribe to one or more of these types via `event_filters`.

        **Note:** Message webhooks dispatched via the polling scheduler may have up to ~60 seconds latency. Inbound message events are delivered in real-time.

        ### Example Request Body Payload
        ```json
        {
          "name": "My CRM Webhook",
          "url": "https://yourapp.com/webhook",
          "event_filters": ["call.ended", "message.received", "message.sent"]
        }
        ```

        ### Security Signatures
        Every delivery includes an `X-Vocatech-Signature` header:
          `X-Vocatech-Signature: t=<unix_timestamp>,v1=<HMAC-SHA256>`
        Verify with: `hash_hmac('sha256', "t={t}.{raw_body}", secret)`
        Reject if `abs(time() - t) > 300` (replay protection).

        ### Webhook Delivery Payload
        When an event fires, your endpoint receives a JSON POST with this structure:

        **Call event example (`call.ended`):**
        ```json
        {
          "id": "evt_67890abcdef",
          "event_type": "call.ended",
          "timestamp": "2026-03-10T14:30:45+00:00",
          "company_id": 42,
          "data": {
            "call_id": "c045ac31-abbe-45dc-9722-efbe4044cc73",
            "direction": "incoming",
            "extension": "101",
            "extension_name": "John Doe",
            "remote_name": "Jane Smith",
            "remote_number": "5551234567",
            "group_number": "5559876543",
            "start_time": "2026-03-10T14:30:00.000Z",
            "end_time": "2026-03-10T14:31:10.000Z",
            "duration": 70
          }
        }
        ```
        For `call.transcription` events, the `data` object also includes:
        - `summary` — AI-generated call summary
        - `transcription` — Full transcription text

        **Message event example (`message.received` with MMS attachment):**
        ```json
        {
          "id": "evt_msg_abc12345",
          "event_type": "message.received",
          "timestamp": "2026-03-15T14:30:45+00:00",
          "company_id": 42,
          "data": {
            "message_id": "sms_12345",
            "direction": "incoming",
            "channel": "text",
            "from": "+14155551234",
            "to": "+17185550000",
            "body": "Check out this photo!",
            "status": "delivered",
            "sent_at": "2026-03-15T14:30:45.000Z",
            "attachments": [
              {
                "url": "https://api.vocatech.com/v1/media/att_456",
                "content_type": "image/jpeg",
                "filename": "photo1.jpg",
                "size": 245760
              }
            ]
          }
        }
        ```
        The `attachments` array is always present. Empty array `[]` for text-only messages. Each attachment includes a secure download URL accessible with your API key.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, event_filters]
              properties:
                name:
                  type: string
                  example: My Production Webhook
                description:
                  type: string
                url:
                  type: string
                  format: uri
                  example: https://myapp.example.com/webhook
                event_filters:
                  type: array
                  items:
                    type: string
                    enum: [call.started, call.answered, call.ended, call.transcription, message.sent, message.received, message.status_updated]
                  example: [call.started, call.ended]
                extension_filters:
                  type: array
                  items:
                    type: string
                  example: ['100', '101']
      responses:
        '201':
          description: Endpoint created. `secret_key` is included in **this response only** — store it now.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  company_id:
                    type: integer
                  name:
                    type: string
                    nullable: true
                  description:
                    type: string
                    nullable: true
                  url:
                    type: string
                    format: uri
                  event_filters:
                    type: array
                    items:
                      type: string
                      enum: [call.started, call.answered, call.ended, call.transcription, message.sent, message.received, message.status_updated]
                  extension_filters:
                    type: array
                    items:
                      type: string
                  enabled:
                    type: boolean
                  secret_key:
                    type: string
                    description: "HMAC signing secret — only returned on creation. Store it securely."
                    example: a1b2c3d4e5f6...
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '422':
          description: Validation error (invalid URL or event type).

  /webhooks/{id}:
    get:
      tags: [Webhooks]
      summary: View a webhook endpoint
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Webhook endpoint (no secret_key).
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  company_id:
                    type: integer
                  name:
                    type: string
                    nullable: true
                  description:
                    type: string
                    nullable: true
                  url:
                    type: string
                    format: uri
                  event_filters:
                    type: array
                    items:
                      type: string
                      enum: [call.started, call.answered, call.ended, call.transcription, message.sent, message.received, message.status_updated]
                  extension_filters:
                    type: array
                    items:
                      type: string
                  enabled:
                    type: boolean
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '404':
          description: Not found (or belongs to a different company).
    patch:
      tags: [Webhooks]
      summary: Update a webhook endpoint
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                url:
                  type: string
                  format: uri
                event_filters:
                  type: array
                  items:
                    type: string
                    enum: [call.started, call.answered, call.ended, call.transcription, message.sent, message.received, message.status_updated]
                extension_filters:
                  type: array
                  items:
                    type: string
                enabled:
                  type: boolean
      responses:
        '200':
          description: Updated endpoint (no secret_key).
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  company_id:
                    type: integer
                  name:
                    type: string
                    nullable: true
                  description:
                    type: string
                    nullable: true
                  url:
                    type: string
                    format: uri
                  event_filters:
                    type: array
                    items:
                      type: string
                      enum: [call.started, call.answered, call.ended, call.transcription, message.sent, message.received, message.status_updated]
                  extension_filters:
                    type: array
                    items:
                      type: string
                  enabled:
                    type: boolean
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '404':
          description: Not found.
        '422':
          description: Validation error.
    delete:
      tags: [Webhooks]
      summary: Delete a webhook endpoint
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted successfully.
        '404':
          description: Not found.

  /webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Send a test delivery
      description: |
        Sends a `webhook.test` event to the endpoint's URL with a valid signature.
        Use this to verify your endpoint is reachable and correctly validates signatures.
        The test delivery uses a 10-second timeout.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Test delivery result. Returns success status regardless of whether the delivery itself succeeded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    description: "Whether the test delivery was received successfully by your endpoint."
                  status_code:
                    type: integer
                    nullable: true
                    description: "HTTP status code returned by your endpoint, or null if the request failed."
                    example: 200
                  error:
                    type: string
                    nullable: true
                    description: "Error message if the delivery failed, or null on success."
        '404':
          description: Webhook not found (or belongs to a different company).

  /webhooks/failures:
    get:
      tags: [Webhooks]
      summary: List failed deliveries
      description: Returns a paginated list of webhook deliveries that failed. Failed jobs are retried automatically with exponential backoff (1m, 5m, 30m, 2h) up to 5 attempts.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
            default: 1
      responses:
        '200':
          description: Paginated list of failed delivery jobs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  failures:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        endpoint_id:
                          type: integer
                        company_id:
                          type: integer
                        endpoint_url:
                          type: string
                        event_type:
                          type: string
                        payload:
                          type: string
                          description: JSON-encoded webhook payload that failed to deliver.
                        error_message:
                          type: string
                          nullable: true
                        attempts:
                          type: integer
                        retried_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total:
                        type: integer
                      total_pages:
                        type: integer

  /calls:
    get:
      tags: [Calls]
      summary: Fetch call reports
      description: |
        Returns call records for the specified company and date range.
        Each record follows the **Call** schema and includes its **journey** (segments) in chronological order.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: start_date
          required: false
          description: Start date (YYYY-MM-DD). Defaults to today at 00:00:00.
          schema:
            type: string
            format: date
          example: "2026-04-21"

        - in: query
          name: end_date
          required: false
          description: End date (YYYY-MM-DD). Defaults to today at 23:59:59.
          schema:
            type: string
            format: date
          example: "2026-04-21"

        - in: query
          name: direction
          required: false
          schema:
            type: string
            enum: [incoming, outgoing, any]
            default: any

        - in: query
          name: status
          required: false
          schema:
            type: string
            enum: [answered, missed, any]
            default: any

        - in: query
          name: search
          required: false
          description: Keyword search applied to **call id**, **name**, or **extension**.
          schema:
            type: string

        - in: query
          name: page
          required: false
          schema:
            type: integer
            minimum: 1

        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500

      responses:
        '200':
          description: Successful response containing query echo, calls array, and meta information.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: object
                    properties:
                      company_id:
                        type: integer
                      start_date:
                        type: string
                        format: date
                      end_date:
                        type: string
                        format: date
                      direction:
                        type: string
                        enum: [incoming, outgoing, any]
                      status:
                        type: string
                        enum: [answered, missed, any]
                      search:
                        type: string
                    required: [company_id, start_date, end_date, direction, status, search]
                  calls:
                    type: array
                    items:
                      type: object
                      properties:
                        call_id:
                          type: string
                        direction:
                          type: string
                          enum: [incoming, outgoing]
                        status:
                          type: string
                          enum: [answered, missed, voicemail, abandoned, busy, failed]
                        remote_name:
                          type: string
                          nullable: true
                        remote_number:
                          type: string
                        group_number:
                          type: string
                        start_time:
                          type: string
                          format: date-time
                        end_time:
                          type: string
                          format: date-time
                        duration:
                          type: integer
                          description: Duration in seconds
                        journey:
                          type: array
                          items:
                            type: object
                            properties:
                              order:
                                type: integer
                              type:
                                type: string
                                enum: [auto_attendant, hunt_group, call_center, user]
                              name:
                                type: string
                              extension:
                                type: string
                                nullable: true
                              start_time:
                                type: string
                                format: date-time
                              end_time:
                                type: string
                                format: date-time
                              duration:
                                type: integer
                                description: Duration in seconds
                              summary:
                                type: string
                                nullable: true
                              transcription:
                                type: string
                                nullable: true
                              recording_url:
                                type: string
                                nullable: true
                                description: URL to download the call recording. Use GET on this URL with your API key to receive a signed download link.
                                example: "https://api.vocatech.com/v1/media/rec_84729"
                            required: [order, type, name, start_time, end_time, duration]
                      required:
                        [call_id, direction, status, remote_number, group_number, start_time, end_time, duration, journey]
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total_pages:
                        type: integer
                      total_calls:
                        type: integer
                    required: [page, limit, total_pages, total_calls]
              examples:
                sample:
                  value:
                    query:
                      company_id: 2223256392
                      start_date: "2025-10-10"
                      end_date: "2025-10-10"
                      direction: "any"
                      status: "any"
                      search: ""
                    calls:
                      - call_id: "c045ac31-abbe-45dc-9722-efbe4044cc73"
                        direction: "incoming"
                        status: "missed"
                        remote_name: "Bound Brook Nj"
                        remote_number: "7322096381"
                        group_number: "7328008500"
                        start_time: "2025-10-10T18:46:30.000Z"
                        end_time: "2025-10-10T18:46:30.000Z"
                        duration: 0
                        journey:
                          - order: 1
                            type: "hunt_group"
                            name: "Js"
                            extension: "206"
                            start_time: "2025-10-10T18:46:30.000Z"
                            end_time: "2025-10-10T18:46:30.000Z"
                            duration: 0
                            summary: null
                            transcription: null
                            recording_url: null
                    meta:
                      page: 1
                      limit: 100
                      total_pages: 1
                      total_calls: 16

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
