Getting Started with the SMSCode API

Getting Started with the SMSCode API

If you’re building anything that touches phone verification — account provisioning, QA automation, multi-region testing — doing it by hand through a dashboard gets old fast. The SMSCode API lets you automate the entire OTP flow from your backend: request a number, wait for the SMS, extract the code, move on. No clicking required.

This guide walks through authentication, the core endpoints, working code examples in both curl and Python, and a few common mistakes that’ll save you debugging time.

TL;DR: The SMSCode V1 API uses bearer token auth. The core flow is: check balance → browse catalog → create order → poll for OTP → complete or cancel. All responses follow { success, data } or { success, error } shapes. Rate limits apply; cache your catalog calls.

Authentication

Every request to the V1 API needs a bearer token in the Authorization header. You’ll find yours under Account Settings in the dashboard.

Authorization: Bearer YOUR_API_TOKEN

Keep this token server-side. Don’t put it in client-side JavaScript, environment files committed to git, or anywhere else it could leak. If a token is compromised, regenerate it from your account settings immediately.

The full API reference is in the docs. If you haven’t created an account yet, sign up here — you’ll need a balance to place orders.

Step 1: Verify your balance

Before placing any orders, check that your account has enough credits:

curl -X GET https://api.smscode.gg/v1/balance \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response:

{
  "success": true,
  "data": {
    "currency": "IDR",
    "balance": 150000
  }
}

Balance is denominated in IDR (Indonesian Rupiah). If it’s zero or lower than the product you want, add funds on the pricing page before continuing.

Python equivalent:

import requests

API_TOKEN = "YOUR_API_TOKEN"
BASE_URL = "https://api.smscode.gg/v1"
headers = {"Authorization": f"Bearer {API_TOKEN}"}

resp = requests.get(f"{BASE_URL}/balance", headers=headers)
data = resp.json()
print(f"Balance: {data['data']['balance']} {data['data']['currency']}")

Step 2: Browse the catalog

The catalog endpoint returns available virtual number products, filterable by country and platform. This is where you find the product_id you’ll use to place an order.

curl -X GET "https://api.smscode.gg/v1/catalog/products?country=indonesia&platform=whatsapp" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

The response includes product IDs, pricing, and available stock levels. You can filter by any combination of country and platform — the virtual number catalog on the website gives you the same data visually if you want to explore first. For example, to get numbers for WhatsApp verification, filter by platform=whatsapp and choose from available countries.

Practical advice: Don’t fetch the catalog on every order. Product lists don’t change by the second. Cache it for at least 60 seconds in production, longer if your traffic is high. This respects rate limits and keeps latency down.

Not sure which country to pick? The country selection guide covers the tradeoffs between price, reliability, and platform compatibility in detail.

Python equivalent:

params = {"country": "indonesia", "platform": "whatsapp"}
resp = requests.get(f"{BASE_URL}/catalog/products", headers=headers, params=params)
products = resp.json()["data"]
# Pick lowest-price product with stock available
product = min(products, key=lambda p: p["price"])
product_id = product["id"]

Step 3: Create an order

Once you have a product_id, create an order to rent the number:

curl -X POST https://api.smscode.gg/v1/orders \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"product_id": 42}'

Response:

{
  "success": true,
  "data": {
    "order_id": "ord_abc123",
    "phone_number": "+628123456789",
    "status": "WAITING",
    "expires_at": "2026-03-14T12:05:00Z"
  }
}

The phone_number is what you enter in the app or website you’re verifying. The order status starts as WAITING — that’s normal. Now go trigger the OTP from the target service using that number.

Python equivalent:

resp = requests.post(
    f"{BASE_URL}/orders",
    headers={**headers, "Content-Type": "application/json"},
    json={"product_id": product_id}
)
order = resp.json()["data"]
order_id = order["order_id"]
phone_number = order["phone_number"]
print(f"Use this number: {phone_number}")

Step 4: Poll for the OTP

Check the order status until the SMS arrives:

