Build SMS verification into your code
A versioned REST API with scoped keys, cursor pagination, predictable error codes and per-key rate limits.
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.
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.
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"
}'{
"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.
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:
{
"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 windowX-RateLimit-Remaining— how many are leftX-RateLimit-Reset— seconds until the window resetsRetry-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.