How I Moved a Production Script in Under an Hour.
Our account registration pipeline went dark at 2 AM on January 2nd. Here’s the full migration story — with working code.
I didn’t find out SMS-Activate had shut down from an announcement or an email.
I found out because our monitoring script threw a cascade of errors at 2:04 AM on January 2nd, 2026, and my phone started buzzing. The pipeline that handled bulk account verification for a QA testing project — about 150–200 activations per day — had been hitting dead endpoints for hours. Connection timeouts. Then 404s. Then nothing.
I pulled up the SMS-Activate dashboard. Static page. “We have completely ceased our operation since December 22.”
Right.
What followed was about six hours of unplanned infrastructure work on a holiday week, testing replacement APIs, rewriting integration code, and piecing together what had actually happened. By morning, the pipeline was running again — on HeroSMS — and I’d learned more about the virtual number API ecosystem than I’d ever wanted to know.
This article is a practical record of that migration: what the SMS-Activate API looked like, what broke, what the direct replacement options are, and how to migrate with minimal code changes. There are working code examples throughout.
What the SMS-Activate API Actually Looked Like
Before getting into migration, it’s worth documenting the original API for context — both for people who are mid-migration and for anyone building new integrations who wants to understand the baseline.
The SMS-Activate API was a simple HTTP GET interface. All requests went to a single handler endpoint with an action parameter:
| https://api.sms-activate.ae/stubs/handler_api.php?api_key=YOUR_KEY&action=ACTION_NAME |
The core workflow for a standard activation looked like this:
Step 1 – Check balance
| GET /handler_api.php?api_key=KEY&action=getBalance Response: ACCESS_BALANCE:12.50 |
Step 2 – Request a number
| GET /handler_api.php?api_key=KEY&action=getNumber&service=tg&country=0 Response: ACCESS_NUMBER:1234567890:79161234567 |
The response format was ACCESS_NUMBER:{activation_id}:{phone_number}.
Step 3 – Mark ready (SMS sent to the number)
| GET /handler_api.php?api_key=KEY&action=setStatus&status=1&id=1234567890 Response: ACCESS_READY |
Step 4 – Poll for the code
| GET /handler_api.php?api_key=KEY&action=getStatus&id=1234567890 Response: STATUS_OK:432156 (code arrived) STATUS_WAIT_CODE (still waiting) STATUS_CANCEL (timed out/failed) |
Step 5 – Finalize
| GET /handler_api.php?api_key=KEY&action=getStatus&id=1234567890 Response: STATUS_OK:432156 (code arrived) STATUS_WAIT_CODE (still waiting) STATUS_CANCEL (timed out/failed) |
Key status codes for setStatus:
- 1 – SMS has been sent to the number (mark ready).
- 3 – Request another SMS (free retry).
- 6 – Finish activation successfully.
- 8 – Cancel activation (number already used or unsuitable).
A typical Python implementation looked like this:
| python import requests import time API_KEY = “your_sms_activate_key” BASE_URL = “https://api.sms-activate.ae/stubs/handler_api.php” def get_balance(): r = requests.get(BASE_URL, params={“api_key”: API_KEY, “action”: “getBalance”}) return float(r.text.split(“:”)[1]) def get_number(service, country=0): r = requests.get(BASE_URL, params={ “api_key”: API_KEY, “action”: “getNumber”, “service”: service, “country”: country }) parts = r.text.split(“:”) return parts[1], parts[2] # activation_id, phone_number def set_status(activation_id, status): requests.get(BASE_URL, params={ “api_key”: API_KEY, “action”: “setStatus”, “status”: status, “id”: activation_id }) def poll_code(activation_id, timeout=120): start = time.time() while time.time() – start < timeout: r = requests.get(BASE_URL, params={ “api_key”: API_KEY, “action”: “getStatus”, “id”: activation_id }) if r.text.startswith(“STATUS_OK”): return r.text.split(“:”)[1] elif r.text == “STATUS_CANCEL”: return None time.sleep(5) return None |
This is the pattern that tens of thousands of developers had in production when December 22nd happened. Suddenly, BASE_URL was returning nothing.
What the Migration to HeroSMS Actually Requires
Here’s the important part, and the reason HeroSMS ended up being the obvious first candidate for migration: the API is structurally compatible with SMS-Activate.
The official HeroSMS documentation confirms it: “Our API inherited SMS-Activate’s authentication methods and maintains a similar endpoint structure, allowing developers who previously integrated SMS-Activate API to transition quickly with minimal code changes.”
In practice, “minimal code changes” means two things:
- Update the base URL.
- Generate a new API key from your HeroSMS account dashboard.
That’s it for the basic flow. The same action parameters, the same response format, the same status codes — all carry over.
Here’s the migrated version of the same Python script:
| python import requests import time API_KEY = “your_herosms_key” # New key from hero-sms.com dashboard BASE_URL = “https://api.hero-sms.com/stubs/handler_api.php” # Updated URL def get_balance(): r = requests.get(BASE_URL, params={“api_key”: API_KEY, “action”: “getBalance”}) return float(r.text.split(“:”)[1]) def get_number(service, country=0): r = requests.get(BASE_URL, params={ “api_key”: API_KEY, “action”: “getNumber”, “service”: service, “country”: country }) parts = r.text.split(“:”) return parts[1], parts[2] # Same response structure def set_status(activation_id, status): requests.get(BASE_URL, params={ “api_key”: API_KEY, “action”: “setStatus”, “status”: status, “id”: activation_id }) def poll_code(activation_id, timeout=120): start = time.time() while time.time() – start < timeout: r = requests.get(BASE_URL, params={ “api_key”: API_KEY, “action”: “getStatus”, “id”: activation_id }) if r.text.startswith(“STATUS_OK”): return r.text.split(“:”)[1] elif r.text == “STATUS_CANCEL”: return None time.sleep(5) return None |
Two lines changed. Everything else is identical.
For our pipeline, migration time was about 25 minutes — mostly account setup, funding the balance, and running validation tests across ten activations on different services.
Node.js Migration
If you’re working in Node.js or TypeScript, there’s an npm package that makes the HeroSMS API even more ergonomic. The hero-sms package (v2.0.0) is explicitly built to be compatible with the SMS-Activate API protocol:
Before (using the old sms-activate npm wrapper):
| javascript import { SMSActivate } from ‘sms-activate-org’; const api = new SMSActivate(‘your_old_key’); const balance = await api.getBalance(); const number = await api.getNumber({ service: ‘tg’, country: 0 }); await number.ready(); const code = await number.getCode(180); await number.success(); |
After (using hero-sms):
| javascript import { HeroSMSClient } from ‘hero-sms’; const client = new HeroSMSClient({ apiKey: ‘your_herosms_key’ }); const balance = await client.getBalance(); const { activationId, phoneNumber } = await client.getNumber({ service: ‘tg’, country: 2, }); await client.markReady(activationId); const status = await client.getStatus(activationId); if (status.status === ‘STATUS_OK’) { console.log(`Code: ${status.code}`); await client.complete(activationId); } |
The method names differ slightly (markReady vs ready, complete vs success), but the logic is identical. If you had abstraction layers in your codebase — and most production implementations do — you’re updating an adapter, not rewriting business logic.
Webhooks: The Better Alternative to Polling
If your integration was using the polling pattern above, the SMS-Activate migration is also a good moment to consider switching to webhooks. HeroSMS supports real-time event notification via webhook, which eliminates the polling loop entirely.
How it works: after purchasing a number, you configure a HTTPS endpoint in your account settings. When an SMS arrives on the purchased number, HeroSMS sends a POST request with the SMS content to your endpoint.
Webhook specs from the HeroSMS documentation:
- Method: POST.
- Content-Type: application/json.
- Response timeout: 3 seconds.
- Up to 3 webhook URLs can be configured simultaneously.
- If no response with code 200 is received: minimum 7 retries, with 20–30 second delays between attempts.
- Total retry window: at least 3 minutes.
This is meaningfully better than a 5-second polling loop for high-volume operations. At 200 activations per day, polling adds thousands of unnecessary HTTP requests. The webhook approach also reduces latency — codes are delivered the moment they arrive, not at the next poll interval.
A minimal Express.js endpoint that handles incoming webhooks:
| javascript const express = require(‘express’); const app = express(); app.use(express.json()); app.post(‘/sms-webhook’, (req, res) => { const { activationId, phoneNumber, smsText, service } = req.body; // Extract verification code from SMS body const codeMatch = smsText.match(/\b\d{4,8}\b/); const code = codeMatch ? codeMatch[0] : null; if (code) { // Store code, trigger next step in your pipeline storeCode(activationId, code); } // Always respond 200 — HeroSMS retries if it doesn’t receive it res.status(200).send(‘OK’); }); |
The recommendation to always return 200 even if the SMS was already processed is worth following — the retry mechanism doesn’t distinguish between “already handled” and “endpoint down.”
When HeroSMS Isn’t the Right Migration Target
HeroSMS handles the 95% case well. But there are specific scenarios where a different migration path makes more sense.
Strict US platforms that block virtual numbers (Tinder, Cash App, certain fintech apps)
If your SMS-Activate scripts were targeting US platforms that have gotten aggressive about blocking VoIP and virtual numbers, HeroSMS numbers will hit the same detection walls. The solution here is TextVerified, which provides numbers from real US carriers. Their API is REST-based, different from the SMS-Activate pattern, and their pricing starts at $0.25 per activation — much higher than HeroSMS. But for platforms that reject every virtual number, it’s the only option that consistently works.
Persistent numbers for ongoing 2FA
If your integration needed to hold a number active for days or weeks — receiving multiple verification codes from the same service over time — the SMS-Activate one-time model was never ideal for that. OnlineSIM offers number rentals from 1 day to indefinite duration, which is purpose-built for this case.
Maximum platform coverage for niche or regional services
SMS-MAN supports 1,500+ platforms across 270+ countries — the widest catalog available. For pipelines that need to verify against obscure regional services, SMS-MAN works well as a fallback when HeroSMS doesn’t have the specific platform listed.
Migration Checklist
For anyone doing this migration now, here’s the condensed version of what needs to happen:
Immediate steps:
- Create a new account on hero-sms.com — registration takes under a minute.
- Fund the balance — HeroSMS accepts cryptocurrencies, bank cards, bank transfer, E-Wallet, and other methods.
- Generate your API key from the account dashboard.
- Update BASE_URL in your codebase to the HeroSMS endpoint.
- Replace the API key.
- Run 5–10 test activations across your primary services before going to production.
Validation tests to run before full cutover:
- getBalance returns a valid number.
- getNumber for each service you use returns a valid phone number format.
- getStatus polling returns STATUS_OK for at least 3 out of 5 test activations.
- setStatus=6 (complete) and setStatus=8 (cancel) both return expected responses.
- Webhook endpoint receives POST data if you’re using that integration pattern.
Service code mapping note:
SMS-Activate used short codes like tg (Telegram), go (Google), ig (Instagram), wa (WhatsApp), fb (Facebook), am (Amazon). HeroSMS maintains the same service code conventions, so your existing service parameters should carry over without changes. Verify codes for any niche or regional services before going live.
What This Migration Taught Us About SMS API Architecture
The SMS-Activate shutdown exposed a design flaw in how most developers — including us — had built these integrations: tight coupling to a single provider’s endpoint.
The two-line migration to HeroSMS worked because the API structures were compatible. That was fortunate. If we’d been running against a service with a completely different API design, the migration would have been days of work instead of hours.
The robust pattern going forward is a provider abstraction layer:
| python class SMSProvider: def get_number(self, service, country): raise NotImplementedError def get_code(self, activation_id): raise NotImplementedError def cancel(self, activation_id): raise NotImplementedError def complete(self, activation_id): raise NotImplementedError class HeroSMSProvider(SMSProvider): # HeroSMS-specific implementation class TextVerifiedProvider(SMSProvider): # TextVerified-specific implementation class SMSManProvider(SMSProvider): # SMS-Man-specific implementation |
With this pattern, switching providers — or routing different services to different providers — becomes a configuration change rather than a code change. Running HeroSMS as the primary provider and TextVerified as a fallback for strict US platforms is a routing rule, not a rewrite.
The SMS-Activate shutdown was a significant disruption. Running your verification pipeline through a single provider, without a tested failover path, is a single point of failure. The lesson cost us a 6-hour outage on a holiday. It doesn’t need to cost you anything.
Code examples were tested against the HeroSMS API in January–March 2026. API specifications and endpoint URLs should be verified directly in the HeroSMS documentation before implementation, as they may be updated.