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 errno codes.
  • One wallet: API purchases debit your main SafeCode balance. No separate top-up.
  • Honest pricing: the price /request/price quotes is the price /purchase/sms charges.
  • 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-Key header 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/price quotes. max_price below that price returns errno 4 with the current price; a higher max_price lets 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 returns errno 12; a retry while the first request is still running returns errno 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:

  • 1 waiting
  • 3 received; code and sms are populated
  • 4 expired (refunded)
  • 6 cancelled 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 returns errno 10 with wait_seconds.
  • If the SMS arrived first, nothing is cancelled: you get errno 6 with status_code: 3 and the code, so you never lose a code you paid for.
  • A finished order returns errno 6 with status (text) and status_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.

  • limit 1 to 100 (default 25), offset for paging, status to filter by status code (1, 3, 4 or 6).
  • Each order has status_code (same values as /sms/check) and service, service_code, country, country_code.

WhatsApp

WhatsApp numbers are available and are fulfilled through a dedicated upstream pool rather than the default one. Two things differ:

  • /sms/resend is not supported for WhatsApp orders and returns errno 8. Keep polling /sms/check; the activation window is the usual 20 minutes.
  • Availability is per country. Ask /request/price?country=…&service=whatsapp first; errno 4 means 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.code holds 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

  1. Create a SafeCode account and generate a key at https://safecode.vip/numbers-api.
  2. Fund your wallet.
  3. GET /request/price to confirm the pair is in stock and see the cost.
  4. POST /purchase/sms → then poll POST /sms/check every few seconds until status is 3.
  5. If nothing arrives you can POST /sms/cancel from 3 minutes after purchase, or just let the order expire: both are refunded automatically.

© 2026 SafeCode. SafeCode and safecode.vip are operated by SAFECODE TECHNOLOGIES LTD, a private company limited by shares incorporated in Ghana under the Companies Act, 2019 (Act 992) (Reg. No. CS216101026, TIN C0068071701). M EXT 24 Malshegu East, Tamale-Kumbungu Road, Tamale, Northern Region, Ghana. support@safecode.vip

SafeCode