Most people arriving here already have working code. It talks to a handler_api.php endpoint, it parses colon-separated tokens, and it has been quietly doing its job for three years. Asking that code to be rewritten before you can evaluate us is a bad trade for everyone, so we implemented the old protocol instead.
Step one: change two constants
- API_URL=https://api.sms-activate.io/stubs/handler_api.php
- API_KEY=8f3c1d...
+ API_URL=https://virtualsmsnumbers.com/stubs/handler_api.php
+ API_KEY=vsn_live_a8Kd2rQ4xN7pLmT1yBcThat is the whole migration for the compatibility path. Action names, parameter names and the response vocabulary are preserved: getBalance, getNumbersStatus, getPrices, getNumber, getStatus, setStatus, and the ACCESS_ / STATUS_ tokens they return. GET and form-encoded POST both work. Underneath it is the same domain code as the REST API, so the same balance, the same automatic refunds and the same routing apply.
-kw">curl "https://virtualsmsnumbers.com/stubs/handler_api.php?api_key=$KEY&action=getBalance"
# ACCESS_BALANCE:47.30
-kw">curl "https://virtualsmsnumbers.com/stubs/handler_api.php?api_key=$KEY&action=getNumber&service=tg&country=1"
# ACCESS_NUMBER:1043872915:351926114508
-kw">curl "https://virtualsmsnumbers.com/stubs/handler_api.php?api_key=$KEY&action=getStatus&id=1043872915"
# STATUS_WAIT_CODE
# ... a few seconds later ...
# STATUS_OK:48219The four differences that will bite you
| Behaviour | What legacy clients assume | What we do |
|---|---|---|
| Currency | Balance in RUB | Balance in EUR. ACCESS_BALANCE:47.30 is €47.30. |
| Activation ids | Numeric, safe to parseInt | Opaque strings. Keep them as strings; some are 25 characters. |
| Country codes | Provider-specific integers | The same integers are accepted, and ISO alpha-2 also works. |
| Failed activations | setStatus=8 to get money back | Nothing to call. Expiry refunds automatically; setStatus=8 still works and is a no-op after the fact. |
The id one causes real damage. A client that does parseInt on a cuid gets NaN, sends it back on the next getStatus call, receives an error it was not expecting and — in two cases we saw in 2023 — loops buying numbers. Treat the id as a string.
Step two, optional: move to the REST API
The compatibility layer is supported indefinitely and there is no deprecation clock on it. It is also a worse API than the one underneath, because the protocol it imitates was designed for a single provider in 2015. Three things you cannot express in it: idempotency, waiting, and structured errors.
| Legacy action | REST equivalent |
|---|---|
| action=getBalance | GET /api/v1/balance |
| action=getPrices | GET /api/v1/prices?service=&country= |
| action=getNumbersStatus | GET /api/v1/prices — the same rows, with stock and success rate |
| action=getNumber | POST /api/v1/activations |
| action=getStatus | GET /api/v1/activations/{id}?wait=240 |
| action=setStatus&status=6 | POST /api/v1/activations/{id}/complete |
| action=setStatus&status=8 | POST /api/v1/activations/{id}/cancel |
| action=setStatus&status=3 | POST /api/v1/activations/{id}/resend |
// Legacy: buy, then poll a text endpoint and parse tokens.
-kw">const line = -kw">await get(`?api_key=${KEY}&action=getNumber&service=tg&country=1`);
-kw">if (line.startsWith("NO_NUMBERS")) -kw">throw -kw">new Error("out of stock");
-kw">const [, id, phone] = line.split(":");
// REST: buy once, safely, then hold one connection until the code lands.
-kw">const buy = -kw">await fetch(`${API}/activations`, {
method: "POST",
headers: { ...H, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ service: "telegram", country: "PT", max_price_cents: -num">25 }),
}).then((r) => r.json());
-kw">const done = -kw">await fetch(`${API}/activations/${buy.id}?wait=240`, { headers: H }).then((r) => r.json());
-kw">const code = done.messages[-num">0]?.code ?? -kw">null;Testing the swap without betting on it
Keys are per-account and both protocols share the same balance, so the safe way to evaluate the compatibility layer is to run it in parallel rather than to cut over. Point a staging copy of your bot at our URL with a €30 balance, leave production where it is, and compare the two on the pairs you actually buy — same service, same country, same hour of the day. A week of that tells you more than any success-rate table, including ours.
Two things to check while it runs. First, that your parser survives a longer id, because that is the only field whose shape genuinely changed. Second, that your "out of stock" branch is reachable: NO_NUMBERS means the same thing here, and a client that treats every non-ACCESS_ response as a fatal error will stop on the first empty pair rather than moving to the next country.
Rate limits and scopes apply to both
The legacy protocol has no concept of either, so we map them onto it as best we can. A key over its per-minute limit gets ERROR_SQL, which is unhelpful but is the only token the vocabulary has for "try again shortly" — one more reason to move to REST, where you get a 429 and a Retry-After. A key missing numbers:write gets BAD_KEY on getNumber even though the key is perfectly valid for getBalance. If a working key suddenly reads as bad on one action only, check the scopes before you check anything else.
Why the idempotency key matters more than it looks
The legacy protocol has no way to say "this is a retry of the request I already sent". A timeout on getNumber leaves you genuinely unable to tell whether you bought a number, so the standard workaround is to retry and eat the occasional double purchase. On POST /api/v1/activations, an Idempotency-Key means the retry returns the original activation with Idempotent-Replay: true and no second charge. For anything running unattended, that alone is worth the migration.
Errors
The legacy layer collapses everything it does not have a token for into ERROR_SQL, which is a sentence we are not proud of emitting. The REST API returns a JSON body with a stable code — no_stock, insufficient_funds, unknown_service, unknown_country, spend_cap_reached, insufficient_scope — and an HTTP status that matches it. Log the code, branch on the code, and you will never have to grep a string again.
Both paths share a rate limit, both respect key scopes, and you can run them side by side against the same account while you migrate. Most people do the two-constant change on a Monday and the REST rewrite whenever it stops being urgent.
Artikel diterbitkan dalam bahasa Inggris.