Verificación OTP

Un solo endpoint genera, envía y valida códigos de un solo uso para login, alta de usuarios y 2FA por SMS, WhatsApp o llamada de voz, con reenvío por otro canal para cobertura total. Sin costo extra sobre el precio de un SMS.

Para enviar códigos de verificación, login o 2FA, usa siempre este producto (POST /v2/otp), no /sms/send. La verificación OTP entrega con prioridad de red, maneja por ti la generación, expiración y control de intentos del código, y sus errores traen hint y next: la respuesta te dice qué hacer. Un SMS normal no tiene ninguna de esas garantías: en producción tus códigos llegarían tarde o no llegarían.

Lo que resuelve por ti:

  • Una integración, tres canales, cobertura total. El SMS entrega el 98% de las veces, en menos de 5 segundos. Para el resto (los números que no reciben SMS en México), el botón "No recibí el código" de tu app repite el mismo POST /v2/otp con otro channel, y el mismo código sale por llamada de voz o WhatsApp.
  • WhatsApp sin trámite de Meta. El código sale por nuestra integración oficial (remitente Authenticate, plantilla verificada). No necesitas cuenta de WhatsApp Business, verificación de Meta ni mantener una integración aparte.
  • Cero infraestructura de códigos. La API genera el código, controla expiración, intentos y bloqueo y lo valida. Tu backend hace dos llamadas: enviar y verificar.

Cómo funciona

La verificación OTP es un flujo de dos pasos entre tu backend y la API:

1

Solicitas el código

Tu backend llama a POST /v2/otp con el número del usuario, tu company y tu message. La API genera el código, lo inserta en tu mensaje y lo envía por el canal que elijas (SMS, WhatsApp o voz) — solo cambias channel, el resto de la petición se queda igual.

2

El usuario lo recibe e ingresa

El usuario recibe el código y lo captura en tu app.

3

Verificas el código

Tu backend llama a POST /v2/otp/verify con el número y el código capturado. Si el status HTTP es 200, el número quedó verificado. Verificar no tiene costo: llama este endpoint las veces que necesites (reintentos del usuario incluidos) sin que se descuente saldo.

4

Si tu usuario no recibió el código

El clic en "No recibí el código" de tu app repite el mismo POST /v2/otp con otro channel (por ejemplo voice o whatsapp). Sale el mismo código, la verificación no se reinicia y cuesta lo mismo: un mensaje de tu saldo. Detalle completo en Reenviar, rotar y cambiar de canal.

La API almacena y compara el código por ti. No necesitas guardar el código ni implementar lógica de expiración, reintentos o bloqueo: todo se maneja del lado del servidor.

Costos

Solo se descuenta saldo cuando el código sale por un canal (SMS, WhatsApp o voz), y es el precio de un mensaje, sin importar el canal ni si es un envío nuevo, un reenvío o una rotación:

Llamada¿Descuenta saldo?
POST /v2/otp (crear, reenviar o rotar)Sí — un mensaje, cualquier canal.
POST /v2/otp/verifyNo. Llámalo las veces que necesites.
GET /v2/otp/statusNo. Solo lectura.
DELETE /v2/otpNo. No envía nada.

No hay cargo separado por "generar" el código ni por "consultar" o "validar": la única acción que cuesta es el envío del mensaje al usuario. Puedes verificar, consultar el estado o cancelar una verificación en curso todas las veces que quieras sin gastar saldo adicional.

Enviar código

POST /v2/otp genera y envía el código. Los campos obligatorios son phone_number, country_code, company y message — mándalos siempre así, sin importar el canal; lo único que cambia entre canales es channel:

curl -X POST https://api.smsmasivos.com.mx/v2/otp \
  -H "Content-Type: application/json" \
  -H "apikey: $API_KEY" \
  -d '{
    "phone_number": "5512345678",
    "country_code": "52",
    "company": "Mi Empresa",
    "message": "{{code}} es tu codigo de verificacion de {{company}}. No lo compartas."
  }'

