openapi: 3.0.3
info:
  title: Sendai SMS API
  description: >-
    Send SMS to one recipient or many, read back what happened to each message, and manage the API
    keys that authenticate it.


    **New to Sendai?** The narrative guides live beside this reference — start with the
    [Quickstart](/guides/quickstart), then [Authentication](/guides/authentication) and
    [Bulk SMS](/guides/bulk-sms).


    **Two response conventions.** `POST /api/v1/sms` returns the message object **bare**, with no
    wrapper. Every other endpoint wraps its payload in `{ status, message, data }`. The examples
    below show exactly what each endpoint returns — check them before writing your client.
  contact:
    email: info@sendai.co.zw
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: 1.0.0
servers:
  - url: https://api.sendai.co.zw
    description: production
  - url: https://staging-api.sendai.co.zw
    description: staging
security:
  - bearer: []
tags:
  - name: sms
    description: "Send an SMS to one recipient or many, and read back its state. A message reaching
      `status: success` with a non-null `delivered_at` is one the carrier confirmed to the handset."
  - name: api-tokens
    description: Create, list, and revoke the API keys that authenticate every request. The key secret
      is returned once, when you create it, and never again.
paths:
  /api/v1/sms:
    post:
      operationId: v1.sms.create
      tags:
        - sms
      summary: Send a single SMS
      description: "Sends one message and returns it **without the `{ status, message, data }` envelope**
        — the message object is the whole body. Responds `202`: accepted, priced, and queued, not
        delivered."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendSmsRequest"
            example:
              to: "263771000000"
              from: Sendai SMS
              message: Your code is 4821
      responses:
        "202":
          description: Accepted and queued. **Bare object — there is no `data` wrapper to unwrap.**
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Sms"
              example:
                id: 0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10
                to: "263771000000"
                message: Your code is 4821
                sender_id: Sendai SMS
                operator: econet
                charge: "450"
                charge_currency: USD
                operator_response: ""
                received_at: 2026-07-06T09:12:03Z
                status: enqueued
                created_at: 2026-07-06T09:12:03Z
                delivered_at: null
        "400":
          description: Malformed JSON body, or a `from` that is not an approved sender id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyError"
              example:
                status: ""
                message: unknown or unapproved sender id
                error:
                  type: ERR_BAD_REQUEST
                  description: unknown or unapproved sender id
        "401":
          description: No API key, or a key that is malformed or revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: missing authentication
                error:
                  code: UNAUTHORIZED
                  description: missing authentication
  /api/v1/sms/{id}:
    get:
      operationId: v1.sms.get
      tags:
        - sms
      summary: Get an SMS
      description: Reads back one message. Unlike the send endpoint, this response **is** enveloped — the
        message sits in `data`.
      parameters:
        - name: id
          in: path
          required: true
          description: The `id` from the send response.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The message and its current state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SmsEnvelope"
              example:
                status: success
                message: request successful
                data:
                  id: 0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10
                  to: "263771000000"
                  message: Your code is 4821
                  sender_id: Sendai SMS
                  operator: econet
                  charge: "450"
                  charge_currency: USD
                  operator_response: DELIVRD
                  received_at: 2026-07-06T09:12:03Z
                  status: success
                  created_at: 2026-07-06T09:12:03Z
                  delivered_at: 2026-07-06T09:12:44Z
        "400":
          description: "`id` is not a valid UUID."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyError"
              example:
                status: ""
                message: invalid sms id
                error:
                  type: ERR_BAD_REQUEST
                  description: invalid sms id
        "401":
          description: No API key, or a key that is malformed or revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: missing authentication
                error:
                  code: UNAUTHORIZED
                  description: missing authentication
        "404":
          description: No such message on your account — unknown id, another account's message, or not an SMS.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyError"
              example:
                status: ""
                message: not found
                error:
                  type: err_not_found
                  description: the resource does not exist
  /api/v1/sms/bulk:
    post:
      operationId: v1.sms.bulk
      tags:
        - sms
      summary: Send an SMS to many recipients
      description: Sends the same body to every recipient in `to`, priced as one send. Returns one entry
        per recipient, in request order. This response **is** enveloped.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendBulkSmsRequest"
            example:
              to:
                - "263771000000"
                - "263772000001"
              from: Sendai SMS
              message: Polls close at 18:00. Thanks for taking part!
              webhook_url: https://example.com/webhooks/dlr
      responses:
        "202":
          description: Every recipient accepted and priced.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkSmsEnvelope"
              example:
                status: success
                message: bulk sms enqueued
                data:
                  total: 2
                  charge: 900
                  currency: USD
                  items:
                    - id: 0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10
                      to: "263771000000"
                      status: enqueued
                      operator: econet
                      charge: 450
                      charge_currency: USD
                    - id: 7e5f2a8b-4c10-4a1b-9c3d-0f9c1b3a6d2e
                      to: "263772000001"
                      status: enqueued
                      operator: netone
                      charge: 450
                      charge_currency: USD
        "400":
          description: A required field is missing or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: Validation failed
                error:
                  code: VALIDATION_ERROR
                  description: One or more fields failed validation
                  validation_errors:
                    - field: from
                      description: cannot be blank
        "401":
          description: No API key, or a key that is malformed or revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: missing authentication
                error:
                  code: UNAUTHORIZED
                  description: missing authentication
        "422":
          description: Your prepaid balance does not cover the send.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: insufficient balance
                error:
                  code: UNPROCESSABLE_ENTITY
                  description: insufficient balance
  /api/v1/api-tokens:
    post:
      operationId: v1.apiTokens.create
      tags:
        - api-tokens
      summary: Create an API key
      description: Creates a key. `data.token` is the secret and is returned **only here** — Sendai stores
        a hash.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiTokenRequest"
            example:
              name: orders-service
      responses:
        "201":
          description: Key created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiTokenCreatedEnvelope"
              example:
                status: success
                message: api token created — save this token, it will not be shown again
                data:
                  id: 9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b
                  name: orders-service
                  token: sk_live_4c8e6b21d0f34a97b5e8c1d2f6a3b904
                  account_id: 2d6f4b8e-1a90-4e21-8b7d-3f1a5c2e9a0c
                  created_at: 2026-07-06T09:12:03Z
        "400":
          description: "`name` is missing."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: Validation failed
                error:
                  code: VALIDATION_ERROR
                  description: One or more fields failed validation
                  validation_errors:
                    - field: name
                      description: cannot be blank
        "401":
          description: No API key, or a key that is malformed or revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: missing authentication
                error:
                  code: UNAUTHORIZED
                  description: missing authentication
    get:
      operationId: v1.apiTokens.list
      tags:
        - api-tokens
      summary: List API keys
      description: "Lists the keys on your account. **This response is not normalised**: it serialises the
        internal record directly, so field names are PascalCase rather than snake_case and include
        storage columns. Read `ID`, `Name`, `Active`, `CreatedAt` and `LastUsedAt`; treat the rest
        as unstable."
      responses:
        "200":
          description: Keys on the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiTokenListEnvelope"
              example:
                status: success
                message: ""
                data:
                  - ID: 9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b
                    CreatedAt: 2026-07-06T09:12:03Z
                    UpdatedAt: null
                    DeletedAt: null
                    Name: orders-service
                    TokenHash: 9b74c9897bac770ffc029102a200c5de0d3d2a2f4b2b6c9a2f0b4b2c1d8e3f7a
                    AccountId: 2d6f4b8e-1a90-4e21-8b7d-3f1a5c2e9a0c
                    Active: true
                    LastUsedAt: 2026-08-06T11:04:19Z
        "401":
          description: No API key, or a key that is malformed or revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: missing authentication
                error:
                  code: UNAUTHORIZED
                  description: missing authentication
  /api/v1/api-tokens/{id}:
    delete:
      operationId: v1.apiTokens.revoke
      tags:
        - api-tokens
      summary: Revoke an API key
      description: Revokes immediately. The next request using that key gets a `401`.
      parameters:
        - name: id
          in: path
          required: true
          description: The key's `ID`.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Key revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageOnlyEnvelope"
              example:
                status: success
                message: api token revoked
        "400":
          description: "`id` is not a valid UUID."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: invalid token id
                error:
                  code: BAD_REQUEST
                  description: invalid token id
        "401":
          description: No API key, or a key that is malformed or revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KitError"
              example:
                status: error
                message: missing authentication
                error:
                  code: UNAUTHORIZED
                  description: missing authentication
