openapi: 3.1.0
info:
  title: DialStack Webhook Protocol
  version: 1.0.0
  description: |
    Reference documentation for the webhook protocol that platforms implement to receive
    appointment requests from DialStack.

    ## Overview

    When DialStack's voice AI searches for availability or creates a booking, DialStack
    sends webhook requests to your platform's configured `webhook_url`. Your platform
    processes the request and returns a response that DialStack relays back to the voice AI.

    ## Configuration

    Configure webhooks on your platform record:
    - **webhook_url**: Base URL for webhook endpoints (e.g., `https://api.yourplatform.com/dialstack`)
    - **webhook_secret**: Shared secret for signature verification

    DialStack appends the endpoint path to your base URL:
    - `{webhook_url}/customers/lookup`
    - `{webhook_url}/availability/search`
    - `{webhook_url}/bookings`

    ## Request Headers

    All webhook requests include these headers:

    | Header | Description |
    |--------|-------------|
    | `Content-Type` | Always `application/json` |
    | `X-DialStack-Signature` | HMAC-SHA256 signature for verification |
    | `X-DialStack-Account-Id` | The account ID for this request |

    ## Signature Verification

    The `X-DialStack-Signature` header contains a timestamp and signature:

    ```
    X-DialStack-Signature: t=1697634600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
    ```

    To verify the signature:

    1. Extract the timestamp (`t`) and signature (`v1`) from the header
    2. Concatenate: `{timestamp}.{request_body}`
    3. Compute: `HMAC-SHA256(webhook_secret, concatenated_string)`
    4. Compare the computed signature with `v1` (use timing-safe comparison)
    5. Reject requests where the timestamp is older than 5 minutes

    ## Timeouts

    - DialStack waits up to **30 seconds** for your webhook response
    - Design your endpoints to respond quickly; defer heavy processing if needed

  contact:
    name: DialStack Support
    email: support@dialstack.ai
    url: https://docs.dialstack.ai

servers:
  - url: https://api.yourplatform.com/dialstack
    description: Your platform's webhook base URL

