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()) 

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); 

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> 

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.