La API de SmsEmpire
Compra verificaciones SMS y email desde tu propio código, con cargo a tu saldo y con descuento sobre el precio de la web.
- →Precio con descuento en cada compra hecha por API
- →Webhooks firmados en cuanto llega el código — sin bucle de sondeo
- →Compras idempotentes: reintentar tras un timeout es seguro
Inicio rápido
Todo cuelga de https://smsempire.com/api/v1. Autentícate enviando tu clave como bearer token. Solo desde servidor: esta API no envía cabeceras CORS, y una clave usada desde un navegador es una clave filtrada.
# 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"Convenciones
- El dinero es una cadena decimal ("0.28000"), nunca un número JSON. Parséalo con un tipo decimal, no con un float.
- Las fechas son RFC3339 en UTC.
- Los errores siempre son {"error": {"code", "message", "request_id"}}. Decide según code; message puede cambiar.
- Cita el request_id en cualquier ticket de soporte: apunta directamente a esa petición en nuestros registros.
- Los límites vienen en las cabeceras X-RateLimit-*, y un 429 incluye Retry-After.
Idempotencia (obligatoria al comprar)
Toda compra necesita una cabecera Idempotency-Key — lo ideal es un UUID. Reutilizarla devuelve la respuesta original con Idempotent-Replay: true en vez de comprar un segundo número.
Existe por una sola situación: tu POST da timeout y no sabes si llegó a ejecutarse. Sin la clave, reintentar compra dos veces y no reintentar pierde el número que ya pagaste. Con ella, simplemente reintenta.
Reutilizar una clave con un cuerpo distinto devuelve 422, no una repetición silenciosa: esa combinación es un fallo de tu cliente, y taparlo te daría un resultado equivocado.
max_price
Opcional, pero muy recomendable. Si no indicas país probamos varios, por orden de calidad y a precios distintos; max_price se comprueba contra cada candidato, así que nunca se te puede cobrar más de lo que aceptaste. Si ninguno encaja recibes un 409 PRICE_ABOVE_MAX con el precio actual, y no se cobra nada.
Webhooks
Registra un endpoint HTTPS en tu panel. Enviamos un POST con el evento en cuanto llega el código. La entrega es al menos una vez, con reintentos escalonados durante unas dos horas y media.
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"
}
}Requisitos de tu endpoint: HTTPS en el puerto 443 y que resuelva a una dirección pública. No seguimos redirecciones y rechazamos destinos privados, de loopback o de metadatos de nube.
Verificar un webhook
La firma es HMAC-SHA256(secreto, "<timestamp>.<cuerpo crudo>") en hexadecimal. Cuatro cosas que se hacen mal, por orden de frecuencia:
- Firmar el JSON reserializado en vez de los bytes crudos. Reserializar cambia los bytes y la firma nunca cuadrará.
- Ignorar el timestamp. Sin la comprobación de ±300 s, una entrega capturada se te puede reenviar para siempre.
- Comparar con == en lugar de con una comparación en tiempo constante.
- No deduplicar por X-SmsEmpire-Delivery. Garantizamos al menos una vez, no exactamente una.
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))),
);
}Al rotar el secreto, el anterior sigue siendo válido 24 horas y ambos firman cada entrega durante esa ventana, así que puedes desplegar sin cortes.
Errores
| code | Qué hacer |
|---|---|
| INVALID_API_KEY | Clave ausente, mal formada o desconocida. No reintentes. |
| KEY_REVOKED / KEY_EXPIRED | Crea una clave nueva. No reintentes. |
| INSUFFICIENT_SCOPE | Esta clave se creó sin permiso de escritura. |
| API_ACCESS_DENIED | La API está desactivada para esta cuenta, o aún no has recargado. |
| API_DISABLED | La API está desactivada temporalmente para todos. Reintenta más tarde. |
| IDEMPOTENCY_KEY_REQUIRED | Envía la cabecera Idempotency-Key en cada compra. |
| IDEMPOTENCY_KEY_REUSED | Misma clave, cuerpo distinto. Corrige tu cliente. |
| IDEMPOTENCY_IN_PROGRESS | El primer intento sigue en curso. Reintenta en un segundo. |
| PRICE_ABOVE_MAX | El precio subió por encima de tu max_price. Vuelve a consultar y decide. |
| INSUFFICIENT_BALANCE | Recarga saldo. No reintentes hasta hacerlo. |
| COUNTRY_OUT_OF_STOCK | Ahora mismo no hay números. Reintenta o prueba otro país. |
| PROVIDER_CAPACITY | No podemos servir ese país temporalmente. Reintenta más tarde. |
| CONCURRENCY_LIMIT | Demasiadas activaciones pendientes. Finaliza o cancela algunas. |
| SPEND_LIMIT | Alcanzado el techo de gasto por hora. Reintenta pasada la ventana. |
| RATE_LIMITED | Baja el ritmo. Respeta Retry-After. |
| VALIDATION_ERROR | El cuerpo de tu petición es incorrecto. No reintentes sin cambiarlo. |
Ciclo de vida de una activación
Una activación está pending hasta que llega el código (received), la marcas como usada (finished) o se cancela. Cancelar devuelve exactamente lo que se te cobró.
Una activación pendiente caduca a los 20 minutos de la compra. La cancelamos en la red y te devolvemos el importe íntegro en cuanto esa cancelación queda confirmada: nunca te quedas pagando un número que no recibió nada.
Hay un periodo de gracia de unos 120 segundos antes de poder cancelar un número; cancelar antes devuelve EARLY_CANCEL_DENIED con los segundos que faltan.
Límites
Las peticiones de lectura, las de escritura y el total por usuario tienen cada una un techo por minuto, y existen topes opcionales de activaciones pendientes, gasto por hora y activaciones por hora. Tus valores actuales están en GET /api/v1/me, donde null significa sin límite. Si necesitas ampliarlos, abre un ticket: son ajustes por cuenta, no valores fijos en el código.
Referencia completa de endpoints
Documento OpenAPI 3.1 interactivo, generado siempre desde el código en ejecución.
Abrir la referencia