> ## Documentation Index
> Fetch the complete documentation index at: https://docs.robase.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> For chatbots and coding agents, start at /guides/ai-agents.
> Hosted MCP: POST https://api.robase.dev/mcp (Streamable HTTP). Auth: Authorization: Bearer robe_… or robe_agent_….
> Phase 1 tools: get_pricing, send_otp, verify_otp, get_otp, send_sms, get_sms. No payment or top-up tools.
> Call get_pricing; treat listed:true as marketed markets (NG, GH, KE, ZA, GB, BJ, CI, US). Do not invent countries or delivery percentages.
> Prefer OTP-shaped tests such as "Your Robase test OTP is 123456". Do not send Hello or Good morning as tests. Match errors on error.type.

# Send an SMS

> POST /v1/sms/send — transactional message, same routing and credits as OTP.



## OpenAPI

````yaml openapi.yaml POST /v1/sms/send
openapi: 3.0.3
info:
  title: Robase SMS OTP API
  description: >
    Multi-provider SMS OTP delivery and transactional SMS API with automatic

    failover and per-country routing. Send and verify one-time passwords across

    Africa and globally.


    ## Authentication


    Every endpoint under `/v1` requires an API key issued in the dashboard,

    passed as a bearer token:


    ```

    Authorization: Bearer robe_your_api_key_here

    ```


    ## Idempotency


    `POST` endpoints accept an `Idempotency-Key` header. Replaying the same key

    within 24 hours returns the original response instead of sending a second

    message — so a request that times out mid-flight can be retried safely

    without double-charging credits. The official SDKs generate a key for every

    `POST` automatically.


    ## Rate limits


    Requests are limited per client IP (100/minute) and per destination phone

    number (3 OTP sends / 10 minutes, 10 OTP verifications / 10 minutes,

    10 transactional SMS / minute). Exceeding a limit returns `429` with a

    `Retry-After` header.


    ## Errors


    Failures return a consistent envelope:


    ```json

    {"error": {"type": "insufficient_credits", "message": "insufficient credit
    balance"}}

    ```


    Match on `error.type` — it is stable — rather than on `error.message`, which

    is prose and may change.


    ## Languages


    `error.message` is written in your workspace's default language. Send an

    `Accept-Language` header (`en` or `fr`) to override that for one request —

    it changes only the prose, never `error.type`.


    `Accept-Language` does not affect the text of an SMS. The OTP message body

    follows the workspace's language setting, or the `language` field on

    `POST /v1/otp/send`. A header describing which language *you* read should

    not decide what your end user receives.


    ## Webhooks


    Dashboard-configured HTTPS callbacks. The signed body is compact JSON

    (no pretty-print, no HTML escaping of `<>&`). HMAC-SHA256 is over those

    exact bytes. Envelope and `data` objects are `WebhookEnvelope`,

    `OTPWebhookData`, `SMSWebhookData`, and `CreditWebhookData`.
  version: 1.0.0
  contact:
    name: Robase Support
    url: https://robase.dev
servers:
  - url: https://api.robase.dev
    description: Production
  - url: http://localhost:8080
    description: Local development
security: []
tags:
  - name: OTP
    description: Generate, deliver and verify one-time passwords.
  - name: SMS
    description: Send arbitrary transactional messages.
  - name: System
    description: Unauthenticated operational endpoints.