curl -X GET https://api.smscode.gg/v1/orders/ord_abc123 \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response when SMS arrives:

{
  "success": true,
  "data": {
    "order_id": "ord_abc123",
    "status": "OTP_RECEIVED",
    "otp_code": "847291",
    "phone_number": "+628123456789"
  }
}

The status field tells you where you are:

  • WAITING — number activated, no SMS yet
  • OTP_RECEIVED — code is in the response, ready to use
  • COMPLETED — order finished
  • CANCELLED — order was cancelled (refund issued if no OTP was received)
  • EXPIRED — the verification window closed

Python polling loop:

import time

max_wait = 120  # seconds
interval = 5    # poll every 5 seconds
elapsed = 0

while elapsed < max_wait:
    resp = requests.get(f"{BASE_URL}/orders/{order_id}", headers=headers)
    order_data = resp.json()["data"]
    status = order_data["status"]

    if status == "OTP_RECEIVED":
        otp = order_data["otp_code"]
        print(f"OTP received: {otp}")
        break
    elif status in ("CANCELLED", "EXPIRED"):
        print(f"Order ended with status: {status}")
        break

    time.sleep(interval)
    elapsed += interval
else:
    print("Timed out waiting for OTP")

A few things to know about polling: 5-second intervals are reasonable for most use cases. Going faster burns through rate limit budget without meaningful benefit — SMS networks don’t deliver codes faster just because you ask more often.

Step 5: Complete or cancel

When you’ve used the OTP, the order completes automatically. If you don’t need the number anymore — because you’re changing strategy mid-run, or the OTP arrived but verification failed — cancel it:

curl -X POST https://api.smscode.gg/v1/orders/ord_abc123/cancel \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Cancelling before the OTP arrives issues an automatic refund. Cancelling after the OTP has been received doesn’t — the delivery was successful at that point.

Error handling

All API errors follow the same shape:

{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Account balance is too low to place this order."
  }
}

Common error codes you’ll encounter:

CodeWhat it means
UNAUTHORIZEDInvalid or missing token
INSUFFICIENT_BALANCENot enough credits — top up
PRODUCT_UNAVAILABLEStock ran out between catalog fetch and order
ORDER_NOT_FOUNDWrong order ID or order doesn’t belong to your account
RATE_LIMITEDToo many requests — back off

The PRODUCT_UNAVAILABLE error catches a lot of people off guard. Catalog stock is live data. By the time you fetch the catalog and place an order, popular products can sell out. Always handle this error with a retry on a different product from the same catalog response.

Rate limits

The API enforces per-account rate limits. Hitting the limit returns a 429 status with a RATE_LIMITED error code. Standard accounts have limits that cover normal automation workloads.

Two practices that keep you well under limits:

  1. Cache catalog responses — don’t fetch the full catalog on every order cycle
  2. Poll at 5-second intervals rather than hammering the order endpoint

If you’re running bulk operations at scale and standard limits aren’t enough, reach out through the support channel. The API integration guide covers high-volume patterns in more depth.

Building a complete automation flow

For teams building production systems, here’s a pattern that handles the most common edge cases:

import requests
import time
from typing import Optional

API_TOKEN = "YOUR_API_TOKEN"
BASE_URL = "https://api.smscode.gg/v1"
headers = {"Authorization": f"Bearer {API_TOKEN}"}

def get_virtual_number(country: str, platform: str) -> Optional[dict]:
    """Get a virtual number, trying multiple products if needed."""
    params = {"country": country, "platform": platform}
    resp = requests.get(f"{BASE_URL}/catalog/products", headers=headers, params=params)
    products = resp.json().get("data", [])

    if not products:
        return None

    # Sort by price, try in order
    products.sort(key=lambda p: p["price"])

    for product in products:
        resp = requests.post(
            f"{BASE_URL}/orders",
            headers={**headers, "Content-Type": "application/json"},
            json={"product_id": product["id"]}
        )
        result = resp.json()

        if result.get("success"):
            return result["data"]

        # If product unavailable, try next
        if result.get("error", {}).get("code") == "PRODUCT_UNAVAILABLE":
            continue

        # Other errors — stop
        break

    return None

