SafeCode Numbers API
Rent virtual phone numbers and receive OTPs / SMS programmatically. Pay-as-you-go, single wallet, refunds on failed delivery.
Base URL: https://api.safecode.vip/v1
Dashboard: https://safecode.vip/numbers-api
Human docs page: https://safecode.vip/developer-apis/numbers
Full-text (this file): https://safecode.vip/developer-apis/numbers.md
LLM index: https://safecode.vip/developer-apis/numbers/llms.txt
Highlights
- Simple integration: stable endpoint shapes, predictable statuses, numeric
errnocodes. - One wallet: API purchases debit your main SafeCode balance. No separate top-up.
- Honest pricing: the price
/request/pricequotes is the price/purchase/smscharges. - Refunds: orders that end without a code (expired, cancelled by you, or withdrawn by the supplier) are refunded in full, automatically.
- Safe retries: send an
Idempotency-Keyheader and a retried purchase returns the same order instead of buying twice.
Authentication
Send your key as a Bearer token. Keys are created at https://safecode.vip/numbers-api and need the otp:receive scope.
Authorization: Bearer sfcd_live_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
You may also pass the key with the ?key= query parameter, but prefer the header: URLs end up in logs.
If you set an IP allowlist in the dashboard, requests from any other IP return errno 1 with HTTP 403.
Response envelope
Every response is JSON with the same three fields, plus endpoint-specific data:
{ "success": 1, "errno": 0, "message": "success", ...data }
Failures set success: 0 and a non-zero errno (table at the bottom).
Endpoints
All paths below are relative to https://api.safecode.vip/v1.
POST /purchase/sms
Buy a virtual number and start waiting for the SMS.
curl -X POST "https://api.safecode.vip/v1/purchase/sms" \
-H "Authorization: Bearer $SAFECODE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f0c2c1e-9a55-4c0e-8f7a-2b1d6c3e4a90" -d '{ "country": "us", "service": "telegram" }'
Body: country (required), service (required), max_price (optional, USD).
- You are charged the price
/request/pricequotes.max_pricebelow that price returnserrno 4with the currentprice; a highermax_pricelets the order through when stock is only available at a higher price. Idempotency-Key(optional header, recommended): any unique string per purchase. A retry with the same key returns the original order (idempotent_replay: true). The same key with a different country or service returnserrno 12; a retry while the first request is still running returnserrno 11.
country and service accept a code (us, gb, telegram, whatsapp), an exact name, or the UUID returned by /country/retrieve_all and /service/retrieve_all. Common aliases such as usa, uk and uae are understood. A partial name is accepted only when it matches exactly one entry; otherwise you get errno 2, so the API never guesses the wrong country.
Response:
{
"success": 1,
"errno": 0,
"message": "Number ordered successfully.",
"order_id": "e0b1…",
"phonenumber": "+14155551234",
"number": "+14155551234",
"country": "United States",
"country_code": "US",
"service": "Telegram",
"cost": 0.32,
"currency": "USD",
"expires_in": 1200,
"expires_at": "2026-07-01T15:04:05Z",
"cancel_available_at": "2026-07-01T14:47:05Z",
"balance": 12.83
}
A number stays reserved for expires_in seconds (20 minutes). Poll /sms/check until a code arrives or the order expires.
POST /sms/check
Poll for the SMS. status values are:
1waiting3received;codeandsmsare populated4expired (refunded)6cancelled by you or withdrawn by the supplier (refunded)
curl -X POST "https://api.safecode.vip/v1/sms/check" \
-H "Authorization: Bearer $SAFECODE_KEY" \
-H "Content-Type: application/json" \
-d '{ "orderid": "e0b1…" }'
Response while waiting:
{ "success": 1, "errno": 0, "status": 1, "order_id": "e0b1…", "phonenumber": "+14155551234", "expires_at": "…", "sms": null, "code": null, "message": "Waiting for SMS" }
Response once received:
{ "success": 1, "errno": 0, "status": 3, "code": "483920", "sms": "Your Telegram code: 483920", "full_sms": "Your Telegram code: 483920", "phonenumber": "+14155551234", "order_id": "e0b1…" }
Received codes are cached, so repeated checks on a completed order are instant and never re-poll the upstream carrier.
POST /sms/cancel
Cancel a number and refund your wallet.
- Orders can be cancelled 3 minutes after purchase (
cancel_available_at), because most codes arrive in that window. Earlier returnserrno 10withwait_seconds. - If the SMS arrived first, nothing is cancelled: you get
errno 6withstatus_code: 3and thecode, so you never lose a code you paid for. - A finished order returns
errno 6withstatus(text) andstatus_code(1, 3, 4 or 6).
curl -X POST "https://api.safecode.vip/v1/sms/cancel" \
-H "Authorization: Bearer $SAFECODE_KEY" \
-H "Content-Type: application/json" \
-d '{ "orderid": "e0b1…" }'
Response:
{ "success": 1, "errno": 0, "status": 6, "order_id": "e0b1…", "refunded": 0.32, "balance": 12.83, "message": "Order cancelled and refunded." }
POST /sms/resend
Ask the destination service to resend its OTP. Not available for every service; see the WhatsApp note below. Unsupported orders return errno 8; keep polling /sms/check instead.
curl -X POST "https://api.safecode.vip/v1/sms/resend" \
-H "Authorization: Bearer $SAFECODE_KEY" \
-H "Content-Type: application/json" \
-d '{ "orderid": "e0b1…" }'
GET /service/retrieve_all
Every service currently purchasable through the API (a few hundred), sorted by name.
{ "success": 1, "errno": 0, "services": [ { "id": "…", "name": "Telegram", "code": "telegram" }, … ] }
GET /country/retrieve_all
Every country currently purchasable through the API.
{ "success": 1, "errno": 0, "countries": [ { "id": "…", "name": "United States", "code": "US" }, … ] }
GET /request/price?country=us&service=telegram
Quote the current price before purchasing. This is the exact amount /purchase/sms will debit.
{ "success": 1, "errno": 0, "price": 0.32, "currency": "USD", "country": "United States", "service": "Telegram" }
GET /request/balance
Current wallet balance in USD.
{ "success": 1, "errno": 0, "balance": 12.83, "currency": "USD" }
GET /request/history?limit=25&offset=0&status=3
API-purchased orders, newest first, with total for paging.
limit1 to 100 (default 25),offsetfor paging,statusto filter by status code (1, 3, 4 or 6).- Each order has
status_code(same values as/sms/check) andservice,service_code,country,country_code.
WhatsApp numbers are available and are fulfilled through a dedicated upstream pool rather than the default one. Two things differ:
/sms/resendis not supported for WhatsApp orders and returnserrno 8. Keep polling/sms/check; the activation window is the usual 20 minutes.- Availability is per country. Ask
/request/price?country=…&service=whatsappfirst;errno 4means that country has no WhatsApp stock right now.
Webhooks
Add an endpoint in the dashboard (Webhooks tab) and we POST each order's outcome to it instead of you polling /sms/check.
Events:
number.received(status 3): the SMS arrived;data.codeholds the code.number.cancelled(status 6): you cancelled, or the supplier withdrew the number; refunded.number.expired(status 4): no SMS before expiry; refunded.
Body:
{
"event": "number.received",
"ts": 1790700000,
"data": {
"order_id": "b2566cc8-…", "livemode": true, "status": "received", "status_code": 3,
"phone_number": "447544323221", "code": "482913",
"service": "Facebook", "service_code": "facebook", "country": "United Kingdom", "country_code": "GB",
"price_usd": 0.37, "currency": "USD", "refunded_usd": null,
"created_at": "…", "finished_at": "…"
}
}
Verify every request: header X-SafeCode-Signature: t=<unix>,v1=<hex> is HMAC-SHA256 of "<t>.<raw body>" with your endpoint's signing secret (shown once when you add it). Reject if it doesn't match or t is more than 5 minutes old. Reply 2xx within 10 seconds; failures are retried 5 more times, after 1, 5, 30, 120 and 720 minutes (about 15 hours). If an event uses up every retry while the endpoint keeps failing, the endpoint is turned off; turn it back on in the dashboard. Events can repeat, so dedupe on order_id + event.
Sandbox
Create a Sandbox key in the dashboard (sfcd_test_…). Every endpoint works the same, with fake numbers and no charges (balance always 100). Code 123456 arrives about 10 seconds after purchase, the 3-minute cancel rule applies, and webhooks fire with livemode: false. Purchase, check, cancel, balance and history responses include livemode: false; catalog and price endpoints are shared with live. Sandbox keys work with the Numbers API only (the Messaging API rejects them).
Pick an outcome with sandbox_outcome on /purchase/sms:
| sandbox_outcome | result |
|---|---|
code (default) |
code 123456 after ~10 s |
slow_code |
code after ~90 s |
no_code |
no code for 20 min (test cancelling, allowed after 3 min) |
expire |
no code, expires after ~45 s |
no_stock |
purchase fails, errno 4 |
no_balance |
purchase fails, errno 3 |
Rate limits
Each key has a token bucket (your plan's requests per second), and purchases have a separate, tighter bucket per account. When one is empty you receive errno 7 with HTTP 429; back off briefly and retry. Poll /sms/check every 3 to 5 seconds per order.
Error codes (errno)
| errno | HTTP | meaning |
|---|---|---|
| 0 | 200 | success |
| 1 | 401 | API key missing or invalid; 403 when the key lacks otp:receive, the account is inactive, or the IP is not on your allowlist |
| 2 | 400 | invalid or missing parameters |
| 3 | 400 | insufficient balance; response includes required and balance |
| 4 | 400 | no numbers available for this service / country (also returned when a pair has no price yet) |
| 5 | 404 | order not found, or it belongs to another key |
| 6 | 400 | order is already finished; on a cancel, status_code: 3 with code means the SMS arrived first |
| 7 | 429 | rate limit exceeded |
| 8 | 400 | number service error; the upstream carrier declined or failed |
| 9 | 503 | this service is currently disabled |
| 10 | 400 | too early to cancel; wait_seconds says how long is left |
| 11 | 409 | a purchase with this Idempotency-Key is still running |
| 12 | 409 | this Idempotency-Key was already used for a different purchase |
| 99 | 500 | internal server error |
Failures also set success: 0.
Getting started
- Create a SafeCode account and generate a key at https://safecode.vip/numbers-api.
- Fund your wallet.
GET /request/priceto confirm the pair is in stock and see the cost.POST /purchase/sms→ then pollPOST /sms/checkevery few seconds untilstatusis3.- If nothing arrives you can
POST /sms/cancelfrom 3 minutes after purchase, or just let the order expire: both are refunded automatically.