SMS verification API
The SMS verification API is three calls: buy a number, wait for the code, release the number. It reaches 517 services in 225 countries, numbers start at €0.07, and an activation that receives nothing is refunded without a support ticket. Keys are scoped, purchases are idempotent, and legacy handler_api clients work unchanged.
The API in one block
Updated 28 Aug 2026- Base URL
- https://virtualsmsnumbers.com/api/v1
- Authentication
- Bearer key, or X-Api-Key
- Rate limit
- 120/min, burst 20/s
- Idempotency window
- 24 hours
- Auto-refund after
- 20 minutes
- Cheapest activation
- €0.07
- Countries covered
- 225
- Format
- JSON, cents, ISO 8601 UTC
Prices and stock come from the operator pool as it stands right now, not from a published rate card.
The three-call flow
Everything else in the reference is optional. This is the entire verification path, and it has not changed since 2019.
- 1
POST /activations
Ask for a service, and a country if you care which. The response carries the number, the price in cents and the expiry timestamp. Send an Idempotency-Key so a retried request cannot buy a second number, and a max_price_cents if the caller is automated.
- 2
GET /activations/{id}?wait=180
Long-poll. The connection is held open until the message lands or the timeout expires, so one request replaces a loop that would otherwise make ninety. The status comes back as code_received with the parsed code alongside the raw text, because senders format them differently.
- 3
POST /activations/{id}/complete
Close the activation and release the line. If nothing arrived, do nothing at all: the window expires on its own and the balance is credited back. Cancelling early is the same call under a different name.
A working example
Set VSN_KEY and run it. The commented responses are the real shapes, including the fields you will want to store.
# 1. Buy a Telegram number in Portugal
-kw">curl -X POST https://virtualsmsnumbers.com/api/v1/activations \
-H "Authorization: Bearer $VSN_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"service":"telegram","country":"PT"}'
# {"id":"1043872915","phone_number":"351926114508","status":"waiting",
# "price_cents":11,"expires_at":"2026-08-26T12:41:07Z"}
# 2. Long-poll until the code lands (blocks up to 120 s)
-kw">curl "https://virtualsmsnumbers.com/api/v1/activations/1043872915?wait=120" \
-H "Authorization: Bearer $VSN_KEY"
# {"id":"1043872915","status":"code_received",
# "messages":[{"code":"48219","text":"Telegram code: 48219"}]}
# 3. Release the number
-kw">curl -X POST https://virtualsmsnumbers.com/api/v1/activations/1043872915/complete \
-H "Authorization: Bearer $VSN_KEY"Rate limits
120 requests per minute per key, bursting to 20 per second. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 tells you how long to wait rather than making you guess.
The limit is rarely the constraint if the API is used as intended: one long poll instead of ninety polls, a webhook instead of a timer, and the country and service lists cached at boot. If you genuinely need more headroom, ask for it — raising a limit is easier for both of us than debugging a fleet of keys.
Idempotency
Send an Idempotency-Key header on POST /activations. Replaying the same key within 24 hours returns the original activation as it stands now, with an Idempotent-Replay header, instead of buying a second number and charging you twice.
The key is not a hash of the body: a different payload under a used key still returns the first activation. Generate one key per logical purchase — a UUID is fine — and reuse it across retries, never across purchases.
Legacy handler_api clients
Scripts written for the classic handler_api.php protocol keep working. Point them at the compatibility endpoint, swap the api_key, and the same actions return the same ACCESS_ and STATUS_ tokens they always did.
It is a translation layer over the same pool, so prices, stock and refunds behave identically either way. New code should still use /api/v1: the JSON responses carry timestamps, error codes and price fields that the token format has no room for.
# Existing scripts keep working: same actions, same response tokens.
BASE="https://virtualsmsnumbers.com/stubs/handler_api.php"
-kw">curl "$BASE?api_key=$KEY&action=getNumber&service=tg&country=117"
# ACCESS_NUMBER:1043872915:351926114508
-kw">curl "$BASE?api_key=$KEY&action=getStatus&id=1043872915"
# STATUS_WAIT_CODE
# STATUS_OK:48219
-kw">curl "$BASE?api_key=$KEY&action=setStatus&id=1043872915&status=6"
# ACCESS_ACTIVATION
-kw">curl "$BASE?api_key=$KEY&action=getBalance"
# ACCESS_BALANCE:41.87Agents, MCP and discovery
There is a hosted MCP server, an OpenAPI 3.1 document and an llms.txt index, so an agent can discover the three tools it needs without hand-written glue. Point your client at the endpoint below and it will find buy, wait and release.
Before you let anything autonomous spend from the balance: scope the key to numbers:read and numbers:write, set a max_price_cents ceiling on every purchase, and give it its own key so revoking it does not take your production integration down.
{
"mcpServers": {
"virtualsmsnumbers": {
"type": "http",
"url": "https://virtualsmsnumbers.com/api/mcp",
"headers": { "Authorization": "Bearer vsn_live_xxxxxxxxxxxx" }
}
}
}Before production
- Set max_price_cents on every purchase. Prices move with the pool, and an unbounded agent will cheerfully buy the expensive end of it.
- Treat identifiers as opaque strings. Activation ids happen to be ten digits today; that is not a promise, and neither is their length.
- Branch on the error code, never on the message. Codes are stable within a version; messages are written for humans and get rewritten.
- Subscribe to sms.received and verify the signature — HMAC-SHA256 over the raw body, with a five-minute replay window — rather than polling for state you could be told about.
- Do arithmetic on the integer cent fields. The formatted strings next to them are for display and are locale-dependent.
The full reference
Six endpoints, error tables, webhook payloads, scopes and the legacy token map.
Frequently asked questions
Median delivery is under nine seconds from the moment the service sends it. The long poll exists because the tail is long: the activation stays open for 20 minutes, and expires into a refund if nothing lands.
Nothing you need to handle. The window closes, the balance is credited back automatically, and the activation ends in an expired state you can read from the API.
Yes, and it costs nothing. Keys carry fixed scopes, an optional IP allowlist and an expiry chosen at creation, so a staging key that leaks cannot buy numbers in production.
There are thin, dependency-free wrappers, but the API is six endpoints over JSON — fetch, requests or curl is genuinely enough for the three-call flow.
On one activation, yes: extra messages arrive on the same number while it is open. Across sessions you need a rental, which holds the line from four hours to twelve months.
No. It is the same pool at the same prices; only the response encoding differs.