def wait_for_otp(order_id: str, timeout: int = 120) -> Optional[str]:
    """Poll for OTP with timeout."""
    deadline = time.time() + timeout

    while time.time() < deadline:
        resp = requests.get(f"{BASE_URL}/orders/{order_id}", headers=headers)
        data = resp.json().get("data", {})

        if data.get("status") == "OTP_RECEIVED":
            return data["otp_code"]

        if data.get("status") in ("CANCELLED", "EXPIRED"):
            return None

        time.sleep(5)

    return None

# Usage
order = get_virtual_number("indonesia", "whatsapp")
if order:
    print(f"Phone: {order['phone_number']}")
    # ... trigger OTP on target service ...
    otp = wait_for_otp(order["order_id"])
    if otp:
        print(f"OTP: {otp}")
    else:
        print("No OTP received")

This pattern handles PRODUCT_UNAVAILABLE gracefully, respects rate limits through the 5-second polling interval, and has a clear timeout.

Common mistakes to avoid

Not handling PRODUCT_UNAVAILABLE. Stock can empty between your catalog call and your order. Always catch this and fall back to the next product in your catalog results.

Polling too aggressively. Hitting the order endpoint every second won’t make SMS arrive faster. It will, however, get you rate limited faster. Five seconds is the sweet spot.

Hardcoding product IDs. Product IDs can change. Always resolve the ID dynamically from the catalog at runtime, filtering by your target country and platform.

Ignoring expires_at. Orders have a finite verification window. If you wait too long to trigger the OTP after getting the number, the order expires. Trigger the OTP from the target service immediately after getting the phone number.

Not caching the catalog. The catalog endpoint can be called reasonably often, but fetching it on every single order in a high-volume loop is wasteful and will eat into your rate limit budget.

Assuming one country will always work. Different platforms have different acceptance rates for different country numbers. Build in fallback logic to try alternative countries if your primary choice fails.

What’s coming

Webhooks are on the roadmap — instead of polling, your endpoint will receive a push notification the moment an OTP arrives. This will simplify the integration considerably for anyone building event-driven systems. Subscribe to product updates in the dashboard to get notified when it ships.

For a broader look at virtual number services and how SMSCode fits in, see the best virtual number services guide. If cost is a factor, finding cheap virtual numbers has a practical breakdown of where to look.


FAQ

Where do I find my API token?

In the dashboard under Account Settings. If you haven’t created an account yet, sign up here — it takes under a minute.

Is there a sandbox or test mode?

Not currently. All API calls run against the live system with real numbers. For development and testing, use the cheapest available products — costs stay low when you’re just validating your integration logic.

What’s the difference between COMPLETED and OTP_RECEIVED?

OTP_RECEIVED means the SMS arrived and the code is available. COMPLETED means the order lifecycle is fully done. In practice, you act on OTP_RECEIVED — that’s when the code is in the response.

Can I use the API to receive multiple OTPs from the same number?

Some platforms send follow-up codes to the same number. If a second SMS arrives on an active order, the otp_code field updates to the latest code. Keep polling until you have what you need or the order expires.

How do I handle a failed verification where the OTP did arrive?

If the OTP arrived but the target service rejected it (wrong code, expired, etc.), cancel the current order (no refund at this point), create a new order, and try again with a fresh number. The number quality guide has tips on choosing products with better delivery and acceptance rates.

What languages does the API work with?

The API is a standard REST API over HTTPS. It works with any language that can make HTTP requests — Python, Node.js, Go, Ruby, PHP, Java, and so on. The examples in this guide use Python and curl, but the concepts translate directly.

How do I pick the best product when multiple options are available?

Sort catalog results by price and start with the cheapest option that has available stock. If that fails with PRODUCT_UNAVAILABLE, try the next. For platforms where delivery rates vary significantly by country, the country selection guide has specific recommendations.

Ready to try SMSCode?

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

Get started →