components:
  securitySchemes:
    SignatureAuth:
      type: apiKey
      in: header
      name: X-DialStack-Signature
      description: |
        HMAC-SHA256 signature in format: `t=<timestamp>,v1=<signature>`

  parameters:
    AccountIdHeader:
      name: X-DialStack-Account-Id
      in: header
      required: true
      description: The DialStack account ID for this request
      schema:
        type: string
      example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'

  schemas:
    # Customer Lookup
    CustomerLookupWebhook:
      type: object
      required:
        - account_id
        - customer
      properties:
        account_id:
          type: string
          description: The account ID (same as X-DialStack-Account-Id header)
          example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
        customer:
          type: object
          required:
            - phone
          properties:
            phone:
              type: string
              description: Caller's phone number (E.164 format)
              example: '+15551234567'

    CustomerLookupResponse:
      type: object
      required:
        - found
      properties:
        found:
          type: boolean
          description: Whether a matching customer record was found
        customer:
          type: object
          description: Customer details (present only when found is true)
          properties:
            name:
              type: string
              description: Customer's full name
              example: 'Jane Smith'
            phone:
              type: string
              description: Customer's phone number
              example: '+15551234567'
            existing_appointment:
              type: object
              description: Next upcoming appointment, if any
              properties:
                start_at:
                  type: string
                  format: date-time
                  description: Appointment start time (ISO 8601)
                  example: '2024-01-20T14:00:00Z'
                end_at:
                  type: string
                  format: date-time
                  description: Appointment end time (ISO 8601)
                  example: '2024-01-20T14:30:00Z'
                status:
                  type: string
                  description: Appointment status
                  example: 'confirmed'

    # Availability Search
    AvailabilitySearchWebhook:
      type: object
      required:
        - account_id
        - query
      properties:
        account_id:
          type: string
          description: The account ID (same as X-DialStack-Account-Id header)
          example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
        query:
          $ref: '#/components/schemas/AvailabilityQuery'

    AvailabilityQuery:
      type: object
      required:
        - filter
      properties:
        filter:
          $ref: '#/components/schemas/AvailabilityFilter'

    AvailabilityFilter:
      type: object
      required:
        - start_at_range
      properties:
        start_at_range:
          type: object
          description: Time range to search for availability
          required:
            - start_at
            - end_at
          properties:
            start_at:
              type: string
              format: date-time
              description: Start of the search range (ISO 8601)
              example: '2024-01-15T09:00:00Z'
            end_at:
              type: string
              format: date-time
              description: End of the search range (ISO 8601)
              example: '2024-01-15T17:00:00Z'

    AvailabilitySearchResponse:
      type: object
      required:
        - availabilities
      properties:
        availabilities:
          type: array
          description: List of available appointment slots
          items:
            $ref: '#/components/schemas/Availability'

    Availability:
      type: object
      required:
        - start_at
        - duration_minutes
      properties:
        start_at:
          type: string
          format: date-time
          description: Slot start time (ISO 8601)
          example: '2024-01-15T10:00:00Z'
        duration_minutes:
          type: integer
          minimum: 1
          description: Slot duration in minutes
          example: 30

    # Booking Creation
    CreateBookingWebhook:
      type: object
      required:
        - account_id
        - idempotency_key
        - booking
      properties:
        account_id:
          type: string
          description: The account ID (same as X-DialStack-Account-Id header)
          example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
        idempotency_key:
          type: string
          description: |
            Unique key to prevent duplicate bookings. If you receive the same key again,
            return the original booking response. Keys can be safely expired after 24 hours.
          example: 'booking-req-123456'
        booking:
          $ref: '#/components/schemas/BookingRequest'

    BookingRequest:
      type: object
      required:
        - start_at
        - duration_minutes
        - customer
      properties:
        start_at:
          type: string
          format: date-time
          description: Requested appointment start time (ISO 8601)
          example: '2024-01-15T10:00:00Z'
        duration_minutes:
          type: integer
          minimum: 1
          description: Appointment duration in minutes
          example: 30
        customer:
          $ref: '#/components/schemas/Customer'
        notes:
          type:
            - string
            - 'null'
          description: Additional notes for the appointment
          example: 'Initial consultation - referred by AI assistant'

    Customer:
      type: object
      required:
        - phone
        - name
      properties:
        phone:
          type: string
          description: Customer phone number (E.164 format)
          example: '+15551234567'
        name:
          type: string
          description: Customer name
          example: 'John Doe'
        email:
          type:
            - string
            - 'null'
          format: email
          description: Customer email address (optional)
          example: 'john@example.com'

    BookingResponse:
      type: object
      required:
        - booking
      properties:
        booking:
          $ref: '#/components/schemas/Booking'

    Booking:
      type: object
      required:
        - id
        - status
        - start_at
        - end_at
      properties:
        id:
          type: string
          description: Your platform's booking ID
          example: 'bkg_01h2xcejqtf2nbrexx3vqjhp41'
        status:
          type: string
          enum:
            - confirmed
            - pending
          description: Booking status
          example: 'confirmed'
        start_at:
          type: string
          format: date-time
          description: Confirmed appointment start time
          example: '2024-01-15T10:00:00Z'
        end_at:
          type: string
          format: date-time
          description: Confirmed appointment end time
          example: '2024-01-15T10:30:00Z'
        customer:
          type: object
          properties:
            phone:
              type: string
              example: '+15551234567'
            name:
              type: string
              example: 'John Doe'
        location:
          type: object
          description: Appointment location details
          properties:
            name:
              type: string
              description: Location name
              example: 'Main Office'
            address:
              type: string
              description: Full address
              example: '123 Main St, City, ST 12345'
        notes:
          type: string
          description: Additional booking information such as assigned provider
          example: 'Provider: Dr. Chen (Sports Medicine)'
        created_at:
          type: string
          format: date-time
          description: When the booking was created
          example: '2024-01-10T14:30:00Z'

    # Error Response
    WebhookError:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code
              example: 'slot_unavailable'
            message:
              type: string
              description: Human-readable error message
              example: 'The requested time slot is no longer available'

  responses:
    BadRequest:
      description: Invalid request format
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/WebhookError'
          example:
            error:
              code: 'invalid_request'
              message: 'Missing required field: booking.customer.phone'

    SlotUnavailable:
      description: Requested time slot is no longer available
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/WebhookError'
          example:
            error:
              code: 'slot_unavailable'
              message: 'The requested time slot is no longer available'

    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/WebhookError'
          example:
            error:
              code: 'internal_error'
              message: 'An unexpected error occurred'

