For developers
API documentation
Read your balance and the live price of any service in any country from your own code. One bearer key, JSON over HTTPS, no SDK to install.
Getting started #
Everything is JSON over HTTPS, authenticated with a single key you create yourself. No SDK to install, no contract to sign, no sandbox to request — a key and curl are enough.
- 1 Create a key Open the account menu, then API key, then Create a key. It is shown once: we store only its fingerprint, so it cannot be looked up again afterwards.
- 2 Send it as a bearer token Every request carries an Authorization header. There is no other way in — no key in the query string, no cookie, no session.
- 3 Read the answer Every response is a JSON object with an ok field. When ok is false, an error field says why in a stable machine-readable string.
Authentication #
One key per account. Sending it any other way than the header below is not supported — a key in a query string ends up in server logs, browser history and referrer headers, so it is refused rather than quietly accepted.
Authorization: Bearer sk_your_key_here
A key can spend money It reads the balance today, and it is the same key that will order numbers when ordering exists. Treat it like a password: out of source control, out of screenshots, replaced rather than shared. Replacing a key closes the old one immediately.
Conventions #
| Base address | https://simsms.com/api/v1 |
|---|---|
| Transport | HTTPS only. Plain HTTP is redirected, and the redirect does not carry your header. |
| Format | JSON in, JSON out. Every response is an object, never a bare array. |
| Success | HTTP 200 with "ok": true. |
| Failure | A 4xx or 5xx code with "ok": false and a stable "error" string. |
| Money | Numbers in US dollars, never strings, never cents. 0.84 means 84 cents. |
| Unknown fields | New fields may be added to any response. Ignore what you do not know rather than failing on it. |
Errors #
The error string is the part to branch on. The sentence next to it is for you, not for your code — it may be reworded; the string will not.
| HTTP | error | When |
|---|---|---|
| 401 | missing_key |
No Authorization header, or it is not a bearer token. |
| 401 | bad_key |
The key is unknown, or it was closed or replaced. |
| 400 | unknown_service |
No service by that code. Codes come from the catalogue, not from the display name. |
| 404 | no_stock |
That service and country pair has no number in stock right now. It is a live figure — it can come back. |
| 429 | rate_limited |
Too many requests. A Retry-After header says how many seconds to wait. |
Rate limits #
600 requests per minute, counted per account rather than per address. A key is meant to run from a server — one address — so a per-address limit would punish normal use and let a stolen key run freely somewhere else. Over the limit you get 429 and a Retry-After header in seconds.
Endpoints #
Account and catalogue reads. Each route lists every parameter it takes, every field it returns, and every error it can answer with — no undocumented behaviour.
Read your balance #
GET
/api/v1/balance
What the account holds right now, and how many codes that buys at the cheapest price currently in the catalogue.
Response fields
| Field | Type | Description |
|---|---|---|
ok |
boolean | Always true on a 200. |
balance |
number | Dollars available, rounded to the cent. |
currency |
string | Always "USD". Present so you never have to assume it. |
codes_at_cheapest |
integer | How many codes the balance buys at the cheapest price in the catalogue. A rough gauge, not a quote — null if the catalogue is empty. |
cheapest_code |
number | That cheapest price, so you can compute the gauge yourself against another figure. |
Request
curl -s "https://simsms.com/api/v1/balance" \
-H "Authorization: Bearer $SIMSMS_KEY"
Response
{
"ok": true,
"balance": 42.5,
"currency": "USD",
"codes_at_cheapest": 425,
"cheapest_code": 0.1
}
Can answer with missing_key bad_key rate_limited
Price one service in one country #
GET
/api/v1/pricing?service={service}&country={country}
The live price and the live stock for a single pair. Both figures move — the price follows the cheapest network carrying that service in that country, and the stock is what the network reports right now.
Parameters
| Parameter | Description | |
|---|---|---|
service |
required | Service code, for example telegram. Lower case, from the catalogue. |
country |
optional | Country code, for example england. Leave it out to get every country at once — see below. |
Response fields
| Field | Type | Description |
|---|---|---|
ok |
boolean | Always true on a 200. |
service |
string | The service code you asked for, echoed back. |
country |
string | The country code you asked for, echoed back. |
price |
number | Dollars for one code, at the cheapest network that has it. |
stock |
integer | Numbers reported available right now. It is a live figure and it moves. |
Request
curl -s "https://simsms.com/api/v1/pricing?service=telegram&country=england" \
-H "Authorization: Bearer $SIMSMS_KEY"
Response
{
"ok": true,
"service": "telegram",
"country": "england",
"price": 0.84,
"stock": 61213
}
Can answer with missing_key bad_key unknown_service no_stock rate_limited
Price one service everywhere #
GET
/api/v1/pricing?service={service}
Drop the country and you get every country that carries the service. The order is the site's own: what actually delivers first, cheapest within that — not raw price, which would put a two-cent number nobody receives a code from at the top.
Parameters
| Parameter | Description | |
|---|---|---|
service |
required | Service code, for example telegram. |
Response fields
| Field | Type | Description |
|---|---|---|
ok |
boolean | Always true on a 200. |
service |
string | The service code you asked for. |
name |
string | Its display name, for example "Telegram". |
countries |
array | One object per country, in the order described above. |
countries[].country |
string | Country code, to feed back into the pair call. |
countries[].name |
string | Its English name. |
countries[].price |
number | Dollars for one code. |
countries[].stock |
integer | Numbers available right now. |
Request
curl -s "https://simsms.com/api/v1/pricing?service=telegram" \
-H "Authorization: Bearer $SIMSMS_KEY"
Response
{
"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 }
]
}
Can answer with missing_key bad_key unknown_service rate_limited
Full examples #
Complete, runnable programs rather than one-line fragments — including the two things fragments always leave out: handling the error shape, and backing off on 429.
Shell #
Find the cheapest country for a service, then check the balance covers it.
#!/usr/bin/env bash
set -euo pipefail
: "${SIMSMS_KEY:?export your key first}"
API="https://simsms.com/api/v1"
auth=(-H "Authorization: Bearer $SIMSMS_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 thing in Node, with the error shape handled properly.
const API = "https://simsms.com/api/v1";
const key = process.env.SIMSMS_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, with the same retry on 429.
import os, time, requests
API = "https://simsms.com/api/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['SIMSMS_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")
Roadmap #
The API covers account and catalogue reads today. The ordering surface is next, and it will sit under the same base address and take the same key — nothing you build now will need rewriting.
| Order a number | Planned Planned. Ordering runs through the site today. |
|---|---|
| Poll for the code | Planned Planned, alongside ordering. |
| Release a number | Planned Planned, alongside ordering. |
| Rental over API | Planned Planned. |
| Webhooks | Planned Planned, once there are order events to deliver. |
This page grows with them: the sections below stay where they are, and new ones are added under Endpoints.
Changelog #
- First public version: keys, GET /balance, GET /pricing.