Die SmsEmpire-API
Kaufen Sie SMS- und E-Mail-Verifizierungen aus Ihrem eigenen Code, bezahlt aus Ihrem Guthaben, mit Rabatt auf den Website-Preis.
- →Rabattierter Preis bei jedem Kauf über die API
- →Signierte Webhooks, sobald der Code eintrifft — keine Polling-Schleife
- →Idempotente Käufe: ein Timeout lässt sich gefahrlos wiederholen
Quickstart
Everything is under https://smsempire.com/api/v1. Authenticate with your key as a bearer token. Server-side only — this API sends no CORS headers, and a key used from a browser is a key that has leaked.
# 1. Create a key in your dashboard, then:
export SE_KEY="se_live_XXXXXXXXXXXX_..."
# 2. What will this cost?
curl -s https://smsempire.com/api/v1/prices?service=tg \
-H "Authorization: Bearer $SE_KEY"
# 3. Buy a number. The Idempotency-Key is required.
curl -s https://smsempire.com/api/v1/activations \
-H "Authorization: Bearer $SE_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"service":"tg","max_price":"0.35"}'
# 4. Collect the code (or let the webhook push it to you).
curl -s https://smsempire.com/api/v1/activations/90210 \
-H "Authorization: Bearer $SE_KEY"Conventions
- Money is a decimal string (
"0.28000"), never a JSON number. Parse it with a decimal type, not a float. - Timestamps are RFC3339 UTC.
- Errors are always
{"error": {"code": ..., "message": ..., "request_id": ...}}. Branch oncode;messagemay change. - Quote
request_idin any support ticket — it points straight at the request in our logs. - Rate limits come back in
X-RateLimit-*, and a 429 carriesRetry-After.
Idempotency (required on purchases)
Every purchase needs an Idempotency-Key header — a UUID is ideal. Reusing it returns the original response with Idempotent-Replay: true instead of buying a second number.
This exists for one situation: your POST times out and you do not know whether it went through. Without the key, retrying buys twice and not retrying loses the number you already paid for. With it, just retry.
Reusing a key with a different body is a 422, not a silent replay — that combination is a bug on your side and hiding it would give you the wrong result.
max_price
Optional but strongly recommended. If you omit the country we try several, in quality order, at different prices — max_price is checked against every candidate, so you can never be charged more than you agreed to. If nothing fits, you get 409 PRICE_ABOVE_MAX with the current price and nothing is charged.
Webhooks
Register one HTTPS endpoint in your dashboard. We POST an event as soon as a code arrives. Delivery is at least once, retried with backoff over roughly two and a half hours.
POST /your/webhook
X-SmsEmpire-Event: activation.code_received
X-SmsEmpire-Delivery: evt_01j8z... <- deduplicate on this
X-SmsEmpire-Timestamp: 1765432991
X-SmsEmpire-Signature: v1=3a7f...
{
"id": "evt_01j8z...",
"type": "activation.code_received",
"created_at": "2026-08-14T12:03:11Z",
"data": {
"activation_id": 90210,
"service": "tg",
"country": "2",
"number": "+79991234567",
"code": "483920",
"status": "received",
"price": "0.28000"
}
}Requirements for your endpoint: HTTPS on port 443, resolving to a public address. We do not follow redirects and we refuse private, loopback and cloud-metadata destinations.
Verifying a webhook
The signature is HMAC-SHA256(secret, "<timestamp>.<raw body>"), hex-encoded. Four things people get wrong, in order of how often:
- Signing the re-serialised JSON instead of the raw bytes. Re-serialising changes the bytes and the signature will never match.
- Ignoring the timestamp. Without the
±300scheck, a captured delivery can be replayed at you forever. - Comparing with
==instead of a constant-time compare. - Not deduplicating on
X-SmsEmpire-Delivery. We guarantee at least once, not exactly once.
import hmac, hashlib, time
from flask import request, abort
SECRET = "whsec_..." # from your dashboard
TOLERANCE = 300 # seconds
def verify(request):
raw = request.get_data() # RAW bytes, before json parsing
ts = request.headers.get("X-SmsEmpire-Timestamp", "")
sig_header = request.headers.get("X-SmsEmpire-Signature", "")
if abs(time.time() - int(ts)) > TOLERANCE: # blocks replays
abort(400)
expected = hmac.new(SECRET.encode(),
f"{ts}.".encode() + raw,
hashlib.sha256).hexdigest()
# More than one v1= during a secret rotation; any match is valid.
sigs = [p.split("=", 1)[1] for p in sig_header.split(",") if p.startswith("v1=")]
if not any(hmac.compare_digest(expected, s) for s in sigs):
abort(400)const crypto = require("crypto");
// express.raw({type: "application/json"}) — you need the RAW body, not the parsed one.
function verify(req, secret) {
const ts = req.get("X-SmsEmpire-Timestamp") || "";
const header = req.get("X-SmsEmpire-Signature") || "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(Buffer.concat([Buffer.from(ts + "."), req.body]))
.digest("hex");
return header
.split(",")
.filter((p) => p.startsWith("v1="))
.some((p) =>
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.slice(3))),
);
}Rotating the secret keeps the previous one valid for 24 hours, and both sign every delivery during that window — so you can deploy without downtime.
Errors
| code | What to do |
|---|---|
| INVALID_API_KEY | Missing, malformed or unknown key. Do not retry. |
| KEY_REVOKED / KEY_EXPIRED | Create a new key. Do not retry. |
| INSUFFICIENT_SCOPE | This key was created without write access. |
| API_ACCESS_DENIED | API access is off for this account, or you have not topped up yet. |
| API_DISABLED | The API is temporarily disabled platform-wide. Retry later. |
| IDEMPOTENCY_KEY_REQUIRED | Send an Idempotency-Key header on every purchase. |
| IDEMPOTENCY_KEY_REUSED | Same key, different body. Fix your client. |
| IDEMPOTENCY_IN_PROGRESS | The first attempt is still running. Retry in a second. |
| PRICE_ABOVE_MAX | The price moved above your max_price. Re-quote and decide. |
| INSUFFICIENT_BALANCE | Top up. Do not retry until you have. |
| COUNTRY_OUT_OF_STOCK | No numbers right now. Retry, or try another country. |
| PROVIDER_CAPACITY | Temporarily unable to source that country. Retry later. |
| CONCURRENCY_LIMIT | Too many pending activations. Finish or cancel some. |
| SPEND_LIMIT | Hourly spend ceiling reached. Retry after the window. |
| RATE_LIMITED | Slow down. Honour Retry-After. |
| VALIDATION_ERROR | Your request body is wrong. Do not retry unchanged. |
Activation lifecycle
An activation is pending until a code arrives (received), you mark it used (finished), or it is cancelled. Cancelling refunds exactly what you were charged.
A pending activation expires 20 minutes after purchase. Expiry cancels it upstream and refunds you in full — you are never left paying for a number that never received anything.
The provider enforces a ~120 second grace period before a number can be cancelled; cancelling sooner returns EARLY_CANCEL_DENIED with the number of seconds to wait.
Limits
Read requests, write requests and total per-user requests each have a per-minute ceiling, plus caps on pending activations, hourly spend and hourly activation count. Your current values are in GET /api/v1/me. If you need them raised, open a ticket — they are per-account settings, not hard-coded.
Full endpoint reference
Interactive OpenAPI 3.1 document, always generated from the running code.
Open the reference