DEV Community

Cover image for How to receive SMS verification codes in your scripts (handler_api.php in Python and Node)
Viktor
Viktor

Posted on

How to receive SMS verification codes in your scripts (handler_api.php in Python and Node)

If your automation ever hits a "we sent you a code" screen, you need a phone number you control from code. Most SMS activation services speak the same plain-text protocol — a single handler_api.php endpoint — so once you've written a client for it, you can switch providers by changing two strings.

This post walks through that protocol with working Python and Node code: buy a number, wait for the code, ask for another one, cancel and get your money back.

Disclosure: I run RelySMS, so the examples point at it. The code works with any service that implements the same protocol — swap the URL and the key.

The protocol in 60 seconds

Everything goes to one URL, with api_key and action in the query string. Responses are plain text: a status word, sometimes followed by :-separated values.

Action What it does Typical answer
getBalance Balance in USD ACCESS_BALANCE:25.00
getNumber Buy a number for a service in a country ACCESS_NUMBER:1042:15550001111
getStatus Has the code arrived? STATUS_WAIT_CODE / STATUS_OK:48213
setStatus 3 another code, 6 finish, 8 cancel ACCESS_RETRY_GET / ACCESS_ACTIVATION / ACCESS_CANCEL
getPrices Price and stock per country and service JSON
getServicesList / getCountries The codes you pass to getNumber JSON

Services and countries are short codes: tg is Telegram, country=187 is the USA. Fetch the full lists once with getServicesList and getCountries instead of hard-coding them.

The lifecycle of one order:

  1. getNumber — money is reserved, you get an order id and a phone number.
  2. Type the number into the site that wants to verify you.
  3. Poll getStatus every 5–10 seconds until it says STATUS_OK:<code>.
  4. setStatus=6 to finish — or setStatus=8 to cancel if nothing came.

Python

import time
import requests

API = "https://relysms.pro/stubs/handler_api.php"
KEY = "YOUR_API_KEY"


def call(action, **params):
    r = requests.get(API, params={"api_key": KEY, "action": action, **params}, timeout=30)
    r.raise_for_status()
    return r.text.strip()


def buy(service, country, max_price):
    answer = call("getNumber", service=service, country=country, maxPrice=max_price)
    if not answer.startswith("ACCESS_NUMBER:"):
        # NO_NUMBERS, NO_BALANCE, BAD_SERVICE, WRONG_MAX_PRICE:0.59 ...
        raise RuntimeError(answer)
    _, order_id, phone = answer.split(":")
    return order_id, phone


def wait_for_code(order_id, give_up_after=120):
    started = time.time()
    while time.time() - started < give_up_after:
        status = call("getStatus", id=order_id)
        if status.startswith("STATUS_OK:"):
            return status.split(":", 1)[1]
        if status == "STATUS_CANCEL":
            return None
        time.sleep(5)
    return None


def get_code(service="tg", country="187", max_price=0.80, attempts=3):
    for _ in range(attempts):
        order_id, phone = buy(service, country, max_price)
        print("Enter this number in the form:", phone)

        code = wait_for_code(order_id)
        if code:
            call("setStatus", id=order_id, status=6)  # done, close the order
            return phone, code

        # Nothing in 2 minutes: cancel (instant refund) and try a fresh number.
        print("No code, cancelling:", call("setStatus", id=order_id, status=8))
    raise RuntimeError("no code after %d numbers" % attempts)


if __name__ == "__main__":
    print(call("getBalance"))
    print(get_code())
Enter fullscreen mode Exit fullscreen mode

A few things worth noticing:

  • maxPrice protects you from buying at a price you didn't expect. If the current price is higher, nothing is charged and you get WRONG_MAX_PRICE:<current price> back.
  • Cancel after two minutes, not twenty. A number that hasn't received anything in two minutes rarely will. Cancelling early refunds instantly and frees you to try another one.
  • If you forget to cancel, the order closes itself when its 20-minute window ends and the money goes back to the balance anyway.

Node.js

Same flow, using built-in fetch (Node 18+):

const API = "https://relysms.pro/stubs/handler_api.php";
const KEY = process.env.SMS_API_KEY;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function call(action, params = {}) {
  const url = new URL(API);
  url.search = new URLSearchParams({ api_key: KEY, action, ...params });
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.text()).trim();
}

async function getCode({ service = "tg", country = "187", maxPrice = "0.80" } = {}) {
  const answer = await call("getNumber", { service, country, maxPrice });
  if (!answer.startsWith("ACCESS_NUMBER:")) throw new Error(answer);
  const [, id, phone] = answer.split(":");
  console.log("Enter this number:", phone);

  const deadline = Date.now() + 120_000;
  while (Date.now() < deadline) {
    const status = await call("getStatus", { id });
    if (status.startsWith("STATUS_OK:")) {
      await call("setStatus", { id, status: "6" });
      return { phone, code: status.slice("STATUS_OK:".length) };
    }
    if (status === "STATUS_CANCEL") break;
    await sleep(5000);
  }
  console.log("No code:", await call("setStatus", { id, status: "8" }));
  return null;
}

getCode().then(console.log, console.error);
Enter fullscreen mode Exit fullscreen mode

Getting a second code on the same number

Some flows send a code twice — sign-up, then a login confirmation a minute later. While the order is still open, ask for another one:

call("setStatus", id=order_id, status=3)   # -> ACCESS_RETRY_GET
# poll getStatus again: STATUS_WAIT_RETRY:<old code> until the new one arrives,
# then STATUS_OK:<new code>
Enter fullscreen mode Exit fullscreen mode

Errors you will actually see

Answer Meaning What to do
NO_NUMBERS Nothing in stock for this service and country Try another country
NO_BALANCE Not enough money Top up
WRONG_MAX_PRICE:0.59 Price is above your maxPrice Raise the limit or pick another country
BAD_SERVICE / BAD_COUNTRY Unknown code Check getServicesList / getCountries
EARLY_CANCEL_DENIED Cancelled within the first 2 minutes Wait, then cancel
BAD_KEY Wrong API key Check the key

Plugging it into automation tools

Because the protocol is shared, tools with SMS-service settings usually let you add a provider by URL. In ZennoPoster (7.2.1+), for example: Settings → SMS → "Add your service", API SmsActivate, server https://relysms.pro/stubs/handler_api.php, plus your key.

If you're on RelySMS specifically

  • 700+ services, 189 countries, from $0.07 per code; prices are public at relysms.pro/numbers.
  • API purchases need a balance of $20 or more; below that getNumber returns NO_BALANCE and everything else still works.
  • Limits: 20 requests/s and 5 purchases/s per key, 50 open orders at once.
  • There's also a REST API (/api/v1) with batch purchase of up to 20 numbers, Idempotency-Key and HMAC-signed webhooks, if you'd rather not poll.
  • Full docs: relysms.pro/developers.

Use it for your own accounts and your own testing. Spam and fraud are against the terms — and the services you're verifying with will catch up with them anyway.

Questions about the protocol or edge cases — ask in the comments.

Top comments (0)