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.
Top comments (0)