Si estás construyendo algo que involucra verificación por teléfono — registro de usuarios, pruebas automatizadas de flujos de onboarding, aprovisionamiento de cuentas en volumen, testing de integraciones, o pipelines de QA — hacer todo esto a mano a través de un dashboard se vuelve insostenible rápidamente. A la décima verificación manual ya es un cuello de botella. A la centésima, es imposible.
La API REST de SMSCode te permite automatizar el ciclo completo desde tu backend: solicitar un número virtual, esperar el SMS, extraer el código OTP y continuar con el flujo. Todo sin clics, sin intervención manual, directamente integrado en tu sistema.
Esta guía cubre autenticación, los endpoints principales con sus parámetros completos, ejemplos funcionales en curl y Python, los errores más comunes que vas a encontrar, y las buenas prácticas que separan una integración robusta de una que falla en producción.
TL;DR: La API V1 de SMSCode usa autenticación Bearer token. El flujo principal es: verificar saldo → consultar catálogo → crear pedido → hacer polling del OTP → completar o cancelar. Todas las respuestas siguen la forma
{ success, data }o{ success, error }. Cachea las llamadas al catálogo y haz polling cada 5 segundos para un uso eficiente que no consuma el rate limit.
Autenticación
Cada llamada a la API V1 necesita un Bearer token en el header Authorization. Lo encontrás en Configuración de cuenta dentro del dashboard.
Authorization: Bearer TU_API_TOKEN
Regla de oro para el token: Guardalo exclusivamente del lado del servidor. No lo incluyas en JavaScript del cliente, no lo pongas en archivos .env commiteados a git, no lo expongas en logs de aplicación. Si un token se compromete, regénéralo inmediatamente desde la configuración de tu cuenta — cualquier token anterior queda inválido instantáneamente.
La referencia completa de la API está en la documentación. Si aún no tenés cuenta, registrate aquí — necesitarás saldo para hacer pedidos reales.
Paso 1: Verificar tu saldo
Antes de hacer cualquier pedido, verificá que tu cuenta tiene crédito suficiente para la verificación que necesitás:
curl -X GET https://api.smscode.gg/v1/balance \
-H "Authorization: Bearer TU_API_TOKEN"
Respuesta exitosa:
{
"success": true,
"data": {
"currency": "IDR",
"balance": 150000
}
}
El saldo está denominado en IDR (Rupias indonesias). Si está en cero o es insuficiente para el producto que querés, recargá desde la página de precios antes de continuar. Los precios de productos también están denominados en IDR en la API — podés ver los equivalentes en USD en el dashboard web.
Equivalente en Python:
import requests
API_TOKEN = "TU_API_TOKEN"
BASE_URL = "https://api.smscode.gg/v1"
headers = {"Authorization": f"Bearer {API_TOKEN}"}
resp = requests.get(f"{BASE_URL}/balance", headers=headers)
resp.raise_for_status()
data = resp.json()
print(f"Saldo: {data['data']['balance']} {data['data']['currency']}")
Paso 2: Consultar el catálogo
El endpoint de catálogo devuelve los productos de números virtuales disponibles. Filtralo con los IDs enteros que devuelven /v1/catalog/countries y /v1/catalog/services; de ahí obtenés el product_id que vas a usar para crear el pedido.
curl -X GET "https://api.smscode.gg/v1/catalog/products?country_id=7&platform_id=1" \
-H "Authorization: Bearer TU_API_TOKEN"
La respuesta incluye IDs de producto, precios en IDR, niveles de stock disponibles, y metadatos del número. Podés filtrar por cualquier combinación de país y plataforma. El catálogo visual en el sitio web te muestra los mismos datos si querés explorar antes de escribir código.
Consejo crítico sobre el cacheo: No consultes el catálogo en cada pedido dentro de un loop de verificaciones. Los productos no cambian segundo a segundo. Cachéalo por al menos 60 segundos en producción. En sistemas de alta frecuencia, una caché de 5 minutos es perfectamente válida. Esto respeta los rate limits y reduce la latencia de tu sistema.
Equivalente en Python con selección del producto más económico:
params = {"country_id": 7, "platform_id": 1}
resp = requests.get(f"{BASE_URL}/catalog/products", headers=headers, params=params)
resp.raise_for_status()
products = resp.json()["data"]
if not products:
raise ValueError("No hay productos disponibles para este país y plataforma")
# Elegí el producto de menor precio con stock disponible
product = min((p for p in products if p["available"] > 0), key=lambda p: p["price"])
product_id = product["id"]
print(f"Producto seleccionado: {product_id} a {product['price']} IDR")
Paso 3: Crear un pedido
Una vez que tenés un product_id, creás el pedido para rentar el número:
IDEMPOTENCY_KEY="$(uuidgen)" # Generala una vez para este pedido lógico
curl -X POST https://api.smscode.gg/v1/orders/create \
-H "Authorization: Bearer TU_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{"product_id": 42}'
Respuesta exitosa:
{
"success": true,
"data": {
"orders": [
{
"id": 90210,
"phone_number": "+5215512345678",
"status": "ACTIVE",
"expires_at": "2026-03-16T12:05:00Z",
"product_id": 42,
"amount": 350
}
],
"failed_count": 0
}
}
El phone_number es el número que ingresás en la app o sitio web que estás verificando. El estado del pedido comienza como ACTIVE — eso es el comportamiento correcto. Ahora activá el proceso de envío del OTP desde el servicio destino usando ese número.
Equivalente en Python:
import uuid
idempotency_key = str(uuid.uuid4()) # Una clave para este pedido lógico
resp = requests.post(
f"{BASE_URL}/orders/create",
headers={
**headers,
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json={"product_id": product_id}
)
resp.raise_for_status()
order = resp.json()["data"]["orders"][0]
order_id = order["id"]
phone_number = order["phone_number"]
expires_at = order["expires_at"]
print(f"Número asignado: {phone_number}")
print(f"Válido hasta: {expires_at}")
Crear un pedido descuenta saldo. Si la respuesta se pierde o reintentás un
error transitorio, reutilizá el mismo Idempotency-Key con exactamente el mismo
body. No generes otra clave dentro del loop de reintento: una clave nueva puede
crear y cobrar un segundo pedido.
Importante: Guardá el order_id — lo vas a necesitar para el polling y para cancelar si fuera necesario. Y tené en cuenta el expires_at — si tu sistema tarda demasiado en activar el OTP, el pedido puede expirar antes de que el SMS llegue.
Paso 4: Hacer polling del OTP
Consultá el estado del pedido repetidamente hasta que llegue el SMS o hasta que el pedido expire:
curl -X GET https://api.smscode.gg/v1/orders/90210 \
-H "Authorization: Bearer TU_API_TOKEN"
Respuesta cuando llega el SMS:
{
"success": true,
"data": {
"id": 90210,
"status": "OTP_RECEIVED",
"otp_code": "847291",
"otp_message": "Tu código de verificación es 847291",
"otp_received_at": "2026-03-16T12:03:22Z",
"sms_revision": 1,
"phone_number": "+5215512345678"
}
}
Estados posibles del campo status:
| Estado | Significado | Acción recomendada |
|---|---|---|
ACTIVE |
Número activado, sin SMS todavía | Continuar polling |
OTP_RECEIVED |
SMS entregado; otp_code puede ser null |
Leer otp_code y otp_message |
COMPLETED |
Pedido finalizado exitosamente | No se requiere acción |
CANCELED |
Pedido cancelado | El reembolso solo aplica si no llegó ningún SMS |
EXPIRED |
Ventana cerrada sin SMS | Crear nuevo pedido |
Loop de polling completo en Python:
import time
max_wait = 120 # segundos máximos de espera
interval = 5 # consultar cada 5 segundos
elapsed = 0
otp = None
status = "ACTIVE"
sms_received = False
while elapsed < max_wait:
resp = requests.get(f"{BASE_URL}/orders/{order_id}", headers=headers)
resp.raise_for_status()
order_data = resp.json()["data"]
status = order_data["status"]
if order_data.get("otp_received_at") is not None:
sms_received = True
otp = order_data.get("otp_code")
message = order_data.get("otp_message")
print(f"SMS recibido: {message}; OTP clasificado: {otp}")
break
elif status in ("COMPLETED", "CANCELED", "EXPIRED"):
print(f"Pedido terminó con estado: {status}")
break
elif status == "ACTIVE":
print(f"Esperando... ({elapsed}s)")
time.sleep(interval)
elapsed += interval
if sms_received and otp is None:
print("El SMS llegó sin un código clasificado; usá otp_message")
elif not sms_received:
print("No se recibió SMS en el tiempo esperado")
Intervalos de 5 segundos son el punto óptimo para la mayoría de casos. Hacer polling más frecuente no acelera la entrega del SMS — el tiempo de entrega depende de la red del proveedor, no de la frecuencia de tus consultas. Lo que sí logra el polling agresivo es consumir tu cuota de rate limits más rápido.
Paso 5: Completar o cancelar
Después de procesar un SMS, finalizá el pedido. Si todavía no llegó ningún SMS y el campo can_cancel es true, podés cancelarlo:
curl -X POST https://api.smscode.gg/v1/orders/cancel \
-H "Authorization: Bearer TU_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": 90210}'
Política de reembolso:
- Cancelar antes de que llegue cualquier SMS y con
can_cancel=true→ reembolso automático - Después de cualquier SMS, incluso sin código clasificado → cancelación y reembolso cerrados
- El pedido expira sin SMS → reembolso automático sin necesidad de cancelar manualmente
Manejo de errores
Todos los errores de la API siguen la misma estructura consistente:
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "El saldo de la cuenta es insuficiente para este pedido."
}
}
Códigos de error que vas a encontrar:
| Código | HTTP Status | Significado y acción |
|---|---|---|
UNAUTHORIZED |
401 | Token inválido o ausente — verificá el token |
INSUFFICIENT_BALANCE |
409 | Saldo insuficiente — recargá antes de continuar |
NO_OFFER_AVAILABLE |
422 | Stock agotado — consultá catálogo para siguiente producto |
NOT_FOUND |
404 | ID de pedido incorrecto o de otra cuenta |
RATE_LIMIT_EXCEEDED |
429 | Demasiadas solicitudes — reducí la frecuencia |
El error NO_OFFER_AVAILABLE merece atención especial. El stock del catálogo es datos en tiempo real. Entre que consultás el catálogo y hacés el pedido, un producto popular puede agotarse — especialmente en plataformas de alto volumen como WhatsApp o Instagram donde hay mucha demanda simultánea. Tu integración debe manejar este error con un retry automático sobre el siguiente producto disponible en tus resultados del catálogo.
# Manejo correcto de NO_OFFER_AVAILABLE con fallback
import uuid
def create_order_with_fallback(products, headers):
for product in products:
idempotency_key = str(uuid.uuid4()) # Una clave para este body
resp = requests.post(
f"{BASE_URL}/orders/create",
headers={
**headers,
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json={"product_id": product["id"]}
)
data = resp.json()
if data.get("success"):
return data["data"]["orders"][0]
if data.get("error", {}).get("code") == "NO_OFFER_AVAILABLE":
continue # Intentar con el siguiente producto
raise Exception(f"Error inesperado: {data}")
raise Exception("No hay productos disponibles en ningún rango de precio")
Rate limits
La API aplica rate limits por cuenta. Superarlos devuelve HTTP 429 con código de error RATE_LIMIT_EXCEEDED. Los rate limits están diseñados para cubrir cargas de automatización normales — si los estás alcanzando consistentemente, hay un problema de diseño en tu integración, no de capacidad de la API.
Dos prácticas que te mantienen por debajo del limit:
- Cachear respuestas del catálogo durante al menos 60 segundos
- Hacer polling cada 5 segundos, no bombardear el endpoint de órdenes
Errores comunes de integración que debes evitar
No manejar NO_OFFER_AVAILABLE. Como se explica arriba, el stock puede vaciarse entre tu llamada al catálogo y tu pedido. Este error ocurrirá en producción — no es un edge case sino un caso esperado.
Polling demasiado agresivo. Consultar el endpoint de órdenes cada segundo no hace que el SMS llegue más rápido. Solo consume tu cuota de rate limits. Cinco segundos es el intervalo correcto.
Hardcodear disponibilidad o precio. El product_id es un identificador estable, pero su precio y el contador available cambian. Consultá el catálogo en runtime con country_id y platform_id antes de comprar.
Ignorar expires_at. Los pedidos tienen una ventana de verificación finita. Si tu sistema espera demasiado para activar el OTP desde el servicio destino, el pedido expira y perdés el número. Activá el OTP en el servicio destino inmediatamente después de obtener el número — no cuando tengas “tiempo”.
No manejar el estado EXPIRED correctamente. El estado EXPIRED es un resultado normal, no un error catastrófico. Tu sistema debe detectarlo, crear un nuevo pedido con un número diferente, y reintentar el flujo completo.
Mezclar COMPLETED y OTP_RECEIVED en la lógica. Actuá sobre la evidencia de entrega (otp_received_at): otp_code puede ser null aunque otp_message esté presente. COMPLETED llega después y confirma que el ciclo cerró.
Casos de uso avanzados
Verificación en lote con procesamiento asíncrono
Si necesitás verificar cientos de cuentas por día, el flujo asíncrono es significativamente más eficiente que el secuencial. En lugar de esperar a que cada pedido resuelva su OTP antes de crear el siguiente, creás múltiples pedidos en paralelo y hacés polling de todos simultáneamente.
import asyncio
async def poll_order(session, order_id, headers, max_wait=120):
"""Poll un pedido hasta la primera entrega SMS o un estado terminal."""
for _ in range(max_wait // 5):
async with session.get(
f"{BASE_URL}/orders/{order_id}",
headers=headers
) as resp:
data = await resp.json()
status = data["data"]["status"]
order = data["data"]
if order.get("otp_received_at") is not None:
return order
if status in ("COMPLETED", "CANCELED", "EXPIRED"):
return order
await asyncio.sleep(5)
return None
# Crear múltiples pedidos en paralelo y hacer polling simultáneo
Testing en pipelines de CI/CD
Integrá la API de SMSCode en tu pipeline de CI/CD para ejecutar pruebas de flujo de verificación reales en el entorno de staging antes de cada deploy. Esto detecta regresiones en el flujo de onboarding antes de que lleguen a producción.
El token de API para testing puede ser diferente al de producción — creá tokens separados en el dashboard para distintos entornos y rotarlos según tu política de seguridad.
Verificación multi-país para testing de cobertura regional
Si tu app soporta usuarios de México, Colombia, Argentina y Chile, resolvé sus IDs desde el catálogo y consultá cada combinación disponible con el mismo token.
country_rows = requests.get(
f"{BASE_URL}/catalog/countries",
headers=headers,
).json()["data"]
wanted_codes = {"mx", "co", "ar", "cl"}
for country in (row for row in country_rows if row["code"] in wanted_codes):
services = requests.get(
f"{BASE_URL}/catalog/services",
headers=headers,
params={"country_id": country["id"]},
).json()["data"]
whatsapp = next((service for service in services if service["code"] == "whatsapp"), None)
if whatsapp is None:
continue
params = {"country_id": country["id"], "platform_id": whatsapp["id"]}
products = requests.get(
f"{BASE_URL}/catalog/products",
headers=headers,
params=params
).json()["data"]
# crear pedido, hacer polling, verificar que el OTP llega
Revisá cómo elegir el país adecuado para más contexto sobre qué países tienen mejor tasa de éxito para cada plataforma.
Webhooks
Los webhooks ya están disponibles. Configurá la URL HTTPS y el secreto con PATCH /v1/webhook; SMSCode enviará eventos firmados cuando llegue cada SMS y cuando cambie el ciclo de vida del pedido. Verificá X-Webhook-Signature: sha256=<hex> sobre el body crudo y mantené el polling como ruta de recuperación. La documentación incluye el endpoint de prueba.
Para una visión más amplia de SMSCode y sus casos de uso, consultá la evaluación completa de SMSCode. Si el costo es un factor clave para tu integración de alto volumen, la guía de mejores servicios de números virtuales 2026 compara las alternativas del mercado.
FAQ
¿Dónde encuentro mi token de API?
En el dashboard de SMSCode bajo Configuración de cuenta. Si aún no tenés cuenta, registrate aquí — toma menos de un minuto y no necesitás número de teléfono.
¿Existe un modo sandbox o de pruebas?
Actualmente no hay entorno sandbox separado. Todas las llamadas a la API operan contra el sistema real con números reales. Para desarrollo y testing inicial, usá los productos más económicos disponibles (Indonesia, India). Los costos de testing se mantienen muy bajos cuando solo validás la lógica de tu integración.
¿Cuál es la diferencia entre COMPLETED y OTP_RECEIVED?
OTP_RECEIVED significa que el SMS llegó. Un código detectado aparece en otp_code; si no hay uno seguro, ese campo es null y el texto después de eliminar U+0000 sigue en otp_message. COMPLETED significa que el ciclo de vida del pedido terminó completamente.
¿Puedo recibir múltiples OTPs en el mismo número?
Sí, durante el período activo del pedido. Cada SMS nuevo actualiza otp_message e incrementa sms_revision. otp_code cambia solo cuando el mensaje nuevo contiene un código detectado de forma segura; un mensaje de texto o enlace conserva el último código válido. Continuá haciendo polling hasta tener lo que necesitás o hasta que el pedido expire.
¿Cómo manejo una verificación fallida donde sí llegó el OTP?
Si el OTP llegó correctamente pero el servicio destino lo rechazó (código incorrecto por tipeo, expirado en el servicio destino, validación fallida por otra razón), no hay reembolso porque la entrega de SMS fue exitosa. Creá un nuevo pedido con un número diferente y reintentá el flujo completo. Para este caso, el costo extra es el precio de un número adicional.
¿La API tiene límites de volumen para cuentas estándar?
Los rate limits están diseñados para cubrir cargas de automatización normales sin restricciones en el uso cotidiano. Si tenés un caso de uso de muy alto volumen — miles de verificaciones por hora — contactá al soporte antes de escalar para coordinar la capacidad necesaria.