SMS Activation API Guide: Automate OTP Verification with OTPX

Use the OTPX SMS activation API to buy numbers, poll for OTP codes and cancel or finish orders, with curl, JavaScript and Python examples plus best practices.

OTPX Team6 min read

The OTPX API lets you automate the whole SMS verification flow: request a virtual number for a service and country, poll until the OTP arrives, then mark the order finished or cancel it for a refund. It speaks the widely used SMS-Activate protocol, so if you already have a script or tool built for that format, you usually only need to change the base URL and API key.

This guide walks through each call with working examples in curl, JavaScript and Python, then covers the error handling and habits that keep an integration reliable. The full reference lives in the API docs.

Before you start

  1. Create an OTPX account — an email is all you need.
  2. Add funds to your wallet with UPI (from ₹10) or crypto (from ₹90: BTC, USDT, ETH, LTC or TRX). Top-ups carry no fees.
  3. Open your dashboard profile and copy your API key. Treat it like a password.

Every request goes to a single endpoint and uses query parameters:

https://otpx.org/api/stubs/handler_api.php?api_key=YOUR_KEY&action=ACTION_NAME

Responses are short plain-text strings (for example ACCESS_BALANCE:123.45) or JSON for the list-style actions. Always parse the prefix before the first colon to decide what happened.

Step 1: check your balance

getBalance is the simplest call and a good way to confirm your key works.

curl "https://otpx.org/api/stubs/handler_api.php?api_key=YOUR_KEY&action=getBalance"
# ACCESS_BALANCE:123.45

If you see BAD_KEY, the key is wrong, has extra whitespace, or was regenerated.

Step 2: discover services, countries and prices

Before ordering, look up the codes you will pass to getNumber:

  • getServicesList — available services and their codes
  • getCountries — country IDs
  • getPrices and getPricesV2 — current prices and availability

Prices vary by service, country and server (provider), so fetch them at runtime instead of hard-coding them. The pricing page shows the same data in a browser.

Step 3: request a number

getNumber takes service and country, plus optional operator, maxPrice and providerIds. Setting maxPrice is strongly recommended: it puts a ceiling on what a single order can cost.

curl "https://otpx.org/api/stubs/handler_api.php?api_key=YOUR_KEY&action=getNumber&service=tg&country=22&maxPrice=20"
# ACCESS_NUMBER:123456789:919876543210

The response contains the activation id and the phone number. Store the id — every following call uses it. The service and country values above are only examples; use the codes returned by the list endpoints.

If your maxPrice is below what any provider currently charges, you get WRONG_MAX_PRICE:<min>, which tells you the lowest available price. NO_NUMBERS means nothing is available right now for that combination, and NO_BALANCE means you need to top up.

Step 4: poll for the code

Send the number to the app you are verifying, then call getStatus with the id until something changes:

  • STATUS_WAIT_CODE — no SMS yet, keep waiting
  • STATUS_OK:<code> — the OTP has arrived
  • STATUS_CANCEL — the activation was cancelled (by you, or automatically)

Poll every 5–10 seconds and back off if you get errors. There is no benefit in hammering the endpoint, and it may get your requests throttled. If no SMS arrives within 20 minutes, OTPX cancels the number and refunds the full amount to your wallet automatically, so cap your own wait at that point.

Step 5: finish, retry or cancel

setStatus takes the id and a status value:

  • status=6 — finish the activation → ACCESS_ACTIVATION
  • status=3 — request another SMS on the same number → ACCESS_RETRY_GET
  • status=8 — cancel before a code arrives, for a refund → ACCESS_CANCEL

Once a code has been received, the order is complete and can no longer be cancelled for a refund.

curl "https://otpx.org/api/stubs/handler_api.php?api_key=YOUR_KEY&action=setStatus&id=123456789&status=8"
# ACCESS_CANCEL

Full example in JavaScript (fetch)

This works in Node.js 18+ (which has fetch built in). Read the key from an environment variable rather than writing it into the source.

const BASE = "https://otpx.org/api/stubs/handler_api.php"
const KEY = process.env.OTPX_API_KEY