paths:
  /v1/sms/send:
    post:
      tags:
        - SMS
      summary: Send a transactional SMS
      description: >
        Sends an arbitrary SMS message. Uses the same routing, credit billing,

        and provider failover as OTPs.


        The response returns once the message is persisted and charged —

        delivery is asynchronous, so `status` is always `pending`. Subscribe to

        the `sms.delivered` / `sms.failed` / `sms.blocked` webhooks, or poll

        `GET /v1/sms/{id}`, to learn the outcome.


        Workspaces with anti-spam enabled have message bodies classified after

        the response, in the worker that dispatches the message — no send waits

        on a classifier. Depending on the configured action, a flagged message
        is

        either tagged in `metadata` and delivered, or stopped before it reaches
        a

        provider: its status becomes `blocked`, the credit is refunded in full,

        and the `sms.blocked` webhook fires with the classifier's verdict.
      operationId: sendSMS
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendSMSRequest'
      responses:
        '200':
          description: SMS created, charged, and queued for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendSMSResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/SendForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ScreeningUnavailable'
      security:
        - BearerAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: |
        Opaque client-generated key. Replaying the same key within 24 hours
        returns the original response (with `Idempotent-Replayed: true`)
        instead of performing the action again. Recommended for every send so
        a network-level retry cannot double-charge. The same key with a
        different endpoint or body returns `422 idempotency_key_reused`; a
        repeat while the first request is still running returns
        `409 request_in_progress`. `5xx` and `429` responses are not kept, so
        a retry with the same key runs again.
      example: 550e8400-e29b-41d4-a716-446655440000
  schemas:
    SendSMSRequest:
      type: object
      required:
        - phone_number
        - message
      properties:
        phone_number:
          type: string
          description: Phone number in E.164 format
          example: '+2348012345678'
        message:
          type: string
          description: |
            SMS message body. The limit is six SMS segments, not a character
            count: a carrier splits a long message into parts and bills each
            one, and how many characters fit in a part depends on the alphabet
            the message needs — 160 per part in GSM-7 (153 once it is split),
            but 70 (then 67) as soon as one character forces UCS-2. So the
            practical ceiling is 918 plain-Latin characters, or 402 in a script
            that needs UCS-2.
          example: 'Your order #12345 has been shipped.'
        metadata:
          type: object
          additionalProperties: true
          description: Arbitrary key-value data attached to the message
    SendSMSResponse:
      type: object
      required:
        - id
        - phone_number
        - country_code
        - credit_cost
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: SMS message ID
        phone_number:
          type: string
          description: Phone number the SMS was sent to
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code
        credit_cost:
          type: integer
          description: |
            Credits consumed: the country's rate multiplied by `segments`, since
            a carrier bills each part of a split message as its own send.
        segments:
          type: integer
          description: |
            How many SMS parts the message was split into, and therefore the
            multiple of the country's rate it was charged at.
          example: 1
        encoding:
          type: string
          enum:
            - gsm7
            - ucs2
          description: |
            The alphabet the message needed, which is what decided the part
            size: 160 characters per part in `gsm7`, 70 in `ucs2`. A single
            character outside the GSM alphabet moves the whole message to
            `ucs2`.
        sanitized:
          type: boolean
          description: |
            Whether the body was rewritten before it was measured. With the
            workspace's `sanitize_symbols` setting on (the default), characters
            GSM-7 cannot carry are replaced with plain equivalents — `₦`
            becomes `NGN`, curly quotes straighten — and anything with no
            equivalent, such as an emoji, is removed. Letters are never
            removed, and the rewrite is dropped whenever it would not lower
            the part count. Turn the setting off to have bodies delivered
            exactly as sent.
        status:
          type: string
          enum:
            - pending
          description: |
            Initial status — always `pending`; transitions to
            sent/delivered/failed asynchronously, or to `blocked` if anti-spam
            screening stops it before dispatch (the credit is then refunded).
        created_at:
          type: string
          format: date-time
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - type
            - message
          properties:
            type:
              type: string
              description: |
                Stable machine-readable identifier. Match on this rather than on
                `message`.
              enum:
                - validation_error
                - invalid_phone
                - country_not_supported
                - unauthorized
                - insufficient_credits
                - kyc_required
                - account_temporarily_restricted
                - otp_not_found
                - sms_not_found
                - insufficient_scope
                - country_not_enabled
                - prefix_blocked
                - content_blocked
                - screening_unavailable
                - idempotency_key_reused
                - request_in_progress
                - otp_expired
                - otp_already_verified
                - max_attempts_exceeded
                - rate_limited
                - internal_error
                - whatsapp_not_available
            message:
              type: string
              description: Human-readable description. Not stable — do not parse.
            required_scope:
              type: string
              description: With `insufficient_scope`, the scope the API key is missing.
      example:
        error:
          type: insufficient_credits
          message: insufficient credit balance
  responses:
    BadRequest:
      description: |
        Invalid request. `error.type` is `validation_error` (malformed body or a
        missing required field), `invalid_phone` (not valid E.164), or
        `country_not_supported` (no SMS route to that destination).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: invalid_phone
              message: phone must be in E.164 format (e.g., +2348012345678)
    Unauthorized:
      description: The API key is missing, malformed, or unknown.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: unauthorized
              message: invalid API key
    PaymentRequired:
      description: The workspace credit balance is below the cost of this send.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: insufficient_credits
              message: insufficient credit balance
    SendForbidden:
      description: |
        Sending is refused. `error.type` is `kyc_required` (business
        verification hold — KYB / financial-alert path, separate from
        antispam), `account_temporarily_restricted` (48-hour antispam
        warning ban), `insufficient_scope` (the API key lacks the send
        scope), `prefix_blocked` (sending to this number range is blocked
        for the workspace after unusual traffic), `country_not_enabled`
        (the destination country is not turned on in Settings → Countries;
        returned only where geo permissions are enforced) or
        `content_blocked` (`/v1/sms/send` only: a new workspace's message
        read as non-transactional in the synchronous content check; a
        workspace is new until it is old enough, has enough message history
        and has enough SMS already sent, and again after a recent spam block).
        Nothing is charged in any case.

        A warning ban auto-lifts when `warning_banned_until` expires.
        Retrying before then will not clear it. Match on `error.type`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            kyc_required:
              value:
                error:
                  type: kyc_required
                  message: sending is paused pending business verification
            account_temporarily_restricted:
              value:
                error:
                  type: account_temporarily_restricted
                  message: >-
                    sending is paused on this workspace until the temporary
                    restriction ends
            insufficient_scope:
              value:
                error:
                  type: insufficient_scope
                  message: this API key does not have the otp:send scope
                  required_scope: otp:send
            prefix_blocked:
              value:
                error:
                  type: prefix_blocked
                  message: >-
                    sending to this number range is blocked for this workspace
                    after unusual traffic; contact support
            content_blocked:
              value:
                error:
                  type: content_blocked
                  message: >-
                    message blocked: content is not transactional; nothing
                    charged
            country_not_enabled:
              value:
                error:
                  type: country_not_enabled
                  message: >-
                    sending to GH is not enabled for this workspace; turn it on
                    in Settings → Countries
    RateLimited:
      description: |
        A rate limit was exceeded: per API key (600 sends or 1200 reads a
        minute by default), per workspace (3000 a minute by default), per
        destination prefix (300 OTPs an hour by default), per destination
        phone, or per client IP. Limits are counted per workspace, so another
        workspace's traffic never uses up yours. Retry after `Retry-After`.
      headers:
        Retry-After:
          description: >-
            Whole seconds until the counter that refused the request resets (at
            least 1).
          schema:
            type: integer
            minimum: 1
          example: 42
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: rate_limited
              message: too many send attempts, try again later
    InternalError:
      description: >-
        An unexpected server-side failure. Safe to retry with the same
        `Idempotency-Key`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: internal_error
              message: an unexpected error occurred
    ScreeningUnavailable:
      description: |
        The synchronous content check could not reach a verdict. A new
        workspace's `/v1/sms/send` request is screened before it is charged
        or queued; when the check fails, the message is not sent. Nothing is
        charged. Retry after `Retry-After` with the same `Idempotency-Key`.
      headers:
        Retry-After:
          description: Whole seconds to wait before retrying.
          schema:
            type: integer
            minimum: 1
          example: 5
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: screening_unavailable
              message: content check unavailable; retry shortly, nothing charged
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: >-
        Your Robase API key. Starts with robe_. Include as Authorization: Bearer
        robe_...

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.