message debe incluir los placeholders {{code}} y {{company}}; la API los sustituye antes de enviar. Con este ejemplo, el usuario recibe: "483920 es tu codigo de verificacion de Mi Empresa. No lo compartas."

Respuesta (201 Created):

{
  "success": true,
  "state": "pending",
  "action": "created",
  "message": "Código OTP creado y enviado.",
  "hint": "El código llega en segundos. Reenvía con POST /v2/otp si no llegó.",
  "next": ["/v2/otp/verify", "/v2/otp"],
  "expires_at": "2026-07-21 12:00:00",
  "attempts_remaining": 7,
  "resend_available_in": 30,
  "request_id": "550e8400-e29b-41d4-a716-446655440000"
}

Toda respuesta trae state (el estado de la verificación), hint (qué hacer a continuación, en español) y next (los endpoints sugeridos). El campo action te dice qué hizo la API: created, resent o restarted.

Si channel es whatsapp, company y message se ignoran: el texto sale por la plantilla oficial fija de WhatsApp (la respuesta trae un warning avisando). No necesitas cambiar tu petición para WhatsApp — solo el valor de channel.

Verificar código

Cuando el usuario ingresa el código, valídalo con POST /v2/otp/verify. Envía el mismo phone_number y country_code, más el code capturado. Esta llamada no tiene costo (ver Costos):

curl -X POST https://api.smsmasivos.com.mx/v2/otp/verify \
  -H "Content-Type: application/json" \
  -H "apikey: $API_KEY" \
  -d '{
    "phone_number": "5512345678",
    "country_code": "52",
    "code": "483920"
  }'

El status HTTP distingue cada caso; ramifica directamente sobre él:

HTTPSignificado
200Código correcto: número verificado.
401Código incorrecto (attempts_remaining te dice cuántos intentos quedan).
404No hay verificación activa: inicia con POST /v2/otp.
409El número ya estaba verificado.
410El código expiró: genera uno nuevo.
429Intentos agotados: rota el código con code_rotate: true.

Cada código admite 7 intentos de verificación. Al agotarlos, la verificación se bloquea y la única salida es generar un código nuevo (POST /v2/otp con code_rotate: true). Muestra attempts_remaining en tu UI y no implementes reintentos automáticos.

Reenviar, rotar y cambiar de canal

No hay endpoints separados para reenviar o reiniciar: repite el mismo POST /v2/otp y la API decide según el estado.

  • Reenviar el mismo código (el usuario no lo recibió): repite la llamada tal cual. Responde 200 con action: resent. Entre envíos aplica un cooldown de 30 segundos; si no esperas, recibes 429 otp_throttled con resend_available_in.
  • Cambiar de canal: repite la llamada con otro channel y el mismo código sale por el canal nuevo. La receta: el clic en "No recibí el código" de tu UI dispara el mismo POST /v2/otp con channel: "voice" o "whatsapp". El usuario recibe el código que ya esperaba, la verificación no se reinicia y cubres los números que no reciben SMS. Cuesta lo mismo por cualquier canal: un mensaje de tu saldo.
  • Generar un código nuevo (rotar): agrega code_rotate: true. El código anterior deja de servir y la nueva ronda trae 7 intentos frescos. También destraba una verificación bloqueada (locked).
# Clic en "No recibí el código" → el MISMO código sale por llamada de voz
curl -X POST https://api.smsmasivos.com.mx/v2/otp \
  -H "Content-Type: application/json" \
  -H "apikey: $API_KEY" \
  -d '{
    "phone_number": "5512345678",
    "country_code": "52",
    "company": "Mi Empresa",
    "message": "{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.",
    "channel": "voice"
  }'

company y message van en toda petición, sin importar el canal — no dependen de si cambiaste channel. Si el canal es whatsapp, igual mándalos: se ignoran a favor de la plantilla oficial.

