ब्लॉग पर वापस
इंजीनियरिंग

Migrating from handler_api.php in an afternoon

Change two constants and your existing bot keeps working. Then, if you want to, move to the REST API properly. Both halves, with the mapping table.

Ilya Novak द्वारा·Engineering·14 फ़र॰ 2023 को प्रकाशित·6 मिनट पढ़ें·5 नव॰ 2024 को अपडेट

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

config.diff
- 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_a8Kd2rQ4xN7pLmT1yBc

That 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.

bash
-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:48219

The four differences that will bite you

BehaviourWhat legacy clients assumeWhat we do
CurrencyBalance in RUBBalance in EUR. ACCESS_BALANCE:47.30 is €47.30.
Activation idsNumeric, safe to parseIntOpaque strings. Keep them as strings; some are 25 characters.
Country codesProvider-specific integersThe same integers are accepted, and ISO alpha-2 also works.
Failed activationssetStatus=8 to get money backNothing 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 actionREST equivalent
action=getBalanceGET /api/v1/balance
action=getPricesGET /api/v1/prices?service=&country=
action=getNumbersStatusGET /api/v1/prices — the same rows, with stock and success rate
action=getNumberPOST /api/v1/activations
action=getStatusGET /api/v1/activations/{id}?wait=240
action=setStatus&status=6POST /api/v1/activations/{id}/complete
action=setStatus&status=8POST /api/v1/activations/{id}/cancel
action=setStatus&status=3POST /api/v1/activations/{id}/resend
before-and-after.ts
// 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.

लेख अंग्रेज़ी में प्रकाशित होते हैं।