For developers
API reference
Read the balance and the standing tariff for any service in any country straight from your own code. One key, carried in a header, and JSON coming back.
First request #
JSON over HTTPS throughout, authenticated by one key you issue yourself. Nothing to install, nothing to sign, no sandbox to be granted — a key and curl will do.
- 1 Create a key Account menu, then API key, then create one. It appears once and only its fingerprint is retained, so there is no looking it up later.
- 2 Present it as a bearer token Every call carries an Authorization header. There is no second route in: not a query parameter, not a cookie, not a session.
- 3 Read the answer Every reply is a JSON object carrying an ok field. Where ok is false, an error field names the cause as a stable string meant for your code.
Presenting the key #
One key to an account. Any route other than the header below is unsupported by design — a key in a query string ends up in access logs, in browser history and in referrer headers, so it is refused outright rather than accepted and quietly leaked.
Authorization: Bearer sk_your_key_here
This key spends money Today it reads a balance; the same key will place orders once ordering exists. Handle it as a password: never in source control, never in a screenshot, replaced rather than passed around. Issuing a replacement retires the previous key on the spot.
Shapes and conventions #
| Base address | https://smsactivate.io/api/v1 |
|---|---|
| Transport | Served over HTTPS only: a plain-HTTP call gets a redirect, and that redirect drops your Authorization header. |
| Format | JSON both ways. What comes back is always an object — a bare array is never a valid response. |
| Success | HTTP 200, and "ok" set to true. |
| Failure | A 4xx or 5xx status, "ok" set to false, and an "error" string whose spelling never changes. |
| Money | Amounts are US-dollar numbers — not strings, not cents: 0.84 is eighty-four cents. |
| Unknown fields | Any response may gain fields over time. Skip the ones you do not recognise instead of treating them as an error. |
When something fails #
Branch on the error string and nothing else. The sentence beside it exists for you rather than for your code, and may be reworded at any time; the string will not be.
| Status | error | Cause |
|---|---|---|
| 401 | missing_key |
No Authorization header arrived, or what arrived was not a bearer token. |
| 401 | bad_key |
The key is not one we know, or it has since been retired or replaced. |
| 400 | unknown_service |
No service answers to that code. Codes come out of the catalogue, never from the name shown on screen. |
| 404 | no_stock |
Nothing is idle for that service-and-country pairing at this moment. |
| 404 | unknown_country |
No country answers to that code for this product. The catalogues do not cover the same ground. |
| 400 | unknown_tech |
That network generation is not offered in this country. Only the United States is sold by metro. |
| 429 | rate_limited |
Too many calls. The Retry-After header names the wait, in seconds. |
How hard you may push #
600 calls a minute, counted against the account rather than the address. A key is meant to live on one server — one address — so counting per address would penalise ordinary use while leaving a stolen key free to work from anywhere else. Past the ceiling you get a 429 with Retry-After in seconds.
Routes #
Account and catalogue reads. Each route names every parameter it accepts, every field it returns, and every way it can refuse.
What the account holds #
GET
/api/v1/balance
The credit standing right now, and how many activations that covers at the lowest tariff currently in the catalogue.
What comes back
| Key | Kind | Meaning |
|---|---|---|
ok |
boolean | Present and true whenever the status is 200. |
balance |
number | The dollars on the balance, to the cent. |
currency |
string | Fixed at "USD"; it is sent so that nothing has to be assumed. |
codes_at_cheapest |
integer | The number of codes the balance would cover at the catalogue's lowest price. An estimate to give a sense of scale, not a quote; null when the catalogue holds nothing. |
cheapest_code |
number | That lowest price itself, in case you want to work the estimate out against a different figure. |
Call
curl -s "https://smsactivate.io/api/v1/balance" \
-H "Authorization: Bearer $SMSACTIVATE_KEY"
Answer
{
"ok": true,
"balance": 42.5,
"currency": "USD",
"codes_at_cheapest": 425,
"cheapest_code": 0.1
}
Ways it refuses missing_key bad_key rate_limited
One service in one country #
GET
/api/v1/pricing?service={service}&country={country}
Live tariff and live idle count for a single pairing. Both drift: the tariff follows the cheapest carrier holding that service there, and the count is whatever the carrier reports at that instant.
What it takes
| Name | Meaning | |
|---|---|---|
service |
needed | The service's code, telegram for instance — lower-case, as spelt in the catalogue. |
country |
may be omitted | The country's code, england for instance. Drop it and every country comes back in one call — see below. |
What comes back
| Key | Kind | Meaning |
|---|---|---|
ok |
boolean | Present and true whenever the status is 200. |
service |
string | The service code, returned unchanged. |
country |
string | The country code, returned unchanged. |
price |
number | The dollar price of one code on the cheapest network that carries it. |
stock |
integer | The count of numbers the networks report as free right now — a live reading that changes minute to minute. |
Call
curl -s "https://smsactivate.io/api/v1/pricing?service=telegram&country=england" \
-H "Authorization: Bearer $SMSACTIVATE_KEY"
Answer
{
"ok": true,
"service": "telegram",
"country": "england",
"price": 0.84,
"stock": 61213
}
Ways it refuses missing_key bad_key unknown_service no_stock rate_limited
One service, every country #
GET
/api/v1/pricing?service={service}
Leave the country out and every country carrying the service comes back. The ordering is the site's own — what is actually delivering first, cheapest within that — rather than raw tariff, which would hoist a two-cent line nobody ever receives anything on to the top.
What it takes
| Name | Meaning | |
|---|---|---|
service |
needed | The service's code, telegram for instance. |
What comes back
| Key | Kind | Meaning |
|---|---|---|
ok |
boolean | Present and true whenever the status is 200. |
service |
string | The service code you sent. |
name |
string | The name shown on the site, "Telegram" for instance. |
countries |
array | An object per country, ordered as explained above. |
countries[].country |
string | The country's code, ready to pass to the pair call. |
countries[].name |
string | Its English name. |
countries[].price |
number | The dollar price of one code. |
countries[].stock |
integer | Numbers free at this moment. |
Call
curl -s "https://smsactivate.io/api/v1/pricing?service=telegram" \
-H "Authorization: Bearer $SMSACTIVATE_KEY"
Answer
{
"ok": true,
"service": "telegram",
"name": "Telegram",
"countries": [
{ "country": "england", "name": "United Kingdom", "price": 0.84, "stock": 61213 },
{ "country": "poland", "name": "Poland", "price": 0.91, "stock": 22140 }
]
}
Ways it refuses missing_key bad_key unknown_service rate_limited
Mobile session tariffs #
GET
/api/v1/proxy-pricing?country={country}&tech={tech}
With no country named you get the four we keep modems in, each with a carrier count and an opening tariff. Name one and you get every carrier there with the cost of each stretch. Stretches are given in days: 1, 7, 30, 365.
What it takes
| Name | Meaning | |
|---|---|---|
country |
may be omitted | The country's code, us for instance. Leave it out and the countries are listed instead. |
tech |
may be omitted | The generation, 4g or 5g. Only the United States offers both; anywhere else the field is disregarded and the one available range is returned. |
What comes back
| Key | Kind | Meaning |
|---|---|---|
ok |
boolean | Present and true whenever the status is 200. |
country |
string | The country code you sent. |
tech |
string | The generation the price applies to, after the default has been chosen. |
techs |
array | Each generation on sale in that country. |
terms |
array | The rental lengths on offer, in days. |
carriers |
array | An object per carrier. |
carriers[].carrier |
string | The carrier's code, to pass along when ordering. |
carriers[].name |
string | The carrier's display name, "T-Mobile" for instance. |
carriers[].prices |
object | The dollar price of each term, indexed by its length in days. |
Call
curl -s "https://smsactivate.io/api/v1/proxy-pricing?country=us" \
-H "Authorization: Bearer $SMSACTIVATE_KEY"
Answer
{
"ok": true,
"country": "us",
"tech": "4g",
"techs": ["4g", "5g"],
"terms": [1, 7, 30, 365],
"carriers": [
{ "carrier": "us-t-mobile", "name": "T-Mobile", "prices": { "1": 8.67, "7": 52, "30": 130, "365": 1300 } },
{ "carrier": "us-verizon", "name": "Verizon", "prices": { "1": 8.67, "7": 52, "30": 130, "365": 1300 } }
]
}
Ways it refuses missing_key bad_key unknown_country unknown_tech rate_limited
Term tariffs #
GET
/api/v1/rental-pricing?country={country}&days={days}
With no country named you get every country a line can be held in, each with its opening tariff. Name one and you get every operator there and what the stretch you asked about costs on it.
What it takes
| Name | Meaning | |
|---|---|---|
country |
may be omitted | The country's code, fr for instance. Leave it out and the countries are listed instead. |
days |
may be omitted | The length in days — 7, 14 or 30. Any other value is treated as the shortest term. |
What comes back
| Key | Kind | Meaning |
|---|---|---|
ok |
boolean | Present and true whenever the status is 200. |
country |
string | The country code you sent. |
days |
integer | The term the price applies to. |
terms |
array | Each term on sale, in days. |
operators |
array | An object per operator, lowest price first. |
operators[].operator |
string | The operator's code, to pass along when ordering. |
operators[].name |
string | Its display name. |
operators[].type |
string | One of physical, virtual or premium. |
operators[].price |
number | The dollar price of the full term. |
Call
curl -s "https://smsactivate.io/api/v1/rental-pricing?country=fr&days=30" \
-H "Authorization: Bearer $SMSACTIVATE_KEY"
Answer
{
"ok": true,
"country": "fr",
"days": 30,
"terms": [7, 14, 30],
"operators": [
{ "operator": "fr-lycamobile", "name": "Lycamobile", "type": "virtual", "price": 14.32 },
{ "operator": "fr-orange", "name": "Orange", "type": "physical", "price": 20.76 }
]
}
Ways it refuses missing_key bad_key unknown_country rate_limited
Worked examples #
Programs that run as they stand rather than one-line fragments — the two parts that trip people up included.
Shell #
Locate the cheapest country for a service, then confirm the balance stretches to it.
#!/usr/bin/env bash
set -euo pipefail
: "${SMSACTIVATE_KEY:?export your key first}"
API="https://smsactivate.io/api/v1"
auth=(-H "Authorization: Bearer $SMSACTIVATE_KEY")
# The catalogue is already ordered: the first country is the one to take.
best=$(curl -sf "${API}/pricing?service=telegram" "${auth[@]}" \
| jq -r ".countries[0] | \"\(.country) \(.price)\"")
country=${best% *}
price=${best#* }
balance=$(curl -sf "${API}/balance" "${auth[@]}" | jq -r .balance)
# ⚠️ Compare as numbers, not as strings: "9.5" > "10" is true in a string sort.
if awk "BEGIN{exit !($balance >= $price)}"; then
echo "ok: $country at \$$price, balance \$$balance"
else
echo "top up first: need \$$price, have \$$balance" >&2
exit 1
fi
JavaScript #
The same again in Node, handling the error shape properly.
const API = "https://smsactivate.io/api/v1";
const key = process.env.SMSACTIVATE_KEY;
async function call(path) {
const r = await fetch(API + path, {
headers: { Authorization: `Bearer ${key}` },
});
const body = await r.json();
// A non-2xx always carries { ok:false, error }. Branch on `error`, never on
// the sentence — the string is stable, the wording is not.
if (!r.ok || !body.ok) {
if (body.error === "rate_limited") {
const wait = Number(r.headers.get("Retry-After") || 60);
await new Promise((s) => setTimeout(s, wait * 1000));
return call(path);
}
throw new Error(body.error ?? `http_${r.status}`);
}
return body;
}
const { countries } = await call("/pricing?service=telegram");
const { balance } = await call("/balance");
const best = countries[0];
console.log(`${best.name}: $${best.price} (${best.stock} in stock)`);
console.log(balance >= best.price ? "balance covers it" : "top up first");
Python #
And in Python, retrying on 429 the same way.
import os, time, requests
API = "https://smsactivate.io/api/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['SMSACTIVATE_KEY']}"
def call(path):
r = S.get(API + path, timeout=20)
body = r.json()
if not r.ok or not body.get("ok"):
if body.get("error") == "rate_limited":
time.sleep(int(r.headers.get("Retry-After", 60)))
return call(path)
raise RuntimeError(body.get("error", f"http_{r.status_code}"))
return body
countries = call("/pricing?service=telegram")["countries"]
balance = call("/balance")["balance"]
best = countries[0]
print(f"{best['name']}: ${best['price']} ({best['stock']} in stock)")
print("balance covers it" if balance >= best["price"] else "top up first")
What is not here yet #
Today the key reads the account and the catalogue. Placing orders is next, and it will answer on the same base address to the same key — so nothing written against these routes will need unpicking when it lands.
| Order a number | Not yet Not yet. For now, ordering goes through the site. |
|---|---|
| Polling for the code | Not yet Planned, alongside ordering. |
| Release a number | Not yet Planned, alongside ordering. |
| Rental over API | Not yet Planned. |
| Webhooks | Not yet Not yet — it arrives together with order events, once there are any to deliver. |
This page grows with them: what is here stays where it is, and anything new appears under Endpoints.
What changed #
- First public release: API keys, GET /balance, GET /pricing.