# Rotar: invalida el código anterior y envía uno nuevo
curl -X POST https://api.smsmasivos.com.mx/v2/otp \
  -H "Content-Type: application/json" \
  -H "apikey: $API_KEY" \
  -d '{
    "phone_number": "5512345678",
    "country_code": "52",
    "company": "Mi Empresa",
    "message": "{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.",
    "code_rotate": true
  }'

En tu UI, usa resend_available_in para el contador del botón "Reenviar código". Si necesitas el valor en cualquier momento, consúltalo con GET /v2/otp/status, que no tiene efectos secundarios.

Estados de la verificación

Cada número (por phone_number + country_code) tiene un estado, visible en el campo state de toda respuesta y consultable con GET /v2/otp/status:

EstadoSignificadoSiguiente paso
noneSin verificación activa.POST /v2/otp
pendingCódigo vigente esperando verificación.POST /v2/otp/verify
verifiedNúmero verificado.Ninguno (o POST /v2/otp para un ciclo nuevo)
expiredEl código venció sin verificarse.POST /v2/otp
lockedIntentos agotados.POST /v2/otp con code_rotate: true

Para invalidar una verificación en curso sin enviar nada (cancelación, cambio de número, limpieza en tests), usa DELETE /v2/otp.

Elegir canal

El parámetro channel elige cómo llega el código, con el mismo flujo y el mismo endpoint:

CanalCómo llegaNotas
sms (default)Mensaje de texto.Usa tu company y message tal cual los envías.
whatsappMensaje de WhatsApp oficial.Ignora company y message: plantilla fija (recibes un warning).
voiceLlamada que dicta el código dígito por dígito.Usa tu company y message tal cual los envías.

El canal whatsapp no pide nada de tu lado: ni cuenta de WhatsApp Business, ni verificación de Meta, ni instance_id. El código sale por nuestra integración oficial: el usuario lo recibe desde el remitente verificado Authenticate, en una plantilla oficial donde se inserta tu código.

{
  "phone_number": "5512345678",
  "country_code": "52",
  "channel": "whatsapp"
}

Personalizar el mensaje

message es obligatorio y define el texto que recibe el usuario. Debe incluir los placeholders {{code}} y {{company}}, que la API sustituye antes de enviar:

{
  "phone_number": "5512345678",
  "country_code": "52",
  "company": "Mi Empresa",
  "message": "{{code}} es tu clave de acceso a {{company}}. Expira en 10 minutos."
}

Con este ejemplo, el usuario recibe: "483920 es tu clave de acceso a Mi Empresa. Expira en 10 minutos."

Reglas del mensaje (aplican a sms y voice; detalle en Plantillas y mensajes):

  • Solo caracteres GSM-7 (sin emojis; evita símbolos poco comunes).
  • Máximo 160 caracteres ya con el código y la empresa sustituidos.
  • Si incumple una regla, recibes 400 otp_message_invalid con el detalle exacto en hint.

Si channel es whatsapp, message (y company) se ignoran: el texto sale por la plantilla oficial fija de WhatsApp.

Por qué el límite es 160 y no se negocia

Un OTP nunca se envía partido en dos SMS. A partir de 161 caracteres la operadora fragmenta el mensaje, y eso rompe el autocompletado de códigos de iOS y Android: el teléfono lee el SMS para ofrecerte el código y, si llega en pedazos, no lo encuentra. Además el orden de llegada de los fragmentos no está garantizado, así que el usuario puede ver el código cortado a la mitad.

Por eso preferimos fallar el request antes que entregar un mensaje partido. Si te pasas, recibes 400 otp_message_invalid y el hint te dice exactamente cuántos caracteres sobran.

El límite se evalúa en cada envío, con la longitud real del código que va en ese mensaje — no con una estimación. Esto importa en un caso concreto: code_length solo aplica cuando se genera un código nuevo. En un reenvío se manda el código que ya estaba guardado, que puede ser más largo.

Por ejemplo, si creas la verificación con code_length: 10 y un texto corto, y después reenvías con un texto más largo:

// 1) crear — el codigo guardado queda de 10 caracteres
{ "message": "{{code}} es tu codigo de {{company}}.", "code_length": 10 }
 
