TL;DR — The SMSCode API is a JSON REST API at
https://api.smscode.gg/v1. Authenticate with a Bearer token, create an order withPOST /v1/orders, pollGET /v1/orders/{id}every 5–10 seconds until status iscompleted, then readotp_code. Implement exponential backoff on 429s, treatexpiredandcancelledas terminal states that trigger a retry, and never poll faster than 5 seconds. Full error codes in the API docs.
Manual SMS verification through a browser doesn’t scale. Once you need to verify more than a handful of accounts, or you need OTP flows embedded in automated tests, you need a virtual number API that your code can drive directly.
This guide covers the complete integration path — authentication, order flow, polling, error handling, retry logic, testing strategies, and how to structure your integration for production reliability. Code examples are in cURL, JavaScript, and Python throughout.
When a virtual number API is the right tool
API access solves a specific set of problems that dashboard workflows can’t handle:
QA automation and CI/CD pipelines — End-to-end tests that involve account creation or phone verification need a way to request a number, read the OTP, and pass it back to the test without a human in the loop. Without API access, these tests require manual intervention every run. Common targets include WhatsApp, Discord, and other platforms with SMS-gated flows.
Multi-account workflows — Any process that creates or verifies multiple accounts in sequence benefits from programmatic number management. You can parallelize, retry automatically, and track results without switching tabs.
Onboarding pipelines — If your product helps users sign up for third-party services as part of its core flow, embedding phone verification into your pipeline is cleaner and faster than directing users through a separate manual step.
Cost monitoring — Programmatic access lets you track per-verification cost, flag unexpectedly high failure rates, and cancel orders automatically when they’re no longer needed — all of which controls your spend.
For context on what virtual numbers are and how they work, see the complete virtual number guide. For a broader look at the market, see best virtual number services.
API fundamentals
The SMSCode API is a standard REST API with consistent JSON responses.
| Property | Value |
|---|---|
| Base URL | https://api.smscode.gg/v1 |
| Auth | Bearer token in Authorization header |
| Response format | { "success": true/false, "data": ... } |
| Rate limit | 300 requests per minute per token |
| Encoding | UTF-8 JSON |
Every response follows the same envelope. Success responses carry "success": true and a data object. Error responses carry "success": false and an error object with code and message fields. This consistency means you write one response handler that works for every endpoint.
Getting started
1. Create an account and generate a token
Sign up for a free account, then go to Account Settings → API Token to generate your token. Store it in an environment variable — never hardcode it in source files.
export SMSCODE_TOKEN="your_token_here"
Treat your API token like a password. Anyone with the token can spend your balance and create orders on your account.
2. Verify authentication with a balance check
Before writing order logic, confirm your token works:
curl -s -H "Authorization: Bearer $SMSCODE_TOKEN" \
https://api.smscode.gg/v1/balance
Expected response:
{
"success": true,
"data": {
"currency": "IDR",
"balance": 150000
}
}
If you get a 401, the token is wrong or missing. If you get a 403, the token is valid but lacks the required permissions.
3. Explore the catalog
Before creating orders, query the catalog to find valid country_id and platform_id values for the verification you need.
List countries:
curl -s -H "Authorization: Bearer $SMSCODE_TOKEN" \
https://api.smscode.gg/v1/catalog/countries
List products (filtered by country):
curl -s -H "Authorization: Bearer $SMSCODE_TOKEN" \
"https://api.smscode.gg/v1/catalog/products?country_id=6"
The catalog response includes country_id, platform_id, price, and available_count for each product. Cache these responses — the catalog updates periodically, not per-request. A 60-second TTL is reasonable.
For information on choosing the right country for your verification, see that dedicated guide.
Core order flow
The standard flow for a single verification is three steps: create order → use the number on the target platform → poll until OTP arrives.
Step 1: Create an order
import os
import requests
API_TOKEN = os.environ["SMSCODE_TOKEN"]
BASE_URL = "https://api.smscode.gg/v1"
def create_order(country_id: int, platform_id: int) -> dict:
response = requests.post(
f"{BASE_URL}/orders",
json={"country_id": country_id, "platform_id": platform_id},
headers={"Authorization": f"Bearer {API_TOKEN}"},
timeout=10,
)
response.raise_for_status()
result = response.json()
if not result["success"]:
raise ValueError(f"Order failed: {result['error']['code']} — {result['error']['message']}")
return result["data"]
The response includes order_id, phone_number, status, and expires_at. Use phone_number immediately on the target platform to trigger the verification SMS.
order = create_order(country_id=6, platform_id=1)
print(f"Phone number: {order['phone_number']}")
print(f"Order ID: {order['order_id']}")
print(f"Expires at: {order['expires_at']}")
Step 2: Poll for OTP delivery
After triggering the verification SMS on the target platform, poll the order endpoint at a fixed interval until the status is completed, expired, or cancelled.
import time
def wait_for_otp(order_id: str, timeout: int = 90, interval: int = 5) -> str | None:
"""
Poll an order until OTP arrives or the order reaches a terminal state.
Returns the OTP code string, or None if the order expired or timed out.
"""
url = f"{BASE_URL}/orders/{order_id}"
headers = {"Authorization": f"Bearer {API_TOKEN}"}
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
data = resp.json()
if not data["success"]:
code = data["error"]["code"]
# Non-retryable API error — surface immediately
raise ValueError(f"API error: {code} — {data['error']['message']}")
status = data["data"]["status"]
if status == "completed":
return data["data"]["otp_code"]
if status in ("expired", "cancelled"):
# Balance auto-refunded by the server — no action needed
return None
# status is "pending" or "active" — keep waiting
time.sleep(interval)
except requests.exceptions.Timeout:
# Network timeout — safe to retry
time.sleep(interval)
return None # Local timeout exceeded
Key decisions in this pattern:
- 5-second minimum interval — polling faster wastes rate limit quota without improving latency. Most OTPs arrive within 15–30 seconds; a 5-second interval catches them promptly.
time.monotonic()for deadlines — nottime.time(), which can jump if the system clock is adjusted.expiredandcancelledare terminal — when the order reaches either state, the balance is refunded automatically. Don’t retry on the same order; create a new one.- Network
Timeoutvs API errors — a network timeout is safe to retry; an API error with a 4xx code (except 429) indicates a problem with the request itself.
Step 3: Complete the flow
def verify_account(country_id: int, platform_id: int) -> str | None:
order = create_order(country_id, platform_id)
phone = order["phone_number"]
order_id = order["order_id"]
print(f"Enter this number on the target platform: {phone}")
# Trigger the SMS on the target platform here (browser automation, API call, etc.)
otp = wait_for_otp(order_id)
if otp is None:
print("OTP not received — order expired, balance refunded.")
return None
print(f"OTP received: {otp}")
return otp
Error handling reference
Different errors require different responses. Here’s the full decision tree:
HTTP 429 — Rate limit exceeded
You’ve sent more than 300 requests per minute. Implement exponential backoff:
async function apiRequest(url, options, attempt = 0) {
const response = await fetch(url, options);
if (response.status === 429) {
if (attempt >= 5) throw new Error("Rate limit: too many retries");
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
return apiRequest(url, options, attempt + 1);
}
return response;
}
The backoff sequence: 1s → 2s → 4s → 8s → 16s → cap at 30s. Always respect the Retry-After header if the server sends one.
HTTP 5xx — Server error
Transient server errors are safe to retry with the same backoff pattern. Cap at 3 retries. If the server is consistently returning 5xx, stop retrying and surface the error — continued retries will only consume rate limit quota.
HTTP 4xx (except 429) — Client error
Don’t retry. The error indicates a problem with the request: invalid parameters, insufficient balance, product unavailable. Read the error.code field and handle each case:
| Error code | Meaning | Action |
|---|---|---|
INSUFFICIENT_BALANCE | Balance too low to create order | Prompt user to deposit or halt |
PRODUCT_UNAVAILABLE | No numbers in stock for this country/platform | Try a different country |
ORDER_NOT_FOUND | Order ID doesn’t exist or doesn’t belong to your token | Check the order ID |
RATE_LIMIT_EXCEEDED | Per-minute limit hit | Exponential backoff |
INVALID_PARAMETERS | Malformed request body | Fix the request structure |
async function createOrder(countryId, platformId) {
const response = await fetch("https://api.smscode.gg/v1/orders", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SMSCODE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ country_id: countryId, platform_id: platformId }),
});
const result = await response.json();
if (!result.success) {
const { code, message } = result.error;
switch (code) {
case "INSUFFICIENT_BALANCE":
throw new Error("Balance too low — add funds before retrying.");
case "PRODUCT_UNAVAILABLE":
throw new Error(`No stock for country ${countryId}, platform ${platformId}. Try another country.`);
default:
throw new Error(`API error ${code}: ${message}`);
}
}
return result.data;
}
Cancelling orders
If you no longer need a number — the verification was abandoned, or you’ve decided to try a different country — cancel the order explicitly:
curl -s -X DELETE \
-H "Authorization: Bearer $SMSCODE_TOKEN" \
https://api.smscode.gg/v1/orders/{order_id}
Early cancellation refunds your balance faster than waiting for the order to expire naturally. Build cancellation into your timeout handling so you’re not holding open orders that waste stock.
Rate limit strategy for high-volume use
At 300 requests per minute per token, the limit is generous for most use cases. For high-volume parallelization, consider:
Batch carefully. If you’re running 50 concurrent verifications, you’ll generate polling traffic quickly. With a 5-second interval and 50 orders in flight, that’s 10 requests per second — 600 per minute, over the limit. Spread polling with staggered start times or group orders into batches of 20–25.
Cache the catalog. GET /v1/catalog/countries and GET /v1/catalog/products don’t need to be called per-order. Cache them with a short TTL (60–300 seconds). This alone reduces your per-order request count significantly.
Count requests per action. A single verification generates: 1 catalog lookup (cached) + 1 create order + N poll requests. At 5-second intervals and a 90-second timeout, that’s at most 18 poll requests per order. Factor this into your concurrency planning.
Testing strategies
Unit tests: mock the API responses
Don’t call the real API in unit tests. Mock the HTTP layer to return controlled responses for each scenario:
from unittest.mock import patch, MagicMock
def test_wait_for_otp_success():
mock_responses = [
{"success": True, "data": {"status": "pending", "otp_code": None}},
{"success": True, "data": {"status": "completed", "otp_code": "847291"}},
]
with patch("requests.get") as mock_get:
mock_get.side_effect = [
MagicMock(json=lambda: r, raise_for_status=lambda: None)
for r in mock_responses
]
result = wait_for_otp("order-123", timeout=30, interval=0)
assert result == "847291"
def test_wait_for_otp_expired():
with patch("requests.get") as mock_get:
mock_get.return_value = MagicMock(
json=lambda: {"success": True, "data": {"status": "expired", "otp_code": None}},
raise_for_status=lambda: None,
)
result = wait_for_otp("order-456", timeout=30, interval=0)
assert result is None
Test every terminal state: completed, expired, cancelled. Test INSUFFICIENT_BALANCE and PRODUCT_UNAVAILABLE error responses. Test the 429 backoff path. These scenarios are predictable — test them without spending real API credits.
Integration tests: use real API with test balance
For integration testing against the real API, use a separate test API token with a small balance. Run these tests against a subset of verifications to confirm the end-to-end flow works. Don’t run integration tests in CI on every commit — they cost real money and depend on external stock availability.
Verify your polling interval
A common mistake is polling too aggressively in tests by setting interval=0. This passes tests but masks rate limit issues in production. Test your polling logic with realistic intervals or mock the sleep to verify the timing logic without actually sleeping.
What to read next
- API documentation — full endpoint reference, all query parameters, response schemas
- Pricing — per-verification cost across countries and platforms
- Choosing the right country — which countries to try first for each platform
- Getting started guide — simplified walkthrough for first-time API users
- Service comparison — how SMSCode’s API compares to alternatives
FAQ
What programming languages does the SMSCode API support?
The API is language-agnostic. Any HTTP client can call it — curl, fetch, requests, axios, Go’s net/http, Rust’s reqwest, or anything else. The examples in this guide use Python and JavaScript, but the same patterns apply in any language. The only requirement is that your HTTP client can set request headers and parse JSON responses.
How do I handle the case where a country runs out of stock?
Check available_count in the catalog response before creating an order. If it’s zero or close to zero, choose an alternative country. Build a priority list of 2–3 countries that work for your target platform and iterate through them on PRODUCT_UNAVAILABLE errors. See choosing the right country for guidance on building that fallback list.
What is the maximum number of concurrent orders my token can have?
The limit is enforced by the rate limit (300 requests per minute) rather than a hard concurrent order cap. In practice, you can manage as many concurrent orders as your polling traffic stays within rate limits. At a 5-second polling interval, each active order generates 12 requests per minute. At 300 requests per minute, that’s approximately 25 concurrent orders before you need to stagger your polling.
How do I secure my API token in a server environment?
Store the token in an environment variable, never in source code or configuration files checked into version control. In container environments, inject via the container runtime’s secrets management (Docker secrets, Kubernetes Secrets, AWS SSM, etc.). In CI/CD, use your pipeline’s secret store. Rotate the token periodically — generate a new one in Account Settings and update it everywhere before revoking the old one. Monitor for unexpected usage spikes that could indicate the token was leaked.
Can I use the API to verify which platforms work best from a specific country?
Not directly via a single endpoint, but you can infer this from the catalog. The available_count field tells you current stock levels. For empirical success rate data, track your own orders: record country, platform, and outcome (completed vs expired) and calculate success rates over time. Our number quality and reliability guide explains the factors that affect success rates by country and platform.