async function call(action, params = {}) {
  const qs = new URLSearchParams({ api_key: KEY, action, ...params })
  const res = await fetch(BASE + "?" + qs.toString())
  return (await res.text()).trim()
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

async function getOtp(service, country, maxPrice) {
  const order = await call("getNumber", { service, country, maxPrice })
  if (!order.startsWith("ACCESS_NUMBER")) throw new Error(order)
  const [, id, phone] = order.split(":")
  console.log("Use this number:", phone)

  const deadline = Date.now() + 20 * 60 * 1000
  let delay = 5000
  while (Date.now() < deadline) {
    await sleep(delay)
    const status = await call("getStatus", { id })
    if (status.startsWith("STATUS_OK")) {
      await call("setStatus", { id, status: 6 })
      return status.split(":")[1]
    }
    if (status === "STATUS_CANCEL") throw new Error("Cancelled")
    delay = Math.min(delay + 1000, 10000)
  }
  await call("setStatus", { id, status: 8 })
  throw new Error("Timed out, number cancelled")
}

Full example in Python (requests)

import os, time, requests

BASE = "https://otpx.org/api/stubs/handler_api.php"
KEY = os.environ["OTPX_API_KEY"]

def call(action, **params):
    params.update(api_key=KEY, action=action)
    r = requests.get(BASE, params=params, timeout=15)
    r.raise_for_status()
    return r.text.strip()

def get_otp(service, country, max_price):
    order = call("getNumber", service=service, country=country, maxPrice=max_price)
    if not order.startswith("ACCESS_NUMBER"):
        raise RuntimeError(order)
    _, act_id, phone = order.split(":")
    print("Use this number:", phone)

    deadline = time.time() + 20 * 60
    delay = 5
    while time.time() < deadline:
        time.sleep(delay)
        status = call("getStatus", id=act_id)
        if status.startswith("STATUS_OK"):
            call("setStatus", id=act_id, status=6)
            return status.split(":", 1)[1]
        if status == "STATUS_CANCEL":
            raise RuntimeError("Cancelled")
        delay = min(delay + 1, 10)

    call("setStatus", id=act_id, status=8)
    raise TimeoutError("No SMS, number cancelled")

Handling errors properly

Map each response to an action instead of treating everything as a generic failure:

  • BAD_KEY — stop immediately and alert someone. Retrying will not help.
  • NO_BALANCE — stop ordering and notify whoever manages top-ups.
  • NO_NUMBERS — try another country or server, or retry later with backoff.
  • WRONG_MAX_PRICE:<min> — decide whether <min> is acceptable; raise your ceiling only if it is.
  • Network errors and timeouts — retry with exponential backoff, but never blindly re-run getNumber (see below).

Best practices for a reliable integration

  • Set timeouts on every HTTP request. A hung connection should not freeze a worker.
  • Back off when polling. Start around 5 seconds and stretch towards 10; slow down further on errors.
  • Be idempotent with orders. If a getNumber call times out, you may not know whether a number was issued. Record the id as soon as you have it, and tie each order to your own job id so a retry does not buy a second number by accident.
  • Cancel unused numbers promptly. If your flow fails before the code is needed, call setStatus with status=8 so the funds return to your wallet straight away rather than after the 20-minute window.
  • Never log or commit your API key. Keep it in environment variables or a secrets manager, redact it from request logs, and rotate it if it leaks.
  • Use maxPrice everywhere. It protects you from unexpected price changes across providers.
  • Remember numbers are temporary. They are for receiving verification codes only; do not design a system that expects to receive messages on the same number weeks later.

Using existing SMS-Activate tools

Because OTPX follows the SMS-Activate protocol — the same action names, parameters and response strings — many existing libraries, bots and automation tools work with it. Point their base URL at https://otpx.org/api/stubs/handler_api.php, paste in your OTPX API key, and test with getBalance first. Service and country codes may differ between providers, so check them with getServicesList and getCountries.

Use it responsibly

The API is meant for legitimate automation such as QA testing of sign-up flows, managing verified business accounts and privacy-preserving workflows. Do not use it for spam, fake accounts, fake reviews or getting around a platform's bans, and follow the terms of every service you verify with. Our acceptable use policy has the details. If a code is slow to arrive, the OTP not received fixes guide covers the usual causes, and support is available at [email protected] or on Telegram at @OTPX_support.

Frequently asked questions

Is the OTPX API compatible with SMS-Activate?

Yes. It uses the SMS-Activate protocol, so most tools built for it work after you change the base URL to the OTPX endpoint and use your OTPX API key.

Where do I find my OTPX API key?

Your API key is in your dashboard profile after you sign in. Keep it secret and store it in an environment variable or secrets manager.

How often should I call getStatus?

Poll every 5 to 10 seconds and back off if you see errors. Polling faster does not make the SMS arrive sooner.

What does WRONG_MAX_PRICE mean?

Your maxPrice is lower than the cheapest available number for that service and country. The value after the colon shows the current minimum, so you can decide whether to raise your limit.

Do I get refunded if no SMS arrives?

Yes. If no SMS arrives within 20 minutes the number is cancelled and the full amount returns to your wallet automatically, and you can cancel earlier with setStatus status=8.

All articles