Virtual Number API Integration Guide — Automate SMS Verification

Virtual Number API Integration Guide — Automate SMS Verification

TL;DR — The SMSCode API is a JSON REST API at https://api.smscode.gg/v1. Authenticate with a Bearer token, create an order with POST /v1/orders, poll GET /v1/orders/{id} every 5–10 seconds until status is completed, then read otp_code. Implement exponential backoff on 429s, treat expired and cancelled as 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.

PropertyValue
Base URLhttps://api.smscode.gg/v1
AuthBearer token in Authorization header
Response format{ "success": true/false, "data": ... }
Rate limit300 requests per minute per token
EncodingUTF-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 — not time.time(), which can jump if the system clock is adjusted.
  • expired and cancelled are terminal — when the order reaches either state, the balance is refunded automatically. Don’t retry on the same order; create a new one.
  • Network Timeout vs 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 codeMeaningAction
INSUFFICIENT_BALANCEBalance too low to create orderPrompt user to deposit or halt
PRODUCT_UNAVAILABLENo numbers in stock for this country/platformTry a different country
ORDER_NOT_FOUNDOrder ID doesn’t exist or doesn’t belong to your tokenCheck the order ID
RATE_LIMIT_EXCEEDEDPer-minute limit hitExponential backoff
INVALID_PARAMETERSMalformed request bodyFix 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.


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.

Ready to try SMSCode?

Create an account and get your first virtual number in under two minutes.

Get started →