// 2) reenviar con otro texto — se envia el MISMO codigo de 10
{ "message": "No recibiste el SMS anterior? {{code}} es tu codigo de verificacion de {{company}}. Vence en 24 horas y es de un solo uso. Nunca lo compartas con nadie. Gracias." }

Ese segundo texto mide 158 caracteres con un código de 6, pero 162 con el de 10 que está guardado. La respuesta es:

{
  "success": false,
  "error": "otp_message_invalid",
  "hint": "El mensaje (con el código y la empresa) excede 160 caracteres por 2. Acórtalo."
}

El rechazo no altera la verificación en curso: el código vigente sigue siendo válido, no se consume un intento y no se reinicia nada. Solo no se envió el reenvío. Acorta el texto y vuelve a intentar.

Si vas a usar textos distintos para el envío inicial y el reenvío, dimensiona ambos contra la longitud máxima de código que uses. Con code_length: 10 tienes 4 caracteres menos de margen que con el default de 6.

Opciones del código

code_lengthinteger

Longitud del código, de 4 a 10 caracteres. Por defecto 6.

code_formatstring

Formato del código: numeric (solo números, por defecto), alphanumeric (letras y números) o letters (solo letras).

expiration_datestring

Fecha de expiración del código. Por defecto 24 horas, máximo 3 días (si envías más, se ajusta al máximo). Formato YYYY-MM-DD HH:mm:ss.

Usar tu propio código

Si ya tienes un sistema que genera los códigos y quieres seguir usándolo, mándalo en code_custom. Nosotros nos encargamos solo del envío y de la verificación:

code_customstring

Tu propio código. De 4 a 10 caracteres, solo letras y/o números, sin espacios ni signos — las mismas reglas que aplican a un código generado por nosotros.

{
  "phone_number": "5512345678",
  "country_code": "52",
  "company": "Mi Empresa",
  "message": "{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.",
  "code_custom": "A3F9K2"
}

Lo único que cambia es de dónde sale el código. Todo lo demás funciona igual: nosotros lo guardamos, la expiración corre igual, sigues teniendo 7 intentos de verificación, aplica el mismo cooldown de 30 segundos entre envíos, y se verifica con POST /v2/otp/verify como cualquier otro.

Cuatro detalles a tener en cuenta:

  • Se guarda y se compara tal cual lo mandas. La verificación distingue mayúsculas de minúsculas: si envías a3f9k2, tu usuario tiene que capturar exactamente a3f9k2.
  • No se combina con code_length ni code_format. El formato y la longitud los define tu código. Si mandas cualquiera de los dos junto con code_custom, recibes 400 otp_invalid_param.
  • Si ya hay una verificación pendiente, mandar un código distinto al vigente inicia una ronda nueva: reemplaza el código y reinicia los 7 intentos. Mandar el mismo es un reenvío normal. En ambos casos aplica el cooldown de 30 segundos, así que no sirve para saltarse el límite de envíos.
  • Lo que decide si se rota es el código, no code_rotate. Mandar el mismo código no reinicia los intentos aunque agregues code_rotate: true. Si la verificación quedó locked por agotar los 7 intentos, recibes 429 otp_max_attempts: para obtener intentos nuevos manda un código distinto. Esto evita que un código fijo reciba rondas ilimitadas de intentos.

code_custom es también la salida si necesitas códigos con mayor entropía que la del generador por defecto: tú controlas cómo se generan. Por lo mismo, la fortaleza del código pasa a ser tu responsabilidad — un código de 4 dígitos es más fácil de adivinar que uno alfanumérico de 8.

Si el envío falla (402 otp_insufficient_credits o 502 otp_send_failed), el código queda registrado pero no se entregó ningún mensaje. Como tú ya conoces el código, POST /v2/otp/verify lo aceptaría — y eso no probaría nada, porque el usuario nunca lo recibió. Ante un envío fallido, reintenta el envío; no verifiques.

