SMS Verification API — receive OTP codes programmatically
SocioSMS gives developers a simple REST SMS verification API to rent virtual phone numbers, receive OTP / SMS codes, and get automatic refunds when a code never arrives. Coverage spans 190+ countries and hundreds of services (WhatsApp, Telegram, Google and more), billed pay-as-you-go from your Naira wallet — no subscription, no setup fee.
Overview
The SocioSMS Developer API lets you programmatically rent SMS numbers from the same server network the website uses. Numbers are routed across five internal servers, exposed to your code as Server A, Server B, Server C, Server D, and Server E. Every rental debits your Naira wallet with an automatic developer discount applied.
- Base URL:
https://sociosms.com/api/public/v1 - Auth:
Authorization: Bearer ssms_live_… - Content type:
application/json - Currency: Naira (₦). All prices, balances and refunds are in NGN.
Servers
Pick a server the same way you would on the website — cheapest option wins by default. If one server is out of stock or a number fails to receive a code, retry with another server.
| Server | Best for |
|---|---|
| A | USA numbers, renewable verifications |
| B | Global activations, widest country coverage |
| C | Global activations, competitive prices |
| D | USA rentals, longer-lived numbers |
| E | Global activations, backup capacity |
1. Get an API key
Go to Developer settings, click Create key, and copy the ssms_live_… value shown once. Also save the webhook signing secret if you plan to receive webhooks.
2. Fund your wallet
The API debits the same Naira wallet used on the website. Top up on your wallet page. Every API rental appears in your normal transactions history tagged source: "api".
3. Quickstart
curl -X POST https://sociosms.com/api/public/v1/rentals \
-H "Authorization: Bearer ssms_live_XXX" \
-H "Content-Type: application/json" \
-d '{ "service": "whatsapp", "country": "US" }'Returns: { "rental": { "id", "phone", "server", "price_ngn", "expires_at", "status": "waiting" } }
Pick a specific server:
curl -X POST https://sociosms.com/api/public/v1/rentals \
-H "Authorization: Bearer ssms_live_XXX" \
-H "Content-Type: application/json" \
-d '{ "service": "telegram", "country": "GB", "server": "B" }'Endpoints (summary)
| Method | Path | Description |
|---|---|---|
| GET | /services | List available services |
| GET | /countries?service=<id> | Countries + best price for a service |
| GET | /prices?service=<id>&country=<cc> | All server options (A–E) for a service+country |
| POST | /rentals | Buy a number { service, country?, server? } |
| GET | /rentals | List your recent API rentals |
| GET | /rentals/:id | Poll status + fetch code |
| POST | /rentals/:id/cancel | Cancel and refund (only before code arrives) |
| POST | /rentals/:id/finish | Mark rental complete (no refund) |
| GET | /wallet | Your Naira balance |
| GET | /transactions | Recent transactions |
| GET|PUT | /webhook | Get or set your webhook URL { url } |
Enums
Fixed values you'll see in requests and responses:
server:"A" | "B" | "C" | "D" | "E"country: ISO-3166 alpha-2, uppercase (e.g."US","GB","NG")- Rental
status:"waiting" | "received" | "completed" | "cancelled" | "expired" - Transaction
type:"deposit" | "purchase" | "refund" | "bonus" | "adjustment" - Transaction
status:"pending" | "completed" | "failed" - Transaction
source:"web" | "api" - Webhook events:
rental.code_received,rental.cancelled,rental.expired,rental.expired_refunded
GET /services
Returns the catalog of services you can rent. IDs are stable and match what the website uses.
GET /api/public/v1/services
Authorization: Bearer ssms_live_XXX
200 OK
{
"services": [
{
"id": "whatsapp",
"name": "WhatsApp",
"logo_slug": "whatsapp",
"logo": "https://cdn.simpleicons.org/whatsapp",
"logo_svg": "https://cdn.simpleicons.org/whatsapp",
"logo_png": "https://unavatar.io/whatsapp.com?fallback=https://cdn.simpleicons.org/whatsapp"
}
]
}logo / logo_svg is an SVG in the brand color — render it in <img> directly. Append /<hex> (e.g. /ffffff for white, /000000 for black) to recolor: https://cdn.simpleicons.org/whatsapp/ffffff. Use logo_png when you need a raster PNG (e.g. email, OG images).
GET /countries
Countries available for a service, with the best (cheapest available) price across all servers.
GET /api/public/v1/countries?service=whatsapp
Authorization: Bearer ssms_live_XXX
200 OK
{
"countries": [
{ "country_code": "US", "country_name": "USA", "flag": "🇺🇸", "flag_url": "https://flagcdn.com/w160/us.png", "price_ngn": 1795, "stock": 42 },
{ "country_code": "GB", "country_name": "England", "flag": "🇬🇧", "flag_url": "https://flagcdn.com/w160/gb.png", "price_ngn": 2100, "stock": 17 }
]
}flag is the emoji; flag_url is a hotlink-safe PNG for UIs that don't render emoji flags. stock is a hint, not a guarantee.
GET /prices
All server options for a service (and optional country). Use this to compare Servers A–E and pick one explicitly at purchase.
GET /api/public/v1/prices?service=whatsapp&country=US
Authorization: Bearer ssms_live_XXX
200 OK
{
"servers": [
{ "server": "A", "country_code": "US", "country_name": "USA", "flag": "🇺🇸", "flag_url": "https://flagcdn.com/w160/us.png", "price_ngn": 1795, "stock": 30 },
{ "server": "D", "country_code": "US", "country_name": "USA", "flag": "🇺🇸", "flag_url": "https://flagcdn.com/w160/us.png", "price_ngn": 1850, "stock": 12 },
{ "server": "B", "country_code": "US", "country_name": "USA", "flag": "🇺🇸", "flag_url": "https://flagcdn.com/w160/us.png", "price_ngn": 2200, "stock": 8 }
]
}POST /rentals
Buy a number. This follows the exact same flow as the website: your Naira wallet is debited, a number is reserved on the chosen server, and the rental starts in waiting.
Request body
{
"service": "whatsapp", // required — service id from /services
"country": "US", // optional — ISO alpha-2; omit for auto-pick
"server": "A" // optional — "A" | "B" | "C" | "D" | "E"; omit for cheapest
}200 OK
{
"rental": {
"id": "9c1e...-uuid",
"service": "WhatsApp",
"country": "USA",
"phone": "+15551234567",
"server": "A",
"price_ngn": 1795,
"expires_at": "2026-07-21T18:32:00.000Z",
"status": "waiting"
}
}Errors
402 insufficient_funds— top up your wallet400 service_unsupported/country_unsupported503 no_numbers— retry with another server409 price_changed— upstream price moved; re-check/prices
GET /rentals/:id
Poll a rental. If the code hasn't arrived yet we transparently re-check the upstream server for you, so simple polling on this endpoint is enough.
GET /api/public/v1/rentals/9c1e...-uuid
Authorization: Bearer ssms_live_XXX
200 OK — waiting
{ "rental": {
"id": "9c1e...", "service": "WhatsApp", "country": "USA",
"phone": "+15551234567", "server": "A",
"code": null, "message": null,
"status": "waiting",
"price_ngn": 1795,
"expires_at": "2026-07-21T18:32:00.000Z",
"created_at": "2026-07-21T18:12:00.000Z",
"refunded_at": null
} }
200 OK — code received
{ "rental": {
"id": "9c1e...", "code": "482913",
"message": "Your WhatsApp code: 482913",
"status": "received", ...
} }Recommended polling: every 3–5s until status !== "waiting" or expires_at passes. Or set a webhook and skip polling.
GET /rentals
Your 50 most recent API-sourced rentals, newest first. Same object shape as GET /rentals/:id (without message).
POST /rentals/:id/cancel
Cancel a rental that hasn't received a code and refund the exact debited amount back to your wallet. Once a code has arrived cancellation is refused with 409 code_received.
Some servers lock a freshly issued number for its first 2 minutes and will not release it yet. In that case the cancel is accepted and queued with 202: we release the number and refund automatically as soon as the lock lifts, then send the usual rental.cancelled webhook. No retry is needed on your side.
Server D holds a number for its first 5 minutes and will not release it early — codes on that network frequently arrive late in that window. Cancelling before then returns 409 cancel_locked with retry_after_seconds, retry_at and a Retry-After header. Surface that countdown to your end-user and keep the cancel button disabled until it reaches zero; keep polling the rental meanwhile, because a code may still land — once it does, hide the cancel button entirely.
POST /api/public/v1/rentals/9c1e.../cancel
Authorization: Bearer ssms_live_XXX
200 OK
{ "ok": true, "refunded_ngn": 1795 }
202 Accepted (number still inside the 2-minute server lock)
{
"ok": true,
"queued": true,
"status": "cancelling",
"retry_after_seconds": 74
}
409 Conflict (Server D — 15-minute cancel lock still active)
{
"ok": false,
"error": { "code": "cancel_locked", "message": "This server locks the number for its first 5 minutes..." },
"retry_after_seconds": 212,
"retry_at": "2026-07-27T10:21:44.000Z"
}POST /rentals/:id/finish
Mark a rental as complete after you've used the code. No refund. This is optional — expired rentals auto-close.
POST /api/public/v1/rentals/9c1e.../finish → 200 { "ok": true }GET /wallet
GET /api/public/v1/wallet → 200 { "balance_ngn": 12450 }GET /transactions
Recent wallet activity. source: "api" marks entries created by your API key.
GET /api/public/v1/transactions?limit=20
Authorization: Bearer ssms_live_XXX
200 OK
{
"transactions": [
{ "id": "...", "type": "purchase", "amount_ngn": -1795,
"description": "SMS rental — WhatsApp USA",
"status": "completed", "source": "api",
"created_at": "2026-07-21T18:12:00.000Z" },
{ "id": "...", "type": "refund", "amount_ngn": 1795,
"description": "API cancel refund",
"status": "completed", "source": "api",
"created_at": "2026-07-21T18:20:00.000Z" }
]
}GET / PUT /webhook
GET /api/public/v1/webhook → 200 { "webhook_url": "https://you.example.com/hooks/sms" }
PUT /api/public/v1/webhook
body: { "url": "https://you.example.com/hooks/sms" } // or { "url": null } to disable
→ 200 { "webhook_url": "https://..." }Webhooks
When set, we POST an event to your URL as soon as a rental changes state. Delivery is retried a few times on non-2xx.
Headers
Content-Type: application/jsonX-SocioSMS-Event:rental.code_received|rental.cancelled|rental.expired|rental.expired_refundedX-SocioSMS-Signature:sha256=<hex hmac of raw body, using your webhook secret>
Example body
{
"event": "rental.code_received",
"rental": {
"id": "9c1e...", "service": "WhatsApp", "country": "USA",
"phone": "+15551234567", "server": "A",
"code": "482913",
"message": "Your WhatsApp code: 482913",
"status": "received",
"price_ngn": 1795
},
"created_at": "2026-07-21T18:14:22.000Z"
}Verifying the signature (Node.js)
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Refund flow (no code received)
Every rental you create is fully refundable back to your API wallet until a code arrives. There are three paths that all end in the same atomic, idempotent refund — a rental can never be refunded twice.
1. Developer cancels early
Call cancel any time before code is set:
curl -X POST https://sociosms.lovable.app/api/public/v1/rentals/9c1e.../cancel \
-H "Authorization: Bearer ssms_live_XXX"
200 OK
{ "ok": true, "refunded_ngn": 1795 }
# After code has arrived:
409 { "error": { "code": "code_received", "message": "Cannot cancel after code received" } }2. Rental expires with no code (auto-refund on poll)
Polling GET /rentals/:id after expires_at will flip the rental to status: "expired" and, if no code was ever received, refund it in the same request. The response includes refunded_at.
GET /api/public/v1/rentals/9c1e...
Authorization: Bearer ssms_live_XXX
200 OK
{
"rental": {
"id": "9c1e...", "status": "expired",
"code": null, "message": null,
"refunded_at": "2026-07-21T18:34:11.000Z",
"price_ngn": 1795, "server": "A"
}
}3. Server-side safety net
Even if you never poll and never cancel, our server sweeps expired rentals and refunds them automatically. You will still receive the rental.expired_refunded webhook. You never lose funds because a request was missed.
Webhook you receive on refund
POST https://you.example.com/hooks/sms
X-SocioSMS-Event: rental.expired_refunded
X-SocioSMS-Signature: sha256=<hex hmac of raw body>
{
"event": "rental.expired_refunded",
"rental": {
"id": "9c1e...", "service": "WhatsApp", "country": "USA",
"phone": "+15551234567", "server": "A",
"status": "expired", "code": null,
"refunded_ngn": 1795,
"refunded_at": "2026-07-21T18:34:11.000Z"
},
"created_at": "2026-07-21T18:34:11.000Z"
}
# When the developer triggered cancel:
X-SocioSMS-Event: rental.cancelled
{ "event": "rental.cancelled", "rental": { ..., "refunded_ngn": 1795 } }End-to-end example: "Refund my order" button
Wire your UI button to call cancel, then credit your end-user in your own DB using therefunded_ngn value in the response.
// Node.js — POST /my-app/orders/:id/refund
export async function refundOrder(req, res) {
const order = await db.orders.find(req.params.id);
if (order.user_id !== req.user.id) return res.status(403).end();
const r = await fetch(
`https://sociosms.lovable.app/api/public/v1/rentals/${order.rental_id}/cancel`,
{ method: "POST", headers: { Authorization: `Bearer ${process.env.SOCIOSMS_KEY}` } }
);
const body = await r.json();
if (r.status === 409 && body.error?.code === "code_received") {
return res.status(409).json({ error: "Code already received — cannot refund." });
}
if (!r.ok) return res.status(502).json({ error: body.error?.message ?? "Refund failed" });
// Credit your own end-user with the exact amount we refunded to your wallet.
await db.wallets.credit(order.user_id, body.refunded_ngn, `Refund for order ${order.id}`);
await db.orders.update(order.id, { status: "refunded" });
res.json({ ok: true, refunded_ngn: body.refunded_ngn });
}Rules to remember
- Cancel is only valid while
codeisnull. After that, refuse the refund on your side. - Refunds are idempotent — calling cancel twice returns success once, no duplicate credit.
- Refund amount always equals the exact NGN debited at purchase time, regardless of FX changes.
- Refunds land in your SocioSMS wallet. Passing that credit to your end-user is your app's responsibility.
- Trust the webhook + response — never assume a refund from a timeout on your side.
Rate limits
Default: 60 requests/minute and 10 purchases/minute per key. Exceeding either returns HTTP 429 with a Retry-After header. Contact support to raise limits.
Errors
All errors return { "error": { "code", "message" } }. Common codes:
unauthorized· missing or invalid key (401)rate_limited· slow down (429)insufficient_funds· top up wallet (402)no_numbers· none available on that server (503)service_unsupported/country_unsupported(400)price_changed· upstream price moved (409)code_received· cannot cancel after code (409)not_found· rental id not owned by your key (404)
Pricing
API prices equal the current website price minus a developer discount (default 7%). Refunds return the exact amount debited. There are no hidden fees.
Frequently asked questions
What is the SocioSMS SMS verification API?
The SocioSMS API is a REST API that lets developers rent virtual phone numbers, receive SMS verification (OTP) codes, and get automatic refunds when no code arrives — all from their own app or backend.
How do I get an API key?
Create a free SocioSMS account, open the Developer dashboard, and generate a key. Keys start with ssms_live_ and are sent as an Authorization: Bearer header.
Which countries and services are supported?
Hundreds of services (WhatsApp, Telegram, Google, and more) across 190+ countries, routed across five servers (Server A to Server E) for stock and price coverage.
Do I get refunded if no SMS code arrives?
Yes. If a rental expires or is cancelled without a code, the exact amount debited is returned to your Naira wallet and a rental.cancelled webhook is sent to your endpoint.
How much does the SMS API cost?
Pay as you go in Naira. API prices equal the live website price minus a developer discount (default 7%), with no monthly fee or hidden charges.
Ready to build? Generate an API key and send your first request in under a minute.