components:
  schemas:
    SendSmsRequest:
      type: object
      required:
        - to
        - message
        - from
      properties:
        to:
          type: string
          description: Recipient MSISDN in international format.
          example: "263771000000"
        message:
          type: string
          description: Message body. Billed per segment.
          example: Your code is 4821
        from:
          type: string
          description: An approved sender id on your account. An unapproved value is rejected with `400`.
          example: Sendai SMS
    SendBulkSmsRequest:
      type: object
      required:
        - to
        - message
        - from
      properties:
        to:
          type: array
          minItems: 1
          description: Recipient MSISDNs. At least one.
          items:
            type: string
          example:
            - "263771000000"
            - "263772000001"
        message:
          type: string
          description: Body sent to every recipient.
          example: Polls close at 18:00. Thanks for taking part!
        from:
          type: string
          description: An approved sender id on your account.
          example: Sendai SMS
        webhook_url:
          type: string
          description: Optional callback applied to every message in the send. Must be a valid URL.
          example: https://example.com/webhooks/dlr
          format: uri
    CreateApiTokenRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Identifies the key. Name it for the service that will use it.
          example: orders-service
    Sms:
      type: object
      properties:
        id:
          type: string
          description: Message id. Pass it to `GET /api/v1/sms/{id}`.
          example: 0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10
          format: uuid
        to:
          type: string
          description: Recipient MSISDN in international format.
          example: "263771000000"
        message:
          type: string
          description: The body as sent.
          example: Your code is 4821
        sender_id:
          type: string
          description: Display name of the approved sender the recipient saw.
          example: Sendai SMS
        operator:
          type: string
          description: Carrier derived from the recipient MSISDN.
          example: econet
          enum:
            - econet
            - netone
            - telecel
        charge:
          type: string
          description: Amount billed, in ten-thousandths of `charge_currency` (`10000` == 1.00, so `450` ==
            0.045). Serialised as a **string** on this endpoint — the bulk endpoint returns the same
            value as an integer.
          example: "450"
        charge_currency:
          type: string
          description: Currency of `charge`.
          example: USD
        operator_response:
          type: string
          description: Raw response from the carrier. Empty until dispatched.
          example: ""
        received_at:
          type: string
          description: When Sendai accepted the message.
          example: 2026-07-06T09:12:03Z
          format: date-time
        status:
          type: string
          description: Lifecycle state. `success` means the carrier accepted or confirmed it — check
            `delivered_at` for confirmed handset delivery. There is no separate delivery-status
            field.
          enum:
            - created
            - enqueued
            - processing
            - success
            - failed
          example: enqueued
        created_at:
          type: string
          description: When the message row was created.
          example: 2026-07-06T09:12:03Z
          format: date-time
        delivered_at:
          type: string
          format: date-time
          nullable: true
          description: "`null` until the carrier confirms delivery, then an RFC 3339 timestamp. This is the
            only delivery signal."
          example: null
    SmsEnvelope:
      type: object
      properties:
        status:
          type: string
          description: ""
          example: success
          enum:
            - success
        message:
          type: string
          description: ""
          example: request successful
        data:
          $ref: "#/components/schemas/Sms"
    BulkSmsEnvelope:
      type: object
      properties:
        status:
          type: string
          description: ""
          example: success
          enum:
            - success
        message:
          type: string
          description: ""
          example: bulk sms enqueued
        data:
          type: object
          properties:
            total:
              type: integer
              description: Recipients accepted.
              example: 2
            charge:
              type: integer
              format: int64
              description: Total held for the send, in ten-thousandths. An **integer** here, unlike the
                single-send endpoint.
              example: 900
            currency:
              type: string
              description: Currency of `charge`.
              example: USD
            items:
              type: array
              description: One entry per recipient, in request order.
              items:
                type: object
                properties:
                  id:
                    type: string
                    description: Message id for this recipient.
                    example: 0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10
                    format: uuid
                  to:
                    type: string
                    description: ""
                    example: "263771000000"
                  status:
                    type: string
                    description: ""
                    example: enqueued
                  operator:
                    type: string
                    description: ""
                    example: econet
                  charge:
                    type: integer
                    format: int64
                    description: Held for this recipient. Operators price independently, so entries can differ.
                    example: 450
                  charge_currency:
                    type: string
                    description: ""
                    example: USD
    ApiTokenCreatedEnvelope:
      type: object
      properties:
        status:
          type: string
          description: ""
          example: success
          enum:
            - success
        message:
          type: string
          description: ""
          example: api token created — save this token, it will not be shown again
        data:
          type: object
          properties:
            id:
              type: string
              description: Use this to revoke the key.
              example: 9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b
              format: uuid
            name:
              type: string
              description: ""
              example: orders-service
            token:
              type: string
              description: The key secret. **Returned only here.**
              example: sk_live_4c8e6b21d0f34a97b5e8c1d2f6a3b904
            account_id:
              type: string
              description: Account the key authenticates as.
              example: 2d6f4b8e-1a90-4e21-8b7d-3f1a5c2e9a0c
              format: uuid
            created_at:
              type: string
              description: ""
              example: 2026-07-06T09:12:03Z
              format: date-time
    ApiTokenListEnvelope:
      type: object
      properties:
        status:
          type: string
          description: ""
          example: success
          enum:
            - success
        message:
          type: string
          description: Empty on this endpoint.
          example: ""
        data:
          type: array
          items:
            type: object
            description: Internal key record, serialised as-is — hence the PascalCase field names.
            properties:
              ID:
                type: string
                description: Key id. Pass to `DELETE /api/v1/api-tokens/{id}`.
                example: 9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b
                format: uuid
              CreatedAt:
                type: string
                description: ""
                example: 2026-07-06T09:12:03Z
                format: date-time
              UpdatedAt:
                type: string
                format: date-time
                nullable: true
                example: null
              DeletedAt:
                type: string
                format: date-time
                nullable: true
                example: null
              Name:
                type: string
                description: ""
                example: orders-service
              TokenHash:
                type: string
                description: SHA-256 hash of the key. Internal — do not build against it.
                example: 9b74c988…
              AccountId:
                type: string
                description: ""
                example: 2d6f4b8e-1a90-4e21-8b7d-3f1a5c2e9a0c
                format: uuid
              Active:
                type: boolean
                description: "`false` once revoked."
                example: true
              LastUsedAt:
                type: string
                format: date-time
                nullable: true
                description: "`null` if never used."
                example: 2026-08-06T11:04:19Z
    MessageOnlyEnvelope:
      type: object
      properties:
        status:
          type: string
          description: ""
          example: success
          enum:
            - success
        message:
          type: string
          description: ""
          example: api token revoked
    LegacyError:
      type: object
      description: Error shape used by `POST /api/v1/sms` and `GET /api/v1/sms/{id}` (v0.2-derived handlers).
      properties:
        status:
          type: string
          description: Empty on legacy error responses.
          example: ""
        message:
          type: string
          description: Human-readable summary.
          example: not found
        error:
          type: object
          properties:
            type:
              type: string
              description: "Machine-readable error type. Note: `type`, not `code`."
              example: err_not_found
            description:
              type: string
              description: What went wrong.
              example: the resource does not exist
            validation_errors:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                    description: Field that failed.
                    example: from
                  description:
                    type: string
                    description: Why.
                    example: cannot be blank
    KitError:
      type: object
      description: Error shape used by `POST /api/v1/sms/bulk`, the API-key endpoints, and the auth middleware.
      properties:
        status:
          type: string
          description: Always `error`.
          example: error
          enum:
            - error
        message:
          type: string
          description: Human-readable summary.
          example: missing authentication
        error:
          type: object
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: UNAUTHORIZED
            description:
              type: string
              description: What went wrong.
              example: missing authentication
            validation_errors:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                    description: Field that failed.
                    example: from
                  description:
                    type: string
                    description: Why.
                    example: cannot be blank
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: "Your API key, sent as `Authorization: Bearer <key>`."
