openapi: 3.1.0
info:
  title: 23 Telecom SMS API
  version: 1.0.0
  description: |
    REST API for sending SMS worldwide: message dispatch, delivery tracking,
    statistics, balance, pricing and webhooks.

    **Base URL:** `https://restlink23telecom.com/api/v1`

    Authenticate with the `X-API-Key` header. Create keys in the customer
    portal under **Settings → API Keys**. Full guides at
    [docs.23telecom.co.uk](https://docs.23telecom.co.uk).
  contact:
    name: 23 Telecom Support
    url: https://restlink23telecom.com
  termsOfService: https://23telecom.co.uk
servers:
  - url: https://restlink23telecom.com/api/v1
    description: Production
tags:
  - name: SMS
    description: Send messages and track delivery.
  - name: Statistics
    description: Aggregate, daily and per-country statistics.
  - name: Messages
    description: Message history, unified view and CSV export.
  - name: Account
    description: Balance, pricing and payment history.
  - name: Webhook settings
    description: Webhook configuration, testing and delivery logs. (Webhook event payloads are described in the Webhooks section below.)

security:
  - ApiKeyAuth: []

paths:
  /sms/send:
    post:
      operationId: sendSms
      tags: [SMS]
      summary: Send SMS
      description: |
        Send an SMS to 1–100 recipients. Requires the `sms.send` permission.

        The request is **atomic**: either every recipient is queued or none is
        (a `500 DB_ERROR` means nothing was sent — safe to retry the whole request).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendSmsRequest'
            example:
              to: ['+14155551234', '+447911123456']
              message: 'Your verification code is 847291'
              sender_id: 'MyApp'
      responses:
        '200':
          description: Messages accepted for delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendSmsResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Insufficient balance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsufficientBalanceError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: Request could not be queued (atomic rollback — nothing was sent).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /sms/status/{message_id}:
    get:
      operationId: getMessageStatus
      tags: [SMS]
      summary: Get message status
      description: |
        Check the delivery status of a sent message. Requires `sms.read`.
        Message IDs look like `api_42_1743667200123456789_a3f8b2c1d9e45f67`.
      parameters:
        - name: message_id
          in: path
          required: true
          schema: { type: string }
          example: api_42_1743667200123456789_a3f8b2c1d9e45f67
      responses:
        '200':
          description: Message found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Message not found (or belongs to another account).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /sms/stats:
    get:
      operationId: getStats
      tags: [Statistics]
      summary: Get statistics
      description: Aggregate SMS statistics for a period. Requires `sms.read`.
      parameters:
        - $ref: '#/components/parameters/Period'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/Source'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Sender'
        - $ref: '#/components/parameters/Phone'
        - $ref: '#/components/parameters/StatusFilter'
      responses:
        '200':
          description: Aggregate statistics.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sms/stats/daily:
    get:
      operationId: getDailyStats
      tags: [Statistics]
      summary: Get daily statistics
      description: |
        Day-by-day breakdown for charts, sorted by date ascending.
        With `granularity=auto` on a single-day range, buckets are hourly.
        Requires `sms.read`.
      parameters:
        - $ref: '#/components/parameters/Period'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/Source'
        - name: granularity
          in: query
          schema: { type: string, enum: [auto] }
          description: '`auto` switches to hourly buckets on a single-day range.'
        - name: countries
          in: query
          schema: { type: string }
          description: 'Comma-separated ISO codes: `US,GB,DE`. Changes sort to descending.'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Sender'
        - $ref: '#/components/parameters/Phone'
        - $ref: '#/components/parameters/StatusFilter'
      responses:
        '200':
          description: Daily statistics.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DailyStatsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sms/stats/by-country:
    get:
      operationId: getCountryStats
      tags: [Statistics]
      summary: Get country statistics
      description: Statistics grouped by destination country, sorted by total_sent descending. Requires `sms.read`.
      parameters:
        - $ref: '#/components/parameters/Period'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/Source'
      responses:
        '200':
          description: Per-country statistics.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CountryStatsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sms/messages:
    get:
      operationId: listMessages
      tags: [Messages]
      summary: List messages
      description: Paginated list of sent messages with filtering. Requires `sms.read`.
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StatusFilter'
        - $ref: '#/components/parameters/Phone'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Sender'
        - $ref: '#/components/parameters/Period'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: Paginated message list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessagesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sms/messages/unified:
    get:
      operationId: listUnifiedMessages
      tags: [Messages]
      summary: List unified messages
      description: |
        Unified message view combining API traffic and broadcast campaigns.
        Note the response schema differs from `GET /sms/messages`
        (`sent_at` instead of `created_at`, extra click/conversion fields).
        Requires `sms.read`.
      parameters:
        - name: source
          in: query
          schema: { type: string, enum: [api, broadcasts, all], default: all }
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StatusFilter'
        - $ref: '#/components/parameters/Phone'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Sender'
        - $ref: '#/components/parameters/Period'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: Paginated unified message list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnifiedMessagesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sms/messages/unified/export:
    get:
      operationId: exportUnifiedMessages
      tags: [Messages]
      summary: Export messages (CSV)
      description: |
        Streaming CSV export of unified messages. Same filters as the unified list.
        Body starts with a UTF-8 BOM (Excel-compatible). Row cap is advertised in
        the `X-Export-Limit` response header (default 1,000,000).
        Requires `sms.read`.
      parameters:
        - name: source
          in: query
          schema: { type: string, enum: [api, broadcasts, all], default: all }
        - $ref: '#/components/parameters/Period'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/StatusFilter'
        - $ref: '#/components/parameters/Phone'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Sender'
      responses:
        '200':
          description: CSV stream.
          headers:
            X-Export-Limit:
              description: Maximum rows included in this export.
              schema: { type: integer }
            Content-Disposition:
              schema: { type: string }
          content:
            text/csv:
              schema: { type: string }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /user/balance:
    get:
      operationId: getBalance
      tags: [Account]
      summary: Get balance
      description: |
        Current account balance, served from a background-refreshed cache.
        If the cache is degraded the response still returns HTTP 200 and sets
        the `X-Sms-Gateway-Status` / `X-Sms-Gateway-Reason` headers.
        Requires `balance.read`.
      responses:
        '200':
          description: Account balance.
          headers:
            X-Sms-Gateway-Status:
              description: Present only when data is degraded (`partial` or `unavailable`).
              schema: { type: string, enum: [partial, unavailable] }
            X-Sms-Gateway-Reason:
              description: Machine-readable degradation reason.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /user/pricing:
    get:
      operationId: getPricing
      tags: [Account]
      summary: Get pricing
      description: SMS rates per country and network. Requires `pricing.read`.
      responses:
        '200':
          description: Array of pricing entries.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PricingEntry'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /user/payments:
    get:
      operationId: getPayments
      tags: [Account]
      summary: Get payments
      description: |
        Transaction history merged from the SMS gateway and portal adjustments.
        Requires `payments.read`.
      responses:
        '200':
          description: Array of payments.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Payment'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /user/webhooks:
    get:
      operationId: getWebhookSettings
      tags: [Webhook settings]
      summary: Get webhook settings
      description: |
        Current webhook configuration for the workspace bound to your API key.
        The signing secret is never echoed — only `signing_secret_set: true|false`.
        Requires `webhooks.read`.
      responses:
        '200':
          description: Webhook settings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      operationId: updateWebhookSettings
      tags: [Webhook settings]
      summary: Update webhook settings
      description: |
        **Partial merge** — only the fields you send are touched.
        Send `null` or `""` for a URL to disable that channel.
        Requires `webhooks.write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSettingsUpdate'
            example:
              delivery_url: 'https://your-server.com/webhook/delivery'
              click_url: 'https://your-server.com/webhook/click'
              unique_clicks_only: true
              delivery_fields: [message_id, recipient, sender_id, status, status_code, num_parts, cost, timestamp]
      responses:
        '200':
          description: Settings updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSettings'
        '400':
          description: Typed validation error (INVALID_URL, INVALID_SECRET, INVALID_FIELDS, INVALID_TOGGLE, CONFLICTING_FIELDS, INVALID_BODY).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /user/webhooks/test:
    post:
      operationId: testWebhook
      tags: [Webhook settings]
      summary: Send test webhook
      description: Sends a sample payload to your configured URL. Requires `webhooks.write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum: [impression, click, conversion, delivery]
              required: [type]
            example: { type: delivery }
      responses:
        '200':
          description: Test dispatched.
        '401':
          $ref: '#/components/responses/Unauthorized'

  /user/webhooks/logs:
    get:
      operationId: getWebhookLogs
      tags: [Webhook settings]
      summary: Get webhook delivery logs
      description: Payload sent, response code, response time and error details per attempt. Requires `webhooks.read`.
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Delivery log entries.
        '401':
          $ref: '#/components/responses/Unauthorized'

webhooks:
  delivery:
    post:
      summary: Delivery report (DLR)
      description: |
        Sent to your `delivery_url` when a carrier returns a delivery report.
        Respond with HTTP 2xx within 5 seconds; otherwise we retry up to 3
        attempts total with 10s and 30s backoff.

        If a signing secret is configured, requests include
        `X-Webhook-Timestamp` and `X-Webhook-Signature: sha256={hex}` where
        `signature = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeliveryEvent'
      responses:
        '200':
          description: Acknowledge receipt with any 2xx.

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Create keys in the portal under **Settings → API Keys**.
        Live keys look like `sk_prod_...`; sandbox keys (`sk_test_...`)
        simulate sends for free with full response parity.
        Optional HMAC mode additionally requires `X-Timestamp` and
        `X-Signature: HMAC-SHA256(secret, "{METHOD}|{path}|{timestamp}|{body}")`.

  parameters:
    Period:
      name: period
      in: query
      schema:
        type: string
        enum: [today, yesterday, 7d, 7days, 30d, 30days, this_month, last_month]
        default: 7d
      description: Named period preset. Takes precedence over `from`/`to`.
    From:
      name: from
      in: query
      schema: { type: string, format: date }
      description: 'Start date `YYYY-MM-DD`. Use only when `period` is omitted.'
    To:
      name: to
      in: query
      schema: { type: string, format: date }
      description: 'End date `YYYY-MM-DD`, inclusive. Use only when `period` is omitted.'
    Source:
      name: source
      in: query
      schema: { type: string, enum: [api, broadcasts, all], default: api }
      description: Traffic source filter.
    Country:
      name: country
      in: query
      schema: { type: string }
      description: 'ISO 3166-1 alpha-2 code: `US`, `GB`, …'
    Sender:
      name: sender
      in: query
      schema: { type: string }
      description: Filter by sender ID.
    Phone:
      name: phone
      in: query
      schema: { type: string }
      description: Search by phone number (substring).
    StatusFilter:
      name: status
      in: query
      schema:
        type: string
        enum: [delivered, undelivered, pending, expired, sent, failed, unknown]
      description: Delivery status filter.
    Page:
      name: page
      in: query
      schema: { type: integer, default: 1, minimum: 1 }
    Limit:
      name: limit
      in: query
      schema: { type: integer, default: 25, maximum: 100 }

  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            status: false
            error_code: INVALID_API_KEY
            description: API key not found
    Forbidden:
      description: Authenticated but not allowed (permission, route or workspace).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ValidationError:
      description: Request validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            status: false
            error_code: INVALID_TO
            description: Missing or empty 'to' array
    RateLimited:
      description: Rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            status: false
            error_code: RATE_LIMIT_EXCEEDED
            description: Too many requests

  schemas:
    SendSmsRequest:
      type: object
      required: [to, message, sender_id]
      properties:
        to:
          type: array
          items: { type: string }
          minItems: 1
          maxItems: 100
          description: Phone numbers in E.164 format.
        message:
          type: string
          description: Message text. Encoding (GSM-7/UCS-2) is detected automatically.
        sender_id:
          type: string
          maxLength: 11
          description: Alphanumeric sender ID (max 11 chars) or phone number.

    SendSmsResponse:
      type: object
      properties:
        status: { type: boolean }
        sandbox:
          type: boolean
          description: Present and true when the request used a sandbox (sk_test_) key — the send was simulated.
        messages:
          type: array
          description: Accepted messages only (backward-compatible array).
          items:
            type: object
            properties:
              dnis: { type: string }
              message_id: { type: string }
              segment_num: { type: integer }
        results:
          type: array
          description: All recipients with individual status.
          items:
            type: object
            properties:
              dnis: { type: string }
              message_id: { type: string }
              segments: { type: integer }
              status:
                type: string
                enum: [accepted, blocked_country, queue_error, config_error, db_error]
        summary:
          type: object
          properties:
            total_recipients: { type: integer }
            total_segments: { type: integer }
            total_cost: { type: number }
            encoding: { type: string, enum: [GSM-7, UCS-2] }
            accepted_count: { type: integer }
            blocked_count: { type: integer }
            queue_error_count: { type: integer }
            config_error_count: { type: integer }
            db_error_count: { type: integer }

    MessageStatusResponse:
      type: object
      properties:
        status: { type: boolean }
        message:
          type: object
          properties:
            message_id: { type: string }
            telecom_message_id: { type: string }
            recipient: { type: string }
            segments: { type: integer }
            message: { type: string }
            sender_id: { type: string }
            status:
              type: string
              enum: [pending, sent, DELIVRD, UNDELIV, REJECTD, EXPIRED, UNKNOWN, failed]
            status_code: { type: string }
            cost: { type: number }
            created_at: { type: string, format: date-time }
            delivered_at: { type: string, format: date-time }

    StatsResponse:
      type: object
      properties:
        status: { type: boolean }
        stats:
          type: object
          properties:
            total_sent: { type: integer }
            sent: { type: integer }
            delivered: { type: integer }
            undelivered: { type: integer }
            pending: { type: integer }
            expired: { type: integer }
            not_billed: { type: integer }
            failed: { type: integer, description: Alias for undelivered. }
            click_count: { type: integer }
            unique_click_count: { type: integer }
            action_count: { type: integer }
            sent_rate: { type: number }
            delivery_rate: { type: number }
            total_cost: { type: number }
            avg_cost_per_msg: { type: number }
            currency: { type: string }
        period:
          $ref: '#/components/schemas/PeriodRange'

    DailyStatsResponse:
      type: object
      properties:
        status: { type: boolean }
        daily_stats:
          type: array
          items:
            type: object
            properties:
              date: { type: string, description: '`YYYY-MM-DD`, or `YYYY-MM-DDTHH:00` for hourly buckets.' }
              sent: { type: integer }
              delivered: { type: integer }
              undelivered: { type: integer }
              pending: { type: integer }
              expired: { type: integer }
              not_billed: { type: integer }
              failed: { type: integer }
              click_count: { type: integer }
              cost: { type: number }
              currency: { type: string }
        period:
          $ref: '#/components/schemas/PeriodRange'

    CountryStatsResponse:
      type: object
      properties:
        status: { type: boolean }
        countries:
          type: array
          items:
            type: object
            properties:
              country: { type: string }
              country_code: { type: string }
              country_flag: { type: string }
              total_sent: { type: integer }
              sent: { type: integer }
              delivered: { type: integer }
              undelivered: { type: integer }
              pending: { type: integer }
              expired: { type: integer }
              not_billed: { type: integer }
              failed: { type: integer }
              click_count: { type: integer }
              delivery_rate: { type: number }
              cost: { type: number }
              currency: { type: string }

    MessagesResponse:
      type: object
      properties:
        messages:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              client_message_id: { type: string }
              recipient: { type: string }
              sender_id: { type: string }
              message: { type: string }
              status: { type: string }
              status_code: { type: string }
              segments: { type: integer }
              cost: { type: number }
              created_at: { type: string, format: date-time }
              delivered_at: { type: string, format: date-time }
        total: { type: integer }
        page: { type: integer }
        limit: { type: integer }
        total_pages: { type: integer }

    UnifiedMessagesResponse:
      type: object
      properties:
        messages:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              source: { type: string, enum: [api, campaign] }
              recipient: { type: string }
              sender_id: { type: string }
              message: { type: string }
              status: { type: string }
              status_code: { type: string }
              cost: { type: number }
              sent_at: { type: string, format: date-time }
              delivered_at: { type: string, format: date-time, nullable: true }
              clicked: { type: boolean }
              clicked_at: { type: string, format: date-time, nullable: true }
              action_type: { type: string, nullable: true }
              action_at: { type: string, format: date-time, nullable: true }
              message_id: { type: string, description: Carrier-assigned UUID. }
              client_message_id: { type: string, nullable: true }
              segments: { type: integer }
              broadcast_id: { type: integer, nullable: true }
              broadcast_name: { type: string }
        total: { type: integer }
        page: { type: integer }
        limit: { type: integer }
        total_pages: { type: integer }

    BalanceResponse:
      type: object
      properties:
        balance: { type: number }
        currency: { type: string }
        updated: { type: string, description: 'Format: `YYYY.MM.DD HH:MM:SS`' }
        credit_blocked:
          type: boolean
          description: Present and true when balance is zero/negative and sending is blocked.

    PricingEntry:
      type: object
      properties:
        mcc: { type: string }
        mnc: { type: string }
        mccmnc: { type: string }
        dialcode: { type: string }
        country: { type: string }
        network: { type: string }
        rate: { type: number }
        prev_rate: { type: number }
        rate_start_date: { type: string }
        rate_end_date: { type: string }
        change_type: { type: string, enum: [Same, Increase, Decrease] }

    Payment:
      type: object
      properties:
        id:
          type: string
          description: '`sms_gateway_S{id}` for gateway payments, `portal_P{id}` for portal adjustments.'
        amount: { type: number }
        currency: { type: string }
        date: { type: string, format: date-time }
        type: { type: string, enum: [deposit, usage, adjustment] }
        description: { type: string }
        source: { type: string, enum: [sms_gateway, portal] }

    WebhookSettings:
      type: object
      properties:
        impression_url: { type: string, nullable: true }
        click_url: { type: string, nullable: true }
        conversion_url: { type: string, nullable: true }
        delivery_url: { type: string, nullable: true }
        unique_clicks_only: { type: boolean }
        signing_secret_set: { type: boolean }
        impression_fields: { type: array, items: { type: string } }
        click_fields: { type: array, items: { type: string } }
        conversion_fields: { type: array, items: { type: string } }
        delivery_fields: { type: array, items: { type: string } }

    WebhookSettingsUpdate:
      type: object
      description: Partial merge — only sent fields are touched.
      properties:
        impression_url: { type: string, nullable: true, description: '`null` or `""` disables the channel.' }
        click_url: { type: string, nullable: true }
        conversion_url: { type: string, nullable: true }
        delivery_url: { type: string, nullable: true }
        unique_clicks_only: { type: boolean }
        signing_secret:
          type: string
          maxLength: 64
          description: Write-only. Omit or send null to preserve the existing secret.
        clear_signing_secret:
          type: boolean
          description: Set true to remove the signing secret. Conflicts with `signing_secret`.
        impression_fields: { type: array, items: { type: string } }
        click_fields: { type: array, items: { type: string } }
        conversion_fields: { type: array, items: { type: string } }
        delivery_fields: { type: array, items: { type: string } }

    DeliveryEvent:
      type: object
      properties:
        message_id: { type: string }
        recipient: { type: string }
        sender_id: { type: string }
        status: { type: string, enum: [DELIVRD, UNDELIV, REJECTD, EXPIRED, UNKNOWN] }
        status_code: { type: string }
        num_parts: { type: integer }
        cost: { type: number }
        timestamp: { type: string, format: date-time }
      example:
        message_id: api_42_1743667200123456789_a3f8b2c1d9e45f67
        recipient: '+14155551234'
        sender_id: MyApp
        status: DELIVRD
        status_code: '000'
        num_parts: 1
        cost: 0.0085
        timestamp: '2026-02-13T10:30:04Z'

    PeriodRange:
      type: object
      properties:
        from: { type: string, format: date }
        to: { type: string, format: date }

    Error:
      type: object
      properties:
        status: { type: boolean, const: false }
        error_code: { type: string }
        description: { type: string }

    InsufficientBalanceError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            balance: { type: number }
            estimated_cost: { type: number }
            currency: { type: string }
            total_recipients: { type: integer }
            billable_recipients: { type: integer }
            segments: { type: integer }
