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:
-
getNumber— money is reserved, you get an order id and a phone number. - Type the number into the site that wants to verify you.
- Poll
getStatusevery 5–10 seconds until it saysSTATUS_OK:<code>. -
setStatus=6to finish — orsetStatus=8to 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:
-
maxPriceprotects you from buying at a price you didn't expect. If the current price is higher, nothing is charged and you getWRONG_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
getNumberreturnsNO_BALANCEand 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-Keyand 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.