openapi: 3.1.2

info:
  title: Phonovation SMS API
  version: 1.0.0
  summary: Build SMS into your product, workflow, or platform with a straightforward, developer-friendly API.
  description: |
    **Build fast. Send with confidence.**

    Add application-to-person (A2P) SMS to your product, workflow, or platform
    with Phonovation. Use this API to send or schedule campaigns, track each
    recipient in your own system, receive delivery updates, and review campaign
    results.

    From appointment reminders and service alerts to customer campaigns, you get
    a clear integration path and the delivery visibility needed to keep every
    message accountable.

    ## Account access

    [Log in or create your Phonovation account](https://app.phonovation.com/) to
    manage your messaging setup and developer settings.

    A typical integration has three steps:

    1. Authenticate with a bearer token.
    2. Create a campaign with your message, sender ID, and recipients.
    3. Track delivery through campaign summaries and signed webhooks.

    ## Authentication

    Every campaign request needs a bearer token — the credential that tells
    Phonovation which account is making the request. Add it to the
    `Authorization` header:

    `Authorization: Bearer <token>`

    ### Recommended: a UI-generated Personal Access Token

    For most integrations, the simplest and preferred option is a Phonovation
    Personal Access Token (PAT) generated in the Phonovation UI. PATs begin with
    `phv_pat_`; keep the casing exactly as shown when sending one:

    `Authorization: Bearer phv_pat_...`

    [Generate and manage your PATs](https://app.phonovation.com/developer?tab=pat-tokens)
    in the Phonovation developer settings.

    Generate the PAT once, store it securely, and reuse it for your API requests.
    **You do not need to obtain or refresh an OAuth token every time you send an
    SMS.** Replace the PAT only when it expires, is revoked, or you intentionally
    rotate it.

    ### Optional: OAuth access tokens

    OAuth/OIDC JWT access tokens remain available for integrations that specifically
    require an OAuth token lifecycle. The `/token` endpoint can exchange user
    credentials for an access token and refresh token, or exchange a refresh token
    for a new access token.

    Even when using OAuth, request an access token once and reuse it until it is close
    to expiry. Use the refresh token to obtain the next access token — do not request
    a new token before every SMS.

    ## Campaign processing

    Campaign requests are accepted and queued for background processing. A successful
    request returns `202 Accepted` with the campaign ID.

    A `202` response confirms receipt, not final campaign creation. Allow time for
    processing before requesting the campaign summary.

    ## Scheduling timezone

    The `sendAt` value is always interpreted as Irish local time in the
    `Europe/Dublin` timezone. Phonovation does not interpret it as UTC and does
    not use the timezone of the caller, server, account, or recipient.

    Do not include a timezone designator such as `Z` or a numeric UTC offset.
    International integrations must convert the intended send time to Irish
    local time before making the request. Ireland observes daylight saving time,
    so use a timezone-aware library and the `Europe/Dublin` timezone instead of
    a fixed UTC offset.

servers:
  - url: https://api.phonovation.com
    description: Phonovation production API
  - url: https://auth.phonovation.com
    description: Phonovation authentication server

security:
  - BearerAuth: []

tags:
  - name: Authentication
    description: Use a UI-generated PAT where possible, or obtain OAuth tokens for integrations that require them.
  - name: Health
    description: Check whether the Phonovation API is available and responding.
  - name: Campaigns
    description: Send now or schedule ahead, then track delivery from one integration.

paths:
  /token:
    post:
      x-hideTryItPanel: true
      tags:
        - Authentication
      summary: Obtain or refresh an OAuth access token
      operationId: obtainAccessToken
      security: []
      servers:
        - url: https://auth.phonovation.com
          description: Phonovation authentication server
      description: |
        This endpoint is an optional OAuth route for integrations that need an access
        token and refresh-token lifecycle.

        **For most integrations, use a Personal Access Token generated in the
        Phonovation UI instead.** A PAT can be reused across SMS requests, so you do
        not need to call this endpoint, generate an OAuth token, or refresh a token
        every time you send a message.

        If your integration does require OAuth, send this request as
        `application/x-www-form-urlencoded`. The OAuth client ID is `messaging-api`.

        Supported grants:

        - `password`: exchange a Phonovation username and password for an access token
          and refresh token. This compatibility flow must be enabled for the
          `messaging-api` client.
        - `refresh_token`: exchange a valid refresh token for a new access token
          without resending the username and password.

        Reuse the returned access token until it is close to expiry, then use the
        refresh token. Do not request a new access token for each SMS.
      requestBody:
        required: true
        description: OAuth credentials or a refresh token, encoded as form fields.
        content:
          application/x-www-form-urlencoded:
            schema:
              oneOf:
                - $ref: "#/components/schemas/PasswordGrantRequest"
                - $ref: "#/components/schemas/RefreshTokenGrantRequest"
            examples:
              passwordGrant:
                summary: Obtain tokens with user credentials
                value:
                  client_id: messaging-api
                  grant_type: password
                  username: user@example.com
                  password: your-password
              refreshTokenGrant:
                summary: Refresh an access token
                value:
                  client_id: messaging-api
                  grant_type: refresh_token
                  refresh_token: your-refresh-token
      responses:
        "200":
          description: OAuth tokens issued successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
              example:
                access_token: eyJhbGciOi...
                expires_in: 300
                refresh_expires_in: 1800
                refresh_token: eyJhbGciOi...
                token_type: Bearer
                not-before-policy: 0
                session_state: 11111111-2222-3333-4444-555555555555
                scope: openid
        "400":
          description: The credentials, grant, or refresh token were missing or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
              examples:
                invalidGrant:
                  summary: Invalid credentials or refresh token
                  value:
                    error: invalid_grant
                    error_description: Invalid user credentials
        "401":
          description: OAuth client authentication failed, if client authentication is required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"

  /health:
    get:
      tags:
        - Health
      summary: Check whether the API is running
      operationId: getHealthStatus
      security: []
      servers:
        - url: https://api.phonovation.com
          description: Phonovation production API
      description: |
        Use this lightweight health check to confirm that the Phonovation API service
        is running and able to respond.

        A `200 OK` response with the value `Healthy` means the service is available at
        the time of the check. It does not guarantee that every connected system,
        mobile network, or individual messaging workflow is free from faults. Think
        of it as a simple availability signal rather than a complete diagnostic or a
        guarantee of message delivery.

        If the request times out, cannot connect, or returns a non-successful status,
        treat the service as not currently operational and try again later.
      responses:
        "200":
          description: The API service is running and responding.
          content:
            text/plain:
              schema:
                type: string
                const: Healthy
              example: Healthy
        "4XX":
          description: The health check request was rejected and did not confirm service availability.
        default:
          description: The service did not report a healthy status and should be treated as unavailable.

  /v1/campaign:
    post:
      tags:
        - Campaigns
      summary: Create and send an SMS campaign
      operationId: createCampaign
      servers:
        - url: https://api.phonovation.com
          description: Phonovation production API
      description: |
        Send an SMS campaign to one or more recipients. You can send immediately,
        schedule it for later, or save it as a draft.

        If you omit `sendAt`, Phonovation uses the current Irish local time when
        creating the recipients.

        **Scheduling always uses Irish local time (`Europe/Dublin`).** Phonovation
        reads the date and clock time in `sendAt` as Irish time. It does not convert
        from UTC or from the caller's timezone. For example,
        `2030-07-08T15:00` means 15:00 in Ireland, even when the request is sent
        from China or another country.

        Do not append `Z` or a numeric UTC offset. Convert the intended send time
        to `Europe/Dublin` before submitting the request. Account for Irish daylight
        saving time rather than using a fixed offset.

        One invalid number does not have to stop the whole campaign. Invalid,
        duplicate, or unpermitted recipients may be skipped while valid recipients
        continue to be processed.

        Duplicate detection happens after phone-number normalization. If two entries
        resolve to the same number, Phonovation keeps the first entry and its
        `ClientReference`.

        A valid request returns `202 Accepted` when it has been queued for background
        processing.
      requestBody:
        required: true
        description: |
          The message, sender, recipients, and optional campaign settings. Any
          `sendAt` value must contain the intended Irish local date and time.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCampaignRequest"
            examples:
              oneRecipient:
                $ref: "#/components/examples/SingleRecipientCampaign"
              oneRecipientWithClientReference:
                $ref: "#/components/examples/SingleRecipientCampaignWithClientReference"
              multipleRecipients:
                $ref: "#/components/examples/MultipleRecipientCampaign"
              multipleRecipientsWithClientReferences:
                $ref: "#/components/examples/MultipleRecipientCampaignWithClientReferences"
              scheduled:
                $ref: "#/components/examples/ScheduledCampaign"
      responses:
        "202":
          description: The campaign is accepted for background processing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateCampaignResponse"
              examples:
                queued:
                  summary: Queued campaign
                  value:
                    id: "ffd14db7-e526-4d69-b41e-ec5e38bc04dd"
                    message: "Campaign received to be processed"
          links:
            GetCampaignSummary:
              operationId: getCampaignSummary
              parameters:
                id: "$response.body#/id"
              description: |
                Use the returned ID to request the campaign summary. Processing may
                still be in progress, so a summary might not be available immediately.
        "400":
          description: Request validation or immediate campaign creation fails.
          content:
            application/problem+json:
              schema:
                type: object
                required:
                  - title
                  - status
                  - detail
                properties:
                  title:
                    type: string
                    const: Validation Error
                  status:
                    type: integer
                    const: 400
                  detail:
                    type: string
                    description: Dynamic validation or campaign failure message.
              example:
                title: Validation Error
                status: 400
                detail: "<dynamic validation or campaign failure message>"
        "401":
          description: Authentication is missing or unsuccessful. No response body is returned.
          headers:
            WWW-Authenticate:
              description: Bearer authentication challenge.
              schema:
                type: string
                const: Bearer
              example: Bearer
        "403":
          description: Authentication succeeds, but the identity is not a recognized client administrator.
          content:
            application/problem+json:
              schema:
                type: object
                required:
                  - title
                  - status
                  - detail
                properties:
                  title:
                    type: string
                    const: Forbidden
                  status:
                    type: integer
                    const: 403
                  detail:
                    type: string
                    const: Authenticated user is not a recognized client administrator
              example:
                title: Forbidden
                status: 403
                detail: "Authenticated user is not a recognized client administrator"

  /v1/campaign/{id}:
    get:
      tags:
        - Campaigns
      summary: View campaign delivery results
      operationId: getCampaignSummary
      servers:
        - url: https://api.phonovation.com
          description: Phonovation production API
      description: |
        See how a campaign is performing with totals for sent, delivered,
        undelivered, and pending messages.

        For security, you can retrieve only campaigns that belong to your
        authenticated account. The API returns `404 Not Found` if the ID is unknown,
        belongs to another account, or does not have a summary yet.

        Draft campaigns do not have a summary until they are marked ready.
      parameters:
        - $ref: "#/components/parameters/CampaignId"
      responses:
        "200":
          description: The latest campaign delivery totals.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CampaignSummary"
              examples:
                summary:
                  value:
                    totalSent: 100
                    delivered: 92
                    undelivered: 3
                    pending: 5
        "400":
          description: The campaign ID is not a valid UUID.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

webhooks:
  Webhook:
    post:
      summary: Receive Phonovation webhook events
      operationId: receiveWebhook
      security: []
      description: |
        Receive real-time events from Phonovation in your application. When an event
        becomes available, Phonovation sends a JSON `POST` request to your configured
        webhook URL.

        [Configure your webhook URL and signing secret](https://app.phonovation.com/developer?tab=webhook)
        in the Phonovation developer settings.

        ## Current events

        ### Delivery receipts (DLRs)

        The webhook currently contains delivery receipts, commonly called DLRs. A DLR
        tells your application what happened after an SMS was sent — for example,
        whether the mobile network delivered it, could not deliver it, or is still
        processing it.

        DLRs give you the visibility to update customer records, trigger follow-up
        workflows, and measure campaign delivery without repeatedly checking the API.

        ### Match a DLR to your recipient

        Add your own `ClientReference` when creating each recipient. Phonovation
        returns that same value in the webhook, making it easy to match the delivery
        update to a customer, order, appointment, or record in your system.

        Property names are case-sensitive and use PascalCase: `To`, `From`, `Status`,
        and `ClientReference`.

        Do not restrict `Status` to a fixed list. Phonovation normally passes through
        the alphabetic gateway `stat:` value and may return the raw gateway status
        when no `stat:` value can be read.

        ## Future events

        This API version documents DLRs, but Phonovation may add other webhook event
        types in the future. Design your endpoint so it can be extended to support new
        events, and avoid rejecting a request only because it contains additional JSON
        properties you do not use. Any new event payloads will be documented before
        they become part of the supported webhook contract.

        ## Check that the webhook is genuine

        If you configure a webhook secret, every request includes an `X-Signature`
        header. Think of this signature as a tamper-evident seal: your application can
        use it to check that the request came from someone who knows the shared secret
        and that the body was not changed in transit.

        The header looks like this:

        `sha256=<lowercase hex HMAC-SHA256>`

        To verify it:

        1. Read the request body as raw bytes, before parsing the JSON.
        2. Calculate an HMAC-SHA256 digest using your webhook secret as the key.
        3. Convert the digest to lowercase hexadecimal and add the `sha256=` prefix.
        4. Compare your result with `X-Signature` using a constant-time comparison.

        Always use the exact UTF-8 request bytes. Parsing and rebuilding the JSON can
        change whitespace or property order and cause a valid signature to fail.
        Reject the webhook if the signatures do not match.

        ## Delivery and retries

        Return any `2xx` response after safely storing or queueing the event to
        acknowledge the webhook.

        The HTTP delivery policy retries:

        - network failures
        - `404 Not Found`
        - `408 Request Timeout`
        - `5xx` responses

        It makes up to three retry attempts with exponential delays:

        1. approximately 2 seconds after the initial failure
        2. approximately 4 seconds after the previous failure
        3. approximately 8 seconds after the previous failure

        Other `4xx` responses are not retried, including `400 Bad Request`,
        `401 Unauthorized`, and `403 Forbidden`.

        A circuit breaker opens for 30 seconds after two transient failures. While
        it is open, a configured retry may fail fast without making an HTTP request
        to your endpoint. Because of this, the circuit breaker may prevent all
        configured retry attempts from becoming actual HTTP requests.

        Retries can deliver the same event more than once. Process webhooks
        idempotently so a duplicate does not repeat a business action or corrupt
        status. Delivery order is not guaranteed.
      parameters:
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        description: The current webhook payload containing an SMS delivery receipt (DLR).
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeliveryReceipt"
            examples:
              delivered:
                $ref: "#/components/examples/DeliveryReceiptDelivered"
              undelivered:
                $ref: "#/components/examples/DeliveryReceiptUndelivered"
      responses:
        "2XX":
          description: Webhook acknowledged. Any 2xx response is treated as success and is not retried.
        "404":
          description: Endpoint not found. Phonovation applies the webhook retry policy.
        "408":
          description: Endpoint timed out. Phonovation applies the webhook retry policy.
        "4XX":
          description: |
            The webhook was rejected and is not retried, except for the explicitly
            retryable `404` and `408` responses.
        "5XX":
          description: Server failure. Phonovation applies the webhook retry policy.

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        A UI-generated Phonovation Personal Access Token (PAT) is the recommended
        option for most integrations. OAuth/OIDC JWT access tokens are also accepted
        when an OAuth lifecycle is required.

        Send either token as:

        `Authorization: Bearer <token>`

        PAT values begin with `phv_pat_`. Generate a PAT once in the Phonovation UI,
        store it securely, and reuse it across requests. You do not need to generate
        or refresh a token for every SMS. Preserve the exact casing in
        `Bearer phv_pat_...`.

  parameters:
    CampaignId:
      name: id
      in: path
      required: true
      description: The public UUID returned when the campaign request was accepted.
      schema:
        type: string
        format: uuid
      example: "ffd14db7-e526-4d69-b41e-ec5e38bc04dd"

    WebhookSignature:
      name: X-Signature
      in: header
      required: false
      description: |
        A tamper-check for the webhook request. Use your shared webhook secret to
        calculate an HMAC-SHA256 signature over the exact raw UTF-8 request body,
        then compare it with this header before trusting or processing the payload.

        The value is `sha256=` followed by a 64-character lowercase hexadecimal
        digest. Phonovation omits the header when no webhook secret is configured.
      schema:
        type: string
        pattern: '^sha256=[0-9a-f]{64}$'
      example: "sha256=bd0f5689f67aa2dfef28b9ea587b0079e95bb022c5818f0c1a52e4ff3f9452a9"

  schemas:
    PasswordGrantRequest:
      type: object
      title: Password Grant Request
      description: |
        OAuth compatibility request that exchanges Phonovation user credentials for
        an access token and refresh token. Most integrations should use a UI-generated
        PAT instead.
      required:
        - client_id
        - grant_type
        - username
        - password
      properties:
        client_id:
          type: string
          enum:
            - messaging-api
          default: messaging-api
          description: OAuth client identifier. Use `messaging-api`.
        grant_type:
          type: string
          enum:
            - password
          description: Use `password` to exchange user credentials for OAuth tokens.
        username:
          type: string
          description: Your Phonovation username.
        password:
          type: string
          format: password
          writeOnly: true
          description: Your Phonovation password.

    RefreshTokenGrantRequest:
      type: object
      title: Refresh Token Grant Request
      description: |
        Exchanges a valid refresh token for a new OAuth access token. Reuse each
        access token until it is close to expiry rather than refreshing before every
        SMS request.
      required:
        - client_id
        - grant_type
        - refresh_token
      properties:
        client_id:
          type: string
          enum:
            - messaging-api
          default: messaging-api
          description: OAuth client identifier. Use `messaging-api`.
        grant_type:
          type: string
          enum:
            - refresh_token
          description: Use `refresh_token` to obtain a new OAuth access token.
        refresh_token:
          type: string
          writeOnly: true
          description: Refresh token returned by an earlier successful token request.

    TokenResponse:
      type: object
      title: OAuth Token Response
      description: OAuth tokens and their lifetimes.
      required:
        - access_token
        - expires_in
        - token_type
      properties:
        access_token:
          type: string
          description: OAuth access token to reuse in the `Authorization` header until it is close to expiry.
        expires_in:
          type: integer
          format: int32
          description: Access-token lifetime in seconds.
        refresh_expires_in:
          type: integer
          format: int32
          description: Refresh-token lifetime in seconds, when returned.
        refresh_token:
          type: string
          description: Token used to obtain a new access token without resending user credentials.
        token_type:
          type: string
          description: Authorization scheme for the access token.
          examples:
            - Bearer
        id_token:
          type: string
          description: OpenID Connect ID token, when returned for the requested scope or flow.
        not-before-policy:
          type: integer
          format: int32
          description: Authentication not-before policy value, when returned.
        session_state:
          type: string
          description: Authentication session identifier, when returned.
        scope:
          type: string
          description: Space-separated OAuth scopes granted to the token, when returned.
      additionalProperties: true

    OAuthError:
      type: object
      title: OAuth Error
      description: Error returned when an OAuth token request cannot be completed.
      required:
        - error
      properties:
        error:
          type: string
          description: Machine-readable OAuth error code.
        error_description:
          type: string
          description: Human-readable detail to help diagnose the failed token request.
      additionalProperties: true

    CreateCampaignRequest:
      type: object
      title: Create Campaign Request
      description: Everything Phonovation needs to create, schedule, or save an SMS campaign.
      required:
        - text
        - from
        - recipientInfo
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 2000
          pattern: '\S'
          description: |
            The message your recipients will receive. It must include at least one
            visible character.

            Phonovation automatically detects the message encoding and calculates how
            many SMS parts will be sent and billed:

            - GSM-7: up to 160 characters in one part, then 153 per part.
            - UTF-16: up to 70 characters in one part, then 67 per part.
            - GSM-7 extended characters count as two character units.

            To improve handset compatibility, Phonovation replaces these typographic
            characters before sending:

            - typographic single quotes to `'`
            - typographic double quotes to `"`
            - en/em dashes to `-`
            - ellipsis to `.`
            - bullet to `*`

            The API does not automatically add opt-out or footer text, so include any
            wording required for your use case and compliance obligations.
          examples:
            - "Your appointment is tomorrow at 10:30."

        from:
          type: string
          minLength: 1
          pattern: '\S'
          allOf:
            - if:
                pattern: '^[0-9]+$'
              then:
                maxLength: 20
              else:
                maxLength: 11
          description: |
            The sender name or number recipients see on their phone.

            Leading and trailing spaces are removed automatically.

            - Numbers-only sender IDs may contain up to 20 digits.
            - All other sender IDs may contain up to 11 characters.
            - Numbers from `50000` through `59999` are reserved and cannot be used.

            Non-numeric sender IDs are not limited to letters and numbers by this API.

            For Irish recipients, the sender ID must be on the permitted sender list.
            Recipients for whom the sender is not permitted are skipped.
          examples:
            - CompanyName
            - "3531234567"

        recipientInfo:
          type: array
          minItems: 1
          description: |
            One or more people who should receive the campaign. The API does not set a
            maximum recipient count.

            Invalid or duplicate numbers may be skipped while valid recipients continue.
            Duplicate detection happens after phone-number normalization, and the first
            occurrence is kept.
          items:
            $ref: "#/components/schemas/Recipient"

        campaignName:
          type: string
          minLength: 1
          maxLength: 30
          pattern: '\S'
          default: API Broadcast
          description: |
            A short name to help you identify the campaign in reporting. If omitted,
            it defaults to `API Broadcast`.

            Keep the name to 30 characters or fewer. `null`, empty, and spaces-only
            values are rejected. Valid names are stored exactly as supplied.
          examples:
            - Summer Promotion

        sendAt:
          type:
            - string
            - "null"
          pattern: '^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$'
          description: |
            When the campaign should be sent, interpreted exclusively as Irish local
            time in the `Europe/Dublin` timezone.

            Phonovation reads the date and clock portion exactly as Irish time. It
            does not use the timezone of the caller, server, account, or recipient,
            and it does not convert the value from UTC. A request sent from China
            with `2030-07-08T15:00` schedules the campaign for 15:00 in Ireland,
            not 15:00 in China.

            Prefer `yyyy-MM-ddTHH:mm`, for example `2030-07-08T14:30`.
            These forms are accepted:

            - `yyyy-MM-ddTHH:mm`
            - `yyyy-MM-ddTHH:mm:ss`
            - `yyyy-MM-dd HH:mm`
            - `yyyy-MM-dd HH:mm:ss`

            If seconds are supplied, they are ignored and the scheduled time is
            normalized to the start of the minute.

            Example conversions for 8 July 2030 at 15:00 in the source country:

            - United Kingdom (`Europe/London`) becomes
              `2030-07-08T15:00` in Ireland.
            - China (`Asia/Shanghai`) becomes `2030-07-08T08:00` in Ireland.
            - New York, United States (`America/New_York`) becomes
              `2030-07-08T20:00` in Ireland.

            - Do not include `Z`, another timezone designator, or a numeric UTC offset.
            - If the intended send time is outside Ireland, convert it to
              `Europe/Dublin` before constructing this value.
            - Ireland uses GMT (UTC+0) during part of the year and Irish Standard
              Time (UTC+1) during part of the year. Use a timezone-aware library;
              do not hard-code a single UTC offset.
            - Omit the field or send `null` to send immediately. Empty and
              whitespace-only strings are invalid.
            - Past dates are accepted.
            - The API does not set a maximum scheduling horizon.
          examples:
            - "2030-07-08T14:30"
            - "2030-07-08T14:30:45"
            - "2030-07-08 14:30"
            - "2030-07-08 14:30:45"

        createAsDraft:
          type: boolean
          default: false
          description: |
            Set to `true` to save the campaign as a draft. A draft has no delivery
            summary until it is marked ready.

        shouldSaveList:
          type: boolean
          default: false
          description: |
            Optional saved-list flag. It is passed through as `shouldSaveList` and
            defaults to `false`. Saved-list behaviour is not part of the current
            public API contract, so leave this as `false` unless Phonovation has
            enabled the feature for your integration.

    Recipient:
      type: object
      title: Campaign Recipient
      description: A mobile number to message, with an optional reference from your system.
      required:
        - msisdn
      properties:
        msisdn:
          type: string
          minLength: 1
          maxLength: 20
          description: |
            The recipient's mobile number (also called an MSISDN). Use digits with an
            optional leading `+`; do not include spaces, brackets, or hyphens.

            The API normalizes supported Irish and UK formats, but it does **not**
            perform full international E.164 validation.

            Accepted Irish examples include:

            - `0871234567`
            - `353871234567`
            - `+353871234567`
            - `00353871234567`

            Supported Irish numbers are stored as `353...`; supported UK numbers are
            stored as `44...`. Other digits-only values are accepted unchanged, but
            the API does not confirm their country or whether they can receive SMS.
          examples:
            - "353871234567"
            - "0871234567"

        ClientReference:
          type:
            - string
            - "null"
          maxLength: 30
          description: |
            Your optional reference for this recipient — for example, a customer,
            order, or appointment ID.

            Phonovation stores the value unchanged and returns it in delivery webhooks
            as `ClientReference`. It does not need to be unique. If omitted,
            `ClientReference` is `null`.
          examples:
            - customer-123

    CreateCampaignResponse:
      type: object
      title: Create Campaign Response
      description: Confirms that the campaign request was accepted and queued.
      required:
        - id
        - message
      properties:
        id:
          type: string
          format: uuid
          description: Public campaign UUID. Store it so you can request delivery totals later.
          examples:
            - "ffd14db7-e526-4d69-b41e-ec5e38bc04dd"
        message:
          type: string
          const: Campaign received to be processed
          description: |
            Human-readable confirmation for logs or troubleshooting. A successful
            `202` response returns `Campaign received to be processed`.
          examples:
            - "Campaign received to be processed"

    CampaignSummary:
      type: object
      title: Campaign Delivery Summary
      description: At-a-glance delivery totals for a campaign that has been marked ready.
      required:
        - totalSent
        - delivered
        - undelivered
        - pending
      properties:
        totalSent:
          type: integer
          format: int32
          description: Number of accepted recipients recorded when the campaign was marked ready.
          examples:
            - 100
        delivered:
          type: integer
          format: int32
          description: Receipts with a parsed status beginning with `DELIV`.
          examples:
            - 92
        undelivered:
          type: integer
          format: int32
          description: Processed receipts that are not classified as delivered.
          examples:
            - 3
        pending:
          type: integer
          format: int32
          description: Accepted recipients with no processed delivered or undelivered receipt yet.
          examples:
            - 5

    DeliveryReceipt:
      type: object
      title: Delivery Receipt
      description: The delivery update Phonovation sends to your webhook.
      required:
        - To
        - From
        - Status
        - ClientReference
      properties:
        To:
          type: string
          description: The recipient's normalized mobile number.
          examples:
            - "353871234567"
        From:
          type: string
          description: The sender ID used for the campaign.
          examples:
            - Phonovation
        Status:
          type: string
          maxLength: 30
          description: |
            The delivery status reported by the messaging gateway.

            Treat this as an open-ended string, not a fixed enum. Phonovation preserves
            parsed alphabetic gateway `stat:` values and may fall back to raw gateway
            status text. Letter casing is not changed.
          examples:
            - DELIVERED
            - UNDELIV
            - REJECTD
        ClientReference:
          type:
            - string
            - "null"
          maxLength: 30
          description: Your original recipient `ClientReference`, or `null` if you did not supply one.
          examples:
            - cli-ref-123

    ProblemDetails:
      type: object
      title: Problem Details
      description: |
        Standard details for validation and application errors. Authentication and
        unexpected server errors may not use this exact response shape.
      required:
        - title
        - status
        - detail
      properties:
        type:
          type:
            - string
            - "null"
          description: Optional identifier for the type of problem.
        title:
          type: string
          description: Short, human-readable summary of what went wrong.
        status:
          type: integer
          minimum: 400
          maximum: 599
          description: HTTP status code for the error.
        detail:
          type: string
          description: A more specific explanation to help you correct or diagnose the request.
        instance:
          type:
            - string
            - "null"
          description: Optional identifier for this specific occurrence of the problem.

  responses:
    ValidationError:
      description: |
        The request could not be processed because one or more values were missing or
        invalid. Check `detail` for the field and reason.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          examples:
            missingMsisdn:
              summary: Missing recipient MSISDN
              value:
                title: Validation Error
                status: 400
                detail: "Msisdn: value is required."

    Unauthorized:
      description: |
        Authentication failed. The bearer token may be missing, malformed, invalid,
        expired, or revoked. No fixed response body or `WWW-Authenticate` header is
        guaranteed.

    Forbidden:
      description: |
        The token is valid, but the identity is not linked to an active, recognized
        Phonovation client or client administrator.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          examples:
            unrecognizedAdministrator:
              value:
                title: Forbidden
                status: 403
                detail: "Authenticated user is not a recognized client administrator"

    NotFound:
      description: |
        No campaign summary is available for this account and campaign ID. The
        campaign may be unknown, belong to another account, still be processing, or
        be saved as a draft.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    InternalServerError:
      description: |
        Phonovation encountered an unexpected application or infrastructure error.
        No fixed response body or media type is guaranteed.

  examples:
    SingleRecipientCampaign:
      summary: Send SMS to one recipient
      value:
        text: "Your appointment is tomorrow at 10:30."
        from: "CompanyName"
        recipientInfo:
          - msisdn: "353871234567"

    SingleRecipientCampaignWithClientReference:
      summary: Send SMS to one recipient with a ClientReference
      value:
        text: "Your appointment is tomorrow at 10:30."
        from: "CompanyName"
        recipientInfo:
          - msisdn: "353871234567"
            ClientReference: "appointment-001"

    MultipleRecipientCampaign:
      summary: Send SMS to multiple recipients
      value:
        text: "Service update"
        from: "CompanyName"
        campaignName: "Service Update"
        recipientInfo:
          - msisdn: "353871234567"
          - msisdn: "353851112222"

    MultipleRecipientCampaignWithClientReferences:
      summary: Send SMS to multiple recipients with ClientReferences
      value:
        text: "Service update"
        from: "CompanyName"
        campaignName: "Service Update"
        recipientInfo:
          - msisdn: "353871234567"
            ClientReference: "customer-001"
          - msisdn: "353851112222"
            ClientReference: "customer-002"

    ScheduledCampaign:
      summary: Schedule a campaign for 14:30 Irish local time
      value:
        text: "Reminder about your scheduled appointment"
        from: "CompanyName"
        campaignName: "Appointment Reminders"
        sendAt: "2030-07-08T14:30"
        recipientInfo:
          - msisdn: "353871234567"
            ClientReference: "appointment-001"
          - msisdn: "353851112222"
            ClientReference: "appointment-002"

    DeliveryReceiptDelivered:
      summary: SMS delivered successfully
      value:
        To: "353871234567"
        From: "Phonovation"
        Status: "DELIVERED"
        ClientReference: null

    DeliveryReceiptUndelivered:
      summary: SMS was not delivered
      value:
        To: "353851112222"
        From: "Phonovation"
        Status: "UNDELIV"
        ClientReference: null