security:
  - SignatureAuth: []

paths:
  /customers/lookup:
    post:
      summary: Lookup Customer
      operationId: lookupCustomer
      description: |
        DialStack sends this request at the start of every call to determine if
        the caller is a new or existing customer.

        Your platform should look up the caller by phone number and return their
        profile if found. Include upcoming appointment information if available —
        the voice AI uses this for context during the call.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/AccountIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerLookupWebhook'
            example:
              account_id: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
              customer:
                phone: '+15551234567'
      responses:
        '200':
          description: Customer lookup result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerLookupResponse'
              examples:
                customer_found:
                  summary: Existing customer with upcoming appointment
                  value:
                    found: true
                    customer:
                      name: 'Jane Smith'
                      phone: '+15551234567'
                      existing_appointment:
                        start_at: '2024-01-20T14:00:00Z'
                        end_at: '2024-01-20T14:30:00Z'
                        status: 'confirmed'
                customer_not_found:
                  summary: No matching customer
                  value:
                    found: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'

  /availability/search:
    post:
      summary: Search Availability
      operationId: searchAvailability
      description: |
        DialStack sends this request when the voice AI needs to find available appointment slots.

        Your platform should query your booking system and return matching availability.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/AccountIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AvailabilitySearchWebhook'
            example:
              account_id: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
              query:
                filter:
                  start_at_range:
                    start_at: '2024-01-15T09:00:00Z'
                    end_at: '2024-01-15T17:00:00Z'
      responses:
        '200':
          description: Available slots found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AvailabilitySearchResponse'
              examples:
                slots_found:
                  summary: Multiple slots available
                  value:
                    availabilities:
                      - start_at: '2024-01-15T10:00:00Z'
                        duration_minutes: 30
                      - start_at: '2024-01-15T10:30:00Z'
                        duration_minutes: 30
                no_availability:
                  summary: No slots available
                  value:
                    availabilities: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'

  /bookings:
    post:
      summary: Create Booking
      operationId: createBooking
      description: |
        DialStack sends this request when the voice AI needs to book an appointment.

        Your platform should:
        1. Check the `idempotency_key` - if you've seen it before, return the original response
        2. Verify the slot is still available
        3. Create the booking in your system
        4. Return the booking confirmation

        **Idempotency**: The `idempotency_key` prevents duplicate bookings. Store it when
        processing a booking, and if the same key arrives again, return the original
        booking response. Keys can be safely expired after 24 hours.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/AccountIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBookingWebhook'
            example:
              account_id: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
              idempotency_key: 'booking-req-123456'
              booking:
                start_at: '2024-01-15T10:00:00Z'
                duration_minutes: 30
                customer:
                  phone: '+15551234567'
                  name: 'John Doe'
                  email: 'john@example.com'
                notes: 'Initial consultation - referred by AI assistant'
      responses:
        '200':
          description: Booking created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingResponse'
              example:
                booking:
                  id: 'bkg_01h2xcejqtf2nbrexx3vqjhp41'
                  status: 'confirmed'
                  start_at: '2024-01-15T10:00:00Z'
                  end_at: '2024-01-15T10:30:00Z'
                  customer:
                    phone: '+15551234567'
                    name: 'John Doe'
                  location:
                    name: 'Main Office'
                    address: '123 Main St, City, ST 12345'
                  created_at: '2024-01-10T14:30:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          $ref: '#/components/responses/SlotUnavailable'
        '500':
          $ref: '#/components/responses/InternalError'

tags:
  - name: Webhooks
    description: |
      Webhook endpoints that your platform implements to receive scheduling requests from DialStack.

      These endpoints are called by DialStack when the voice AI needs to look up a customer,
      search for availability, or create a booking. Your platform processes the request and
      returns a response.