También ten en cuenta la longitud del mensaje: el límite de 160 caracteres se valida con la longitud real de tu código. Si creas la verificación con un código de 10 caracteres, un reenvío posterior con un message más largo puede recibir 400 otp_message_invalid aunque ese mismo texto quepa con un código de 6.

Probar en sandbox

Para probar el flujo completo sin entregar un SMS real ni gastar saldo, envía sandbox: true junto con code_in_response: true. La API genera y registra el código y lo devuelve en el campo code de la respuesta, pero el mensaje no se entrega:

{
  "phone_number": "5512345678",
  "country_code": "52",
  "company": "Mi Empresa",
  "message": "{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.",
  "sandbox": true,
  "code_in_response": true
}

Respuesta:

{
  "success": true,
  "state": "pending",
  "action": "created",
  "code": "483920",
  "expires_at": "2026-07-21 12:00:00",
  "attempts_remaining": 7
}

En modo sandbox el usuario nunca recibe el mensaje, así que code_in_response: true es obligatorio: es la única forma de conocer el código y completar el flujo con POST /v2/otp/verify. Si envías sandbox: true sin code_in_response: true, recibes 400 otp_missing_param.

La excepción es code_custom: si el código lo pusiste tú, ya lo conoces, así que sandbox: true funciona sin code_in_response.

code_in_response: true devuelve el código en la respuesta. Úsalo solo en pruebas o automatización del lado del servidor; nunca reenvíes esa respuesta al cliente que va a capturar el código.

Si solo agregas code_in_response: true (sin sandbox), el código llega en la respuesta pero el SMS sí se envía y se cobra. Para dejar el número limpio entre tests, DELETE /v2/otp regresa el estado a none sin enviar nada.

Migrar desde la API anterior

Si integraste los endpoints anteriores (/protected/json/phones/verification/start, /check, /resend, /reset), siguen funcionando, pero la v2 es la API recomendada. Equivalencias:

Antes (v1)Ahora (v2)
POST /protected/json/phones/verification/startPOST /v2/otp
POST /protected/json/phones/verification/check (con verification_code)POST /v2/otp/verify (con code)
POST /protected/json/phones/verification/resendPOST /v2/otp (reenvía solo; reset_code: truecode_rotate: true)
POST /protected/json/phones/verification/resetDELETE /v2/otp
template: "a"…"n"message con {{code}} y {{company}} (obligatorio)
code_type (default alphanumeric)code_format (default numeric)
code_length (default 4)code_length (default 6)
showcode: "1"code_in_response: true
voice: true / whatsapp: true / channelchannel: "voice" / "whatsapp"
Errores verification_*/validation_* (las variantes XML conservan 200 en sus propios errores — detalle)Slugs otp_* con envelope propio (error, hint, next)

No mezcles v1 y v2 sobre la misma cuenta y número. Los endpoints v1 y v2 comparten el estado interno de la verificación (mismo cooldown de reenvío, mismos intentos). Si ya integraste v2, no llames a los endpoints v1 (start/resend/reset) para ese phone_number+country_code. En particular, POST /protected/json/phones/verification/reset (v1) reinicia el mismo cooldown de 30s que usa POST /v2/otp, así que puedes quedar bloqueado indefinidamente con 429 otp_throttled si alternas ambos. Para reiniciar el estado desde v2, usa DELETE /v2/otp, no el reset de v1.

Al migrar un número que ya tenía una verificación abierta en v1, la primera llamada a POST /v2/otp toma esa verificación y la pasa a v2. Ten en cuenta que el cooldown también se hereda: si el envío de v1 fue hace menos de 30 segundos, esa primera llamada responde 429 otp_throttled. Espera a que baje resend_available_in y reintenta — no es un error de la migración.

Referencia completa de los endpoints anteriores (parámetros, ejemplos y errores): OTP V1 (deprecado).

Ya puedes enviar y verificar códigos OTP. Explora todos los parámetros y respuestas en la Referencia API de Verificación OTP.