SmsEmpire

L'API SmsEmpire

Achetez des vérifications SMS et e-mail depuis votre propre code, débitées de votre solde, avec une remise sur le prix du site.

  • Tarif remisé sur chaque achat effectué via l'API
  • Webhooks signés dès l'arrivée du code — pas de boucle d'interrogation
  • Achats idempotents : réessayer après un timeout est sans risque

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 on code; message may change.
  • Quote request_id in any support ticket — it points straight at the request in our logs.
  • Rate limits come back in X-RateLimit-*, and a 429 carries Retry-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:

  1. Signing the re-serialised JSON instead of the raw bytes. Re-serialising changes the bytes and the signature will never match.
  2. Ignoring the timestamp. Without the ±300s check, a captured delivery can be replayed at you forever.
  3. Comparing with == instead of a constant-time compare.
  4. 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

codeWhat to do
INVALID_API_KEYMissing, malformed or unknown key. Do not retry.
KEY_REVOKED / KEY_EXPIREDCreate a new key. Do not retry.
INSUFFICIENT_SCOPEThis key was created without write access.
API_ACCESS_DENIEDAPI access is off for this account, or you have not topped up yet.
API_DISABLEDThe API is temporarily disabled platform-wide. Retry later.
IDEMPOTENCY_KEY_REQUIREDSend an Idempotency-Key header on every purchase.
IDEMPOTENCY_KEY_REUSEDSame key, different body. Fix your client.
IDEMPOTENCY_IN_PROGRESSThe first attempt is still running. Retry in a second.
PRICE_ABOVE_MAXThe price moved above your max_price. Re-quote and decide.
INSUFFICIENT_BALANCETop up. Do not retry until you have.
COUNTRY_OUT_OF_STOCKNo numbers right now. Retry, or try another country.
PROVIDER_CAPACITYTemporarily unable to source that country. Retry later.
CONCURRENCY_LIMITToo many pending activations. Finish or cancel some.
SPEND_LIMITHourly spend ceiling reached. Retry after the window.
RATE_LIMITEDSlow down. Honour Retry-After.
VALIDATION_ERRORYour 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