robase-dev SMS API

Send arbitrary transactional messages.

Operations 3

POST /v1/sms/send Send a transactional SMS #
GET /v1/sms/{id} Get SMS status #
GET /v1/pricing Get the price list #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/robase-dev:robase-dev-sms-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

robase-dev-sms-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Robase OTP SMS 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
tags:
- name: SMS
  description: Send arbitrary transactional messages.
paths:
  /v1/sms/send:
    post:
      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
      tags:
      - SMS
      security:
      - BearerAuth: []
      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'
        '422':
          $ref: '#/components/responses/SenderIDRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ScreeningUnavailable'
  /v1/sms/{id}:
    get:
      summary: Get SMS status
      description: 'Retrieves the current status and details of a transactional SMS. Only

        messages belonging to the authenticated workspace are visible.


        `timeline` lists each hop the message took, oldest first: `queued`,

        `sent`, then `delivered`, `failed` or `blocked`. A terminal `failed` or

        `blocked` hop carries a `reason` token and whether the credit was

        `refunded`. `delivered` appears only when the carrier sends a receipt.

        See Delivery timeline.'
      operationId: getSMS
      tags:
      - SMS
      security:
      - BearerAuth: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: SMS message ID returned from send
      responses:
        '200':
          description: SMS details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SMSDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/pricing:
    get:
      summary: Get the price list
      description: 'The published price list: what one credit costs, how many credits an

        SMS to each destination is charged, and live WhatsApp OTP credits.


        No authentication. Every customer pays the published price, so the

        response is the same for a signed-in tenant and an anonymous reader,

        and it is safe to cache: the server holds it for a minute.


        `countries` is ordered cheapest first. `credits` is charged when no

        telco rate matches the number; `networks` lists the per-telco rates

        where an operator has set them, and `min_credits` / `max_credits` span

        the two so a caller can show a range without walking the list.

        `listed` marks the destinations the published price table names.

        Unlisted ones are still sendable and still billed at the price shown.


        `whatsapp` lists WhatsApp OTP credit costs computed at request time

        from `whatsapp_otp_costs` plus FX: `ceil(cost_ngn * 1.20 / credit_price)`,

        minimum 1, the same formula as send. Empty when no cost rows are

        loaded. WhatsApp is OTP-only (Meta Authentication + copy-code).


        A long SMS is split into parts and charged per part; see

        Segments and encoding.'
      operationId: getPricing
      tags:
      - SMS
      security: []
      responses:
        '200':
          description: The price list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pricing'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  schemas:
    SMSDetails:
      type: object
      required:
      - id
      - phone_number
      - country_code
      - message
      - credit_cost
      - status
      - created_at
      properties:
        id:
          type: string
          format: uuid
        phone_number:
          type: string
        country_code:
          type: string
        message:
          type: string
        credit_cost:
          type: integer
        status:
          type: string
          enum:
          - pending
          - sent
          - delivered
          - failed
          - blocked
          description: '`blocked` means anti-spam screening stopped the message before any

            provider saw it. It is terminal, and the credit was refunded in

            full; `metadata.antispam` carries the classifier''s verdict.

            '
        metadata:
          type:
          - object
          - 'null'
          additionalProperties: true
          description: 'Whatever was sent with the message. Screening adds an `antispam`

            key when the classifier flagged or blocked it, carrying `is_spam`,

            `score`, `category` and `reason`.

            '
        created_at:
          type: string
          format: date-time
        timeline:
          type: array
          description: Hops the message took, oldest first.
          items:
            $ref: '#/components/schemas/TimelineItem'
          example:
          - at: '2026-09-13T09:00:00Z'
            status: queued
          - at: '2026-09-13T09:00:02Z'
            status: sent
          - at: '2026-09-13T09:00:41Z'
            status: failed
            reason: delivery_failed
            refunded: true
    TimelineItem:
      type: object
      required:
      - at
      - status
      properties:
        at:
          type: string
          format: date-time
          description: When the hop happened (RFC 3339).
        status:
          type: string
          description: '`queued` (charged and waiting for the worker), then the message''s

            own status values: `sent`, `delivered`, `failed`, `blocked`, and for

            OTPs `verified` and `expired`. A hop is listed only when its time

            is known; `status` on the parent object is always authoritative.

            '
        reason:
          type: string
          description: 'Stable machine token. Present on a terminal hop when known, and on

            `sent` when the anti-spam screen flagged but still sent the message.

            Tokens: `send_failed`, `delivery_failed`, `antispam_blocked`,

            `antispam_flagged`, `max_attempts_exceeded`, `workspace_suspended`,

            `workspace_restricted`, `kyc_required`. Omitted when unknown.

            '
        refunded:
          type: boolean
          description: 'On a terminal `failed` or `blocked` hop only. Reflects the credit

            ledger: `true` means the charge was returned.

            '
    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
    PricingWhatsApp:
      type: object
      required:
      - country_code
      - credits
      - price
      properties:
        country_code:
          type: string
          description: 'ISO 3166-1 alpha-2 country code, or `*` for the platform default

            row used when a destination has no country-specific cost.

            '
          example: NG
        country_name:
          type: string
          description: Display name when known; `Default` for `*`.
          example: Nigeria
        credits:
          type: integer
          description: 'Credits charged for one WhatsApp OTP to this destination.

            `ceil(cost_ngn * 1.20 / credit_price)`, minimum 1.

            '
          example: 5
        price:
          type: integer
          description: '`credits` times `credit_price`.'
          example: 30
    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_suspended
              - 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
    Pricing:
      type: object
      required:
      - currency
      - credit_price
      - sender_id_requires_purchase
      - countries
      - whatsapp
      properties:
        currency:
          type: string
          description: The currency credits are sold in.
          example: NGN
        credit_price:
          type: integer
          description: What one credit costs, in `currency`.
          example: 6
        sender_id_requires_purchase:
          type: boolean
          description: 'A workspace must complete at least one credit purchase before it

            may request a custom Sender ID.

            '
          example: true
        countries:
          type: array
          description: Priced SMS destinations, cheapest first.
          items:
            $ref: '#/components/schemas/PricingCountry'
        whatsapp:
          type: array
          description: 'WhatsApp OTP credit costs by destination, computed live from

            `whatsapp_otp_costs` (+ FX) with the same formula as send.

            Always present; empty when no rows are loaded.

            '
          items:
            $ref: '#/components/schemas/PricingWhatsApp'
    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
          maxProperties: 50
          description: 'Arbitrary key-value data attached to the message. At most 50 keys

            and 4 KB of JSON, or the request fails with `validation_error`.

            Reserved keys (`antispam` and any key starting with `robase_`) are

            dropped.

            '
    PricingCountry:
      type: object
      required:
      - country_code
      - country_name
      - credits
      - price
      - min_credits
      - max_credits
      - listed
      properties:
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code
          example: NG
        country_name:
          type: string
          example: Nigeria
        credits:
          type: integer
          description: Credits charged when no telco rate matches the number.
          example: 1
        price:
          type: integer
          description: '`credits` times `credit_price`.'
          example: 6
        min_credits:
          type: integer
          description: The cheapest rate to this destination, across every telco.
          example: 1
        max_credits:
          type: integer
          description: The dearest rate to this destination, across every telco.
          example: 3
        listed:
          type: boolean
          description: 'Whether the published price table names this destination. An

            unlisted one is still sendable and still charged at this price.

            '
          example: true
        networks:
          type: array
          description: Per-telco rates, cheapest first. Absent where none are set.
          items:
            $ref: '#/components/schemas/PricingNetwork'
    PricingNetwork:
      type: object
      required:
      - key
      - name
      - credits
      - price
      properties:
        key:
          type: string
          description: Stable identifier for the telco.
          example: glo
        name:
          type: string
          example: Glo Mobile
        credits:
          type: integer
          example: 3
        price:
          type: integer
          description: '`credits` times `credit_price`.'
          example: 18
  responses:
    InsufficientScope:
      description: 'The API key does not have the scope this endpoint needs. Agent keys

        carry scopes (`otp:send`, `otp:verify`, `sms:send`, `read`,

        `approvals:request`); keys made before scopes existed keep full access.

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: insufficient_scope
              message: this API key does not have the read scope
              required_scope: read
    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
    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
    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
    SenderIDRequired:
      description: 'The workspace needs its own approved sender ID for this destination

        country. Workspaces created after 8 October 2026 send transactional SMS

        only under their own name where custom sender IDs are offered. Nothing

        is charged. OTP is not affected.

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: sender_id_required
              message: Request a sender ID to send SMS.
    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)
    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
    ScreeningUnavailable:
      description: 'A pre-send check could not reach a verdict: the content check on a

        new workspace''s `/v1/sms/send` request, or the pumping controls on

        any `/v1/sms/send` or `/v1/otp/send`. The checks run before anything

        is charged or queued; when one 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
    SendForbidden:
      description: 'Sending is refused. `error.type` is `kyc_required` (business

        verification hold: KYB / financial-alert path, separate from

        antispam), `account_suspended` (the workspace is suspended after

        antispam review; contact support to appeal),

        `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_suspended:
              value:
                error:
                  type: account_suspended
                  message: this workspace is suspended and cannot send
            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
    NotFound:
      description: No record with that ID exists in this workspace.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              type: otp_not_found
              message: OTP not found
  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
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: 'Your Robase API key. Starts with robe_. Include as Authorization: Bearer robe_...'