SMS-Activate Migration Hub 2026: Developer Checklist, API Mapping & Refund Comparison
If you are still carrying handler_api.php calls in your repo pointed at sms-activate.org, this is the hub you open at 9pm on a Friday and close with a working integration before midnight. Real endpoint mapping, real code diffs, refund policy comparison, and the gotchas that will bite you between the line you change and the alert that wakes you up on Monday morning.
Why this playbook exists
Most "SMS-Activate is dead, here is a list of alternatives" posts miss the part that actually burns developer hours: the code. Signing up for a new provider takes five minutes. Rewriting an integration that has been quietly running in production for years, with untested corner cases and a cost tracker attached to it, takes longer than you think.
After we shipped our compatibility layer in January 2026 we started getting the same three questions from every team that tried it:
- Which endpoints map cleanly and which ones need manual changes?
- How do I keep my cost tracking and refund flow working without a rewrite?
- What breaks silently and shows up a week later as a billing surprise?
This playbook answers those three questions in order and gives you copy-paste code you can audit before running.
🔒 Need a virtual number right now?
Instant SMS verification • 150+ countries • from $0.20
📱 Download VerifySMS FreeThe 48-hour recap: what actually happened
SMS-Activate went dark on December 29, 2025. There was no scheduled maintenance banner, no migration tool, and no public notice. Users trying to log in hit a single page saying the service had closed permanently. The API returned connection resets on every endpoint within a few hours.
Three things happened fast:
- Dashboard balances became unreachable. Reports on the
r/smssubreddit and the official Telegram channels described balances of $20 to several thousand dollars frozen with no recovery path. - Running integrations broke hard. Any service polling
handler_api.phpstarted receiving HTTP connection errors, which in most bug trackers mean "circuit breaker tripped, alert the team." - The migration window closed in days, not weeks. Within 72 hours every remaining provider had a queue. 5sim, SMSPVA, and SMS-MAN all acknowledged capacity strain. VerifySMS held up because we were the smallest in the group and had spare headroom, but it was genuinely close.
The fallout is still playing out. As of April 2026 there are active small-claims cases in Russia and at least two coordinated community lawsuits trying to recover frozen balances. None of that helps your code, which is why we are going to focus on the part you can actually fix.
Part 1: API deprecation map
SMS-Activate shipped a single public endpoint at https://sms-activate.org/stubs/handler_api.php. Every action was a query string parameter on that URL. The table below maps every major action to its VerifySMS equivalent. The compat layer at https://api.verifysms.app/compat/handler_api.php accepts the exact same query string shape.
| SMS-Activate action | Purpose | VerifySMS compat layer | Native VerifySMS API |
|---|---|---|---|
getBalance | Return USD balance as text | Works unchanged. Returns ACCESS_BALANCE:X.YY | GET /v1/balance returns JSON |
getNumbersStatus | Per-country availability | Works. Returns the legacy map format | GET /v1/countries/availability |
getNumber | Lease a number for a service | Works. Returns ACCESS_NUMBER:id:+phone | POST /v1/rentals |
setStatus | Confirm or cancel a lease | Works. Status codes 1/3/6/8 behave identically | POST /v1/rentals/{id}/status |
getStatus | Poll for SMS arrival | Works. Returns STATUS_WAIT_CODE, STATUS_OK:CODE, STATUS_WAIT_RETRY | GET /v1/rentals/{id} |
getPrices | Fetch price table | Works. Returns VerifySMS pricing in the legacy JSON shape | GET /v1/prices |
getCountries | Country code map | Works. Returns both legacy numeric IDs and ISO-3166 codes | GET /v1/countries |
getTopCountriesByService | Top countries per service | Returns real-time VerifySMS data instead of cached SMS-Activate ranks | GET /v1/services/{id}/top-countries |
A handful of less-used SMS-Activate actions do not map one to one. getRentServicesAndCountries and the long-lease rental API were SMS-Activate specific and have no compat layer. If your integration used those, you should move to the native VerifySMS long-lease endpoint at POST /v1/rentals/long, which is documented separately.
🔒 Need a virtual number right now?
Instant SMS verification • 150+ countries • from $0.20
📱 Download VerifySMS FreePart 2: Code migration walkthroughs
The following snippets are the exact shape I tested against our own staging environment in January. I have kept them deliberately boring so you can read them against your own code without context switching.
Python (requests)
The only required change is the base URL. If you have already wrapped the API in a small client module, the diff is a single line.
import os
import requests
# BEFORE
# BASE_URL = "https://sms-activate.org/stubs/handler_api.php"
# AFTER
BASE_URL = "https://api.verifysms.app/compat/handler_api.php"
API_KEY = os.environ["SMS_API_KEY"]
def get_number(service: str, country: int) -> tuple[str, str]:
resp = requests.get(BASE_URL, params={
"api_key": API_KEY,
"action": "getNumber",
"service": service,
"country": country,
}, timeout=30)
resp.raise_for_status()
# ACCESS_NUMBER:12345:+441234567890
status, rental_id, phone = resp.text.split(":", 2)
if status != "ACCESS_NUMBER":
raise RuntimeError(f"unexpected response: {resp.text}")
return rental_id, phone
def wait_for_code(rental_id: str, deadline_seconds: int = 180) -> str:
import time
start = time.monotonic()
while time.monotonic() - start < deadline_seconds:
resp = requests.get(BASE_URL, params={
"api_key": API_KEY,
"action": "getStatus",
"id": rental_id,
}, timeout=15).text
if resp.startswith("STATUS_OK:"):
return resp.split(":", 1)[1]
time.sleep(4)
# Mark as unused so we get the refund
requests.get(BASE_URL, params={
"api_key": API_KEY,
"action": "setStatus",
"status": 8,
"id": rental_id,
}, timeout=15)
raise TimeoutError(f"no code after {deadline_seconds}s")
Node.js (axios)
import axios from "axios";
// BEFORE
// const BASE_URL = "https://sms-activate.org/stubs/handler_api.php";
// AFTER
const BASE_URL = "https://api.verifysms.app/compat/handler_api.php";
const API_KEY = process.env.SMS_API_KEY;
export async function getNumber(service, country) {
const { data } = await axios.get(BASE_URL, {
params: { api_key: API_KEY, action: "getNumber", service, country },
timeout: 30_000,
});
const [status, rentalId, phone] = data.split(":");
if (status !== "ACCESS_NUMBER") {
throw new Error(`unexpected response: ${data}`);
}
return { rentalId, phone };
}
export async function waitForCode(rentalId, deadlineMs = 180_000) {
const start = Date.now();
while (Date.now() - start < deadlineMs) {
const { data } = await axios.get(BASE_URL, {
params: { api_key: API_KEY, action: "getStatus", id: rentalId },
timeout: 15_000,
});
if (data.startsWith("STATUS_OK:")) return data.split(":")[1];
await new Promise((r) => setTimeout(r, 4000));
}
await axios.get(BASE_URL, {
params: { api_key: API_KEY, action: "setStatus", status: 8, id: rentalId },
timeout: 15_000,
});
throw new Error(`no code after ${deadlineMs}ms`);
}
PHP (curl)
<?php
// BEFORE
// const BASE_URL = "https://sms-activate.org/stubs/handler_api.php";
// AFTER
const BASE_URL = "https://api.verifysms.app/compat/handler_api.php";
function sms_call(array $params): string {
$params["api_key"] = getenv("SMS_API_KEY");
$url = BASE_URL . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$body = curl_exec($ch);
curl_close($ch);
return $body;
}
function get_number(string $service, int $country): array {
$resp = sms_call(["action" => "getNumber", "service" => $service, "country" => $country]);
[$status, $id, $phone] = explode(":", $resp, 3);
if ($status !== "ACCESS_NUMBER") {
throw new RuntimeException("unexpected: $resp");
}
return ["id" => $id, "phone" => $phone];
}
Go (net/http)
package sms
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// BEFORE
// const baseURL = "https://sms-activate.org/stubs/handler_api.php"
// AFTER
const baseURL = "https://api.verifysms.app/compat/handler_api.php"
func call(params url.Values) (string, error) {
params.Set("api_key", os.Getenv("SMS_API_KEY"))
req, _ := http.NewRequest("GET", baseURL+"?"+params.Encode(), nil)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
func GetNumber(service string, country int) (id, phone string, err error) {
body, err := call(url.Values{
"action": {"getNumber"},
"service": {service},
"country": {fmt.Sprint(country)},
})
if err != nil {
return "", "", err
}
parts := strings.SplitN(body, ":", 3)
if len(parts) != 3 || parts[0] != "ACCESS_NUMBER" {
return "", "", errors.New("unexpected: " + body)
}
return parts[1], parts[2], nil
}
The Go example is deliberately written without any third-party client so you can drop it into a minimal service without adding dependencies. The same pattern holds in every other language: swap the URL, keep the rest, and let your existing error handling carry over.
Part 3: Gotchas that will burn you
These are the places where the compat layer is faithful to SMS-Activate's quirks but the quirks themselves will surprise you if you have not touched this code in a while.
Country IDs are not ISO codes
SMS-Activate numbered countries in their own order: Russia was 0, USA was 187, Indonesia was 6, and so on. If your integration has these magic numbers hardcoded, they still work on the compat layer. If you are writing new code, prefer the ISO-3166 alpha-2 form (RU, US, ID) which the compat layer also accepts. Do not mix both styles in the same call site, because future debugging will be painful.
🔒 Need a virtual number right now?
Instant SMS verification • 150+ countries • from $0.20
📱 Download VerifySMS FreeStatus code 3 versus 6
setStatus action code 3 meant "request another SMS" in the SMS-Activate world, and code 6 meant "accept the code as valid." These two codes are easy to swap in a hurry and they have opposite billing outcomes: 3 keeps you billed, 6 confirms the successful verification. The compat layer behaves the same way. Grep your code for setStatus and make sure the branch that takes code 6 only runs after you are certain the verification succeeded.
Timeouts and circuit breakers
SMS-Activate under load sometimes returned a 200 with an empty body instead of an HTTP error. Defensive clients wrap the call in a timeout and treat the empty body as a retry signal. VerifySMS never returns an empty body on the compat layer. If your client still treats empty as a retry, it will burn budget on a flaky network because the retry will hit a different rental ID. The safer pattern is to check for the known response prefixes (ACCESS_, STATUS_, BAD_) and treat anything else as a hard failure, not a transient one.
Rate limits move from per-key to per-IP
SMS-Activate rate limits were keyed to the API token. VerifySMS rate limits are keyed to the combination of API token and source IP address, because we see a lot of abuse from scraper scripts sharing one key across a botnet. For normal production traffic from a single server or a load-balanced pool, this is invisible. If you run distributed CI jobs that all share one staging key, you may see a 429 the first time the fleet all warms up together. The fix is to let the canary run from one node for a day before fanning out.
Refund timing feels instant because it actually is instant
This one is not a trap so much as a pleasant surprise. Where SMS-Activate refunds took a few hours to appear in your balance, VerifySMS refunds appear within 60 seconds. If your cost tracker reads the balance on a schedule, it will register the refund as a credit that the old system would have missed. Reconciliation dashboards sometimes flag this as an anomaly on the first day.
Part 4: Pricing comparison reality check
Before the shutdown, SMS-Activate was the floor of the market. Russian numbers cost $0.03 to $0.05 per verification, and large-volume buyers paid even less. That floor is gone. Here is where the remaining providers sit in April 2026 for the most common services, pulled from each provider's public pricing page on :
| Service | 5sim | TextVerified | SMSPVA | SMS-MAN | VerifySMS |
|---|---|---|---|---|---|
| WhatsApp / Russia | $0.014 | — | $0.05 | $0.035 | $0.10 |
| WhatsApp / USA | $0.27 | $0.25 | $0.28 | $0.22 | $0.18 |
| Telegram / Russia | $0.016 | — | $0.05 | $0.04 | $0.10 |
| Telegram / USA | $0.35 | $0.40 | $0.38 | $0.30 | $0.20 |
| Google / Indonesia | $0.07 | — | $0.08 | $0.06 | $0.10 |
The pattern is simple: 5sim and SMS-MAN win on rock-bottom Russian pricing, TextVerified is the US premium tier, and VerifySMS sits in the middle with a flat $0.10 baseline for everything except the most expensive US non-VoIP numbers. If your budget was tuned to SMS-Activate's floor prices, expect to pay two to five times more per verification regardless of which replacement you pick.
Two notes on this table. First, every provider (VerifySMS included) raises and lowers individual country prices in response to carrier costs, so confirm the current price in your own dashboard before committing a budget. Second, the effective price per successful verification depends on the refund ratio. A provider with a $0.08 sticker price and a 70% success rate costs you more per success than a $0.10 provider with automatic refunds and a 90% success rate.
Part 5: The 10-step migration checklist
This is the actual sequence we walked our own users through in January. It assumes a single developer with repo access, one production service, and a staging environment. Scale the canary percentages up if you are running multiple services or a monorepo.
- Inventory every call site. Run
git grep -n 'sms-activate\.org\|handler_api\.php\|getNumber\|setStatus'and list every file that hits the old API. If you find more than a dozen, pick a wrapper module and centralize the calls first before migrating. - Get a VerifySMS API key. Sign up, add a small amount of balance, and generate a scoped key for staging. Keep the production key out of the repo.
- Change the base URL. Replace the SMS-Activate host with
api.verifysms.app/compat/handler_api.php. Do not change the query string. Commit this alone so the diff is clean. - Run your existing tests. If the tests hit the real API, point them at staging and watch for shape mismatches. If they mock the API, run them against the live staging endpoint as well so you catch contract drift.
- Reconfirm country IDs. Skim your code for country constants. If you are using the legacy numeric IDs, they still work. If you have the chance, replace them with ISO-3166 codes because the next developer to touch this file will thank you.
- Wire up refund claims. Confirm that your timeout path calls
setStatuswithstatus=8. Without this, you will still get refunds (we auto-refund expired leases) but your cost tracker will lag the reality. - Update your cost tracker. Read the cost from the
X-VerifySMS-Costresponse header instead of parsing it out of the pricing table. This single change makes your finance dashboard accurate to the cent. - Monitoring. Add success rate, p95 latency, and refund ratio alerts against your existing baseline. Pick thresholds you can defend, not ones you think will be "fine."
- Canary 5 percent for 24 hours. Route a small slice of production traffic through the new endpoint. Watch the dashboard, not just the alerts.
- Cut the rest over. Once the canary window is clean, move the remaining 95 percent and leave the old client code commented out (not deleted) for one release cycle so you have a fast rollback.
Delete the old code in the next release after that. Do not leave dead call sites around for longer than a week because the next person to touch the module will paste them right back into a new integration by accident.
Frequently asked questions
Is the SMS-Activate compatibility layer a real API or just a stub?
It is a real endpoint at api.verifysms.app/compat/handler_api.php that accepts every major action from the SMS-Activate public docs: getBalance, getNumber, getStatus, setStatus, getPrices, and getCountries. Requests are proxied to our native API under the hood, so you get VerifySMS pricing, coverage, and refund behavior with no code changes on your side.
Will my old API key work?
No. SMS-Activate API keys stopped authenticating the day the service shut down. You need a new key from VerifySMS. Sign up, add a small amount of balance, and generate a key from the dashboard. The key format is identical in length so you can paste it into the same environment variable.
How do refunds work compared to SMS-Activate?
SMS-Activate required you to call setStatus with status code 8 within 20 minutes to mark a number as unused, and refunds were processed manually within a few hours. VerifySMS accepts the same setStatus call and refunds the full amount to your balance within 60 seconds. If you forget to call setStatus entirely, our system still auto-refunds any number that never received an SMS after the lease window expires.
Which countries are supported?
VerifySMS covers 200 plus countries. Every country that SMS-Activate offered is available on VerifySMS, including Russia, Indonesia, Vietnam, Nigeria, and the US. You can keep your existing country ID mapping or migrate to ISO-3166 alpha-2 codes whenever you want.
Is the pricing the same?
No. SMS-Activate bottom-of-market prices of $0.03 to $0.05 per verification for Russian numbers are gone from the open market. Current market pricing ranges from $0.10 for common services up to $0.25 for US non-VoIP numbers on stricter platforms. VerifySMS charges $0.10 as the baseline and publishes per-country pricing on the dashboard.
Do I need to change my polling logic?
No. The getStatus call returns STATUS_WAIT_CODE and STATUS_OK in the same format SMS-Activate used. Polling intervals of 3 to 5 seconds still work. The only new behavior is that VerifySMS also exposes a webhook URL in the dashboard, so you can stop polling entirely if you prefer an event-driven flow.
What happens if the compat layer is ever deprecated?
The compatibility layer is considered a permanent public interface. If we ever change its behavior, we will publish a minimum six-month deprecation window with a full migration note. The native VerifySMS JSON API is also documented, so you can migrate off the compat layer at your own pace whenever it makes sense.
How do I test without spending money?
The VerifySMS dashboard exposes a sandbox mode that returns simulated phone numbers and canned SMS codes without debiting your balance. Flip the sandbox flag in the dashboard or send the X-Sandbox-Mode header with any request to exercise your code paths before going live.
Can I migrate from other services too?
Yes. This playbook is written around the SMS-Activate API because that is where most stranded code lives, but the same checklist applies to migrations from 5sim, SMS-MAN, or any other handler_api-compatible service. The compat layer recognizes handler_api.php parameters regardless of which service you were previously calling.
How long does a real migration take?
For a single service integration with a few dozen call sites, plan on two to four hours of focused work, plus a 24-hour canary window before you cut over full traffic. Larger multi-service migrations with custom error handling, analytics, and retries can run longer but still usually finish inside a single working day.
Do I lose my historical data?
SMS-Activate verification history went offline when the service shut down and is not recoverable. VerifySMS maintains a full audit log of every verification attempt on your account for 12 months, accessible from the dashboard and via the /compat/handler_api.php?action=getHistory extension.
Does this affect my GDPR or compliance posture?
VerifySMS is registered in the United Kingdom and follows UK GDPR. We publish our data retention policy, sub-processors, and DPA on the privacy page. If your previous setup required a DPA with SMS-Activate, contact us and we will countersign the same agreement within one business day.
Next steps
If you have read this far, you already have the pieces you need. Start with the inventory step, get the base URL swap in front of a teammate for review, and run the canary overnight. The playbook is small on purpose; the hard part is the discipline to stop after the canary window instead of cutting everything over in one commit.
Related reading on the rest of the site:
- State of SMS Verification 2026 — full independent benchmark of 8 services with pricing, traffic, and refund policy data.
- SMS-Activate Shut Down: 7 Best Alternatives — non-developer guide to the replacement options.
- VerifySMS vs SMS-Activate — side-by-side comparison for teams still evaluating.
- SMS Verification API Integration Guide — native VerifySMS API walkthrough for when you are ready to move off the compat layer.
Ready to cut the migration to a single evening?
Create a VerifySMS API key →Sandbox mode included · Auto-refund guarantee · 200+ countries · SMS-Activate compat layer on /compat/handler_api.php
Ready to protect your privacy?
Get VerifySMS — Free on App Store
150+ countries • Instant activation • Auto-refund if no SMS • From $0.20
Download Free App★★★★★ 4.8 • iOS 16+ • Free