Skip to main content
Developers

Build SMS verification into your code

A versioned REST API with scoped keys, cursor pagination, predictable error codes and per-key rate limits.

Full API referenceAPI access coming soon

Scoped keys

Grant only what a key needs. Read-only keys cannot spend.

Rate limited

Per-key limits with standard headers so you can back off cleanly.

Idempotent writes

Send a key and a retry returns the original order, never a second charge.

Cursor pagination

Stable pagination that does not skip rows as new orders arrive.

Authentication

Every request needs a bearer token. API access opens soon; from then you will create keys in your dashboard. Each is shown once and stored only as a hash, so keep it in a secret manager rather than in source control.

bash
curl https://your-domain/api/v1/countries \
  -H "Authorization: Bearer smspvo_live_8f3k2q_…"

Buying a number

Lock the price with a quote, then place the order with it: the order pays exactly the quoted price. Send an idempotency_key too — if the request is retried (a timeout, a lambda replay, a nervous client) you get the original order back rather than a second charge.

bash
curl -X POST https://your-domain/api/v1/quotes \
  -H "Authorization: Bearer $SMSPVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "country": "NG", "service": "telegram" }'

curl -X POST https://your-domain/api/v1/orders \
  -H "Authorization: Bearer $SMSPVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "NG",
    "service": "telegram",
    "quote": "5b0e…",
    "idempotency_key": "signup-test-4812"
  }'
json
{
  "data": {
    "id": "cm3x…",
    "reference": "ORD-8F3K2Q",
    "status": "WAITING_SMS",
    "number": "+2348012345678",
    "country": "NG",
    "service": "telegram",
    "price": "14",
    "currency": "USD",
    "deadline": "2026-09-11T10:02:04.000Z",
    "replacements": 0,
    "messages": []
  }
}

If a number goes quiet, the order moves to a new one by itself, free; if none delivers, it is refunded in full. You can also skip the quote and pay the current price, adding max_price to refuse anything dearer.

Waiting for the code

Poll the order. Each read also moves it on, so you do not need a separate refresh call. Poll every few seconds rather than in a tight loop — the endpoint is rate limited, and upstream checks are throttled behind it regardless. If number changes between reads, the order has moved to a new number: use the new one.

javascript
async function waitForCode(orderId, { timeoutMs = 300000 } = {}) {
  const deadline = Date.now() + timeoutMs

  while (Date.now() < deadline) {
    const response = await fetch(
      `https://your-domain/api/v1/orders/${orderId}`,
      { headers: { Authorization: `Bearer ${process.env.SMSPVO_API_KEY}` } },
    )
    const { data } = await response.json()

    const code = data.messages?.find((message) => message.code)?.code
    if (code) return code

    // No code will come: cancelled, or refunded after no number delivered.
    if (['CANCELLED', 'EXHAUSTED'].includes(data.status)) {
      throw new Error(`Order ended as ${data.status}`)
    }

    await new Promise((resolve) => setTimeout(resolve, 3000))
  }

  throw new Error('Timed out waiting for the verification code')
}

Money in responses

All amounts are strings holding the currency’s minor unit — kobo for NGN, cents for USD. A price of "14" in USD means $0.14. They are strings because JSON numbers cannot represent large integers safely, and because money should never pass through a float.

Errors

Every error has the same shape and a stable machine-readable code:

json
{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Your wallet balance is too low for this purchase."
  },
  "requestId": "a3f9…"
}

Branch on code, never on the message text — messages may be reworded, codes will not. Include requestId when reporting a problem.

Rate limits

Every response carries the standard headers:

  • X-RateLimit-Limit — requests allowed per window
  • X-RateLimit-Remaining — how many are left
  • X-RateLimit-Reset — seconds until the window resets
  • Retry-After — sent on a 429

Testing

In a development deployment with mock mode enabled, the mock supplier issues simulated numbers and delivers a simulated code after a few seconds — enough to exercise the whole flow in CI without a supplier account. Simulated messages are prefixed [SIMULATED] and mock mode cannot be enabled in production.