openapi: 3.0.3
info:
  title: VoicerOnePBX API
  version: "1.0.0"
  description: |
    REST API for **VoicerOnePBX** — send fax, SMS, email and appointment-reminder calls, originate
    programmable-voice calls, poll received messages, and read live queue status.

    Except for `/api/queue-status` (a read-only LAN wallboard feed), every endpoint requires a **Bearer
    token** — the reminder-engine or CRM integration token configured in the PBX (Settings → Integrations).
  contact:
    name: VoicerOnePBX Documentation
    url: https://www.sneekypeek.ca/static/help/voiceronepbx/
servers:
  - url: https://{host}:8085
    description: HTTPS (recommended)
    variables:
      host: { default: pbx.example.com }
  - url: http://{host}:8084
    description: HTTP (LAN only)
    variables:
      host: { default: 192.168.10.50 }
security:
  - bearerAuth: []
tags:
  - name: Messaging
    description: Send fax, SMS, email and reminder calls
  - name: Voice
    description: Originate and drive calls
  - name: Inbox
    description: Poll received faxes, SMS and voicemail
  - name: Status
    description: Live status feeds
paths:
  /api/fax:
    post:
      tags: [Messaging]
      summary: Send a fax
      operationId: sendFax
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FaxRequest' }
      responses:
        '201':
          description: Queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FaxResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/sms:
    post:
      tags: [Messaging]
      summary: Send an SMS
      operationId: sendSms
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SmsRequest' }
      responses:
        '200':
          description: Sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  sent: { type: integer, example: 1 }
                  recipients: { type: integer, example: 1 }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '503': { description: No SMS trunk configured (Settings → SMS) }
  /api/email:
    post:
      tags: [Messaging]
      summary: Send an email
      operationId: sendEmail
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmailRequest' }
      responses:
        '200':
          description: Delivered
          content:
            application/json:
              schema:
                type: object
                properties:
                  recipients: { type: integer, example: 1 }
                  delivered: { type: integer, example: 1 }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/reminders:
    post:
      tags: [Messaging]
      summary: Schedule appointment-reminder call(s)
      operationId: scheduleReminders
      description: Body may be a single reminder object, a bare array of them, or `{ "reminders": [ ... ] }`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/Reminder'
                - type: array
                  items: { $ref: '#/components/schemas/Reminder' }
                - type: object
                  properties:
                    reminders:
                      type: array
                      items: { $ref: '#/components/schemas/Reminder' }
      responses:
        '200': { description: Accepted / scheduled }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/voice/originate:
    post:
      tags: [Voice]
      summary: Place a call and drive it with programmable voice
      operationId: originateCall
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OriginateRequest' }
      responses:
        '200':
          description: Dialing
          content:
            application/json:
              schema:
                type: object
                properties:
                  callId: { type: string }
                  status: { type: string, example: dialing }
                  to: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/inbox/faxes:
    get:
      tags: [Inbox]
      summary: List received faxes
      operationId: listInboxFaxes
      parameters:
        - { $ref: '#/components/parameters/since' }
        - { $ref: '#/components/parameters/limit' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InboxPage' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/inbox/sms:
    get:
      tags: [Inbox]
      summary: List inbound SMS
      operationId: listInboxSms
      parameters:
        - { $ref: '#/components/parameters/since' }
        - { $ref: '#/components/parameters/limit' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InboxPage' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/inbox/voicemails:
    get:
      tags: [Inbox]
      summary: List voicemail for a mailbox
      operationId: listInboxVoicemails
      parameters:
        - name: ext
          in: query
          required: true
          schema: { type: string }
          description: Mailbox extension (required — voicemail is per-mailbox)
        - { $ref: '#/components/parameters/since' }
        - { $ref: '#/components/parameters/limit' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InboxPage' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/queue-status:
    get:
      tags: [Status]
      summary: Live queue status (for wallboards)
      operationId: queueStatus
      security: []            # read-only LAN feed — no token
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/QueueStatus' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: The reminder-engine or CRM integration token from Settings → Integrations.
  parameters:
    since:
      name: since
      in: query
      schema: { type: integer, default: 0 }
      description: Opaque cursor from a prior response's `nextSince` (0 to start).
    limit:
      name: limit
      in: query
      schema: { type: integer, default: 50, maximum: 200 }
  responses:
    BadRequest:
      description: Malformed or missing parameters
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid Bearer token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
    FaxRequest:
      type: object
      required: [to, pdfBase64]
      properties:
        to: { type: string, example: "19725329272" }
        pdfBase64: { type: string, description: The document, base64-encoded (a data: prefix is tolerated). }
        from: { type: string, description: Optional caller ID }
        trunkId: { type: string, description: Optional — else a fax-capable trunk is auto-selected }
        externalRef: { type: string, description: Echoed back in the status callback }
        statusCallbackUrl: { type: string, format: uri }
        cover:
          type: object
          properties:
            to: { type: string }
            from: { type: string }
            subject: { type: string }
            note: { type: string }
    FaxResponse:
      type: object
      properties:
        faxId: { type: string }
        status: { type: string, example: sending }
        to: { type: string }
    SmsRequest:
      type: object
      required: [to, message]
      properties:
        to:
          description: One number, a comma/semicolon-separated list, or an array of numbers.
          oneOf:
            - { type: string, example: "905-555-0134" }
            - { type: array, items: { type: string } }
        message: { type: string, example: "Your appointment is confirmed." }
        text: { type: string, description: Alias for `message`. }
    EmailRequest:
      type: object
      required: [to, subject]
      description: Provide `html` or `text`.
      properties:
        to:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
        subject: { type: string }
        html: { type: string }
        text: { type: string }
        attachment:
          type: object
          properties:
            filename: { type: string, example: receipt.pdf }
            base64: { type: string }
    Reminder:
      type: object
      required: [customerName, phoneNumber, appointmentAt]
      properties:
        customerName: { type: string, example: "Jane Roe" }
        phoneNumber: { type: string, example: "905-555-0134" }
        appointmentAt: { type: string, format: date-time }
        serviceName: { type: string }
        staffName: { type: string }
        templateId: { type: string }
        customNote: { type: string }
        externalRef: { type: string }
        statusCallbackUrl: { type: string, format: uri }
    OriginateRequest:
      type: object
      required: [to, controlUrl]
      properties:
        to: { type: string, example: "19055550134" }
        controlUrl:
          type: string
          format: uri
          description: Your programmable-voice control URL; the PBX POSTs here when the call is answered.
        callerId: { type: string }
        secret: { type: string, description: Shared secret used to sign the callback (recommended). }
        timeoutMs: { type: integer, default: 2000 }
        statusCallbackUrl: { type: string, format: uri }
        externalRef: { type: string }
        authorization: { type: string, description: Optional Authorization header presented to your control URL. }
    InboxPage:
      type: object
      properties:
        items:
          type: array
          items: { type: object, additionalProperties: true }
        nextSince:
          type: integer
          description: Cursor to pass as `since` on the next poll.
    QueueStatus:
      type: object
      properties:
        server: { type: string, example: VoicerOnePBX }
        generatedAt: { type: string, format: date-time }
        queues:
          type: array
          items: { $ref: '#/components/schemas/Queue' }
    Queue:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        open: { type: boolean }
        waiting: { type: integer, description: Callers waiting now }
        longestWaitSeconds: { type: integer }
        agents:
          type: object
          properties:
            available: { type: integer }
            busy: { type: integer }
            total: { type: integer }
        agentDetail:
          type: array
          items:
            type: object
            properties:
              extension: { type: string }
              state: { type: string, example: available }
