> ## 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.

# Verify an OTP

> POST /v1/otp/verify — constant-time check. Wrong codes with attempts left are 200 valid:false.



## OpenAPI

````yaml openapi.yaml POST /v1/otp/verify
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/otp/verify:
    post:
      tags:
        - OTP
      summary: Verify an OTP code
      description: |
        Verifies a code against a previously sent OTP using a constant-time
        comparison.

        A wrong code that has not exhausted the attempt budget is **not** an
        error: the call returns `200` with `valid: false` and
        `attempts_remaining`. Terminal states — expired, already verified, or
        attempts exhausted — return `409`.
      operationId: verifyOTP
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyOTPRequest'
      responses:
        '200':
          description: |
            The code was checked. `valid` reports whether it matched; a `false`
            result still counts against the attempt budget.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyOTPResponse'
              examples:
                accepted:
                  summary: Correct code
                  value:
                    valid: true
                    status: verified
                    attempts_used: 1
                rejected:
                  summary: Wrong code, attempts remaining
                  value:
                    valid: false
                    status: sent
                    attempts_used: 2
                    attempts_remaining: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: |
            The OTP is in a terminal state and can no longer be verified.
            `error.type` is `otp_expired`, `otp_already_verified`, or
            `max_attempts_exceeded`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                expired:
                  value:
                    error:
                      type: otp_expired
                      message: OTP has expired
                alreadyVerified:
                  value:
                    error:
                      type: otp_already_verified
                      message: OTP already verified
                maxAttempts:
                  value:
                    error:
                      type: max_attempts_exceeded
                      message: maximum verification attempts exceeded
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
      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:
    VerifyOTPRequest:
      type: object
      required:
        - otp_id
        - code
      properties:
        otp_id:
          type: string
          format: uuid
          description: The `id` returned by the send endpoint
        code:
          type: string
          description: The code the user entered
          example: '123456'
    VerifyOTPResponse:
      type: object
      required:
        - valid
        - status
        - attempts_used
      properties:
        valid:
          type: boolean
          description: Whether the submitted code matched
        status:
          type: string
          enum:
            - pending
            - sent
            - verified
            - failed
          description: |
            The OTP's state after this attempt. `verified` on success;
            `failed` once the attempt budget is exhausted.
        attempts_used:
          type: integer
          description: Verification attempts made so far, including this one
        attempts_remaining:
          type: integer
          description: >-
            Attempts left before the OTP is burned. Present only when `valid` is
            false.
    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
    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
    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
    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
  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.