Verificación OTP
Verificación en dos pasos (OTP): códigos de un solo uso por SMS, WhatsApp o llamada de voz, con una sola integración para los tres canales.
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 (channel: sms, whatsapp o voice) — solo cambias channel.
El usuario lo recibe e ingresa
El usuario recibe el código y lo captura en tu app. Por WhatsApp sale por nuestra integración oficial: sin cuenta de WhatsApp Business ni verificación de Meta.
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. No tiene costo.
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.
Usar estos métodos no tiene costo extra: solo se descuenta un mensaje de tu saldo cuando la API le envía el código al usuario por SMS, WhatsApp o llamada de voz (POST /v2/otp, ya sea al crear, reenviar o rotar), sin importar el canal. Verificar el código, consultar su estado o invalidarlo nunca tienen costo, sin importar cuántas veces los llames.
Los otros dos endpoints son auxiliares; en un flujo normal no los necesitas:
GET /v2/otp/status: solo lectura, para tu UI (contador de reenvío, intentos restantes).DELETE /v2/otp: cancela una verificación en curso (limpieza en tests, cambio de número). No envía nada.
Usa estos endpoints para códigos de login, verificación de cuenta, 2FA y recuperación de contraseña. No construyas un sistema OTP manual sobre /sms/send: perderías la generación, expiración, validación y control de intentos que este flujo ya maneja. Guía completa en Verificación OTP.
Enviar Código de Verificación (OTP)
channel: el mismo código sale por el canal nuevo y cubres los números que no reciben SMS. La respuesta siempre te dice qué hizo (action) y qué sigue (next).52.Canal de entrega del código:
sms(default): mensaje de texto.whatsapp: mensaje de WhatsApp oficial con plantilla fija (ignoramessageycompany). No requiere cuenta de WhatsApp Business ni verificación de Meta: sale por nuestra integración oficial (remitenteAuthenticate).voice: llamada que dicta el código dígito por dígito.
En un reenvío puedes cambiar de canal: el mismo código pendiente sale por el canal nuevo (fallback).
smswhatsappvoiceNombre de tu empresa o app. Se inserta en message mediante el placeholder {{company}}.
Si channel es whatsapp, se ignora: la plantilla oficial de WhatsApp es fija (recibes un warning avisando).
Texto que recibe el usuario. Debe incluir los placeholders {{code}} y {{company}}, usar solo caracteres GSM-7 (sin emojis ni acentos raros) y no exceder 160 caracteres ya con el código y la empresa sustituidos.
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 del teléfono. Por eso el request falla en vez de enviarse. El límite se evalúa en cada envío con la longitud real del código que va en ese mensaje. En un reenvío se manda el código ya guardado, que puede ser más largo que el code_length de ese request: si creaste con code_length: 10 o con un code_custom largo, dimensiona el texto del reenvío contra esa longitud. El rechazo no altera la verificación en curso.
Ejemplo armado (con code: "483920" y company: "Mi Empresa"): 483920 es tu codigo de verificacion de Mi Empresa. No lo compartas.
Si channel es whatsapp, se ignora: el texto sale por la plantilla oficial fija de WhatsApp (recibes un warning avisando).
Tu propio código, si ya tienes un sistema que los genera y quieres seguir usándolo. Nosotros solo lo enviamos y lo verificamos.
Debe cumplir las mismas reglas que un código generado por nosotros: de 4 a 10 caracteres, solo letras y/o números, sin espacios ni signos.
- Se guarda y se verifica tal cual lo envías: la comparación distingue mayúsculas de minúsculas, así que si mandas
a3f9k2tu usuario debe capturar exactamentea3f9k2. - No se combina con
code_formatnicode_length(el formato y la longitud los define tu código). Si envías cualquiera de los dos junto concode_custom, responde400 otp_invalid_param. - Si ya hay una verificación pendiente y el código que envías es distinto al vigente, se inicia una ronda nueva (reemplaza el código y reinicia los 7 intentos). Si es el mismo, es un reenvío normal. En ambos casos aplica el cooldown de 30s.
- Con
code_custom, lo que decide si se rota es el código, nocode_rotate. Mandar el mismo código no reinicia los intentos aunque agreguescode_rotate: true: si la verificación estálocked, responde429 otp_max_attempts. Para obtener 7 intentos nuevos manda un código distinto. - Con
sandbox: trueno necesitascode_in_response: true: ya conoces el código. - Si el envío falla (
402 otp_insufficient_creditso502 otp_send_failed), el código queda registrado pero no se entregó nada: reintenta el envío, no verifiques.
Formato del código generado:
numeric(default): solo números.alphanumeric: letras y números.letters: solo letras.
No aplica si envías code_custom (el formato lo define tu código).
numericalphanumericletterscode_custom (la longitud la define tu código).true descarta el código vigente y genera uno nuevo (nueva ronda con 7 intentos). Sin él, un código vigente se reenvía tal cual. El cooldown de 30s aplica igual.Devuelve el código generado en el campo code de la respuesta:
false(default): no devolver.true: devolver (pruebas/automatización del lado del servidor).
true el flujo se ejecuta completo (genera y registra el código) pero el SMS no se entrega y no se descuenta saldo. Como el usuario nunca recibe el mensaje, debes enviar también code_in_response: true para obtener el código en la respuesta; si falta, responde 400 otp_missing_param. La excepción es code_custom: si tú pusiste el código, ya lo conoces y code_in_response no hace falta.YYYY-MM-DD HH:mm:ss (futura). Por defecto 24 horas; máximo 3 días (si envías más, se ajusta al máximo).curl -X POST https://api.smsmasivos.com.mx/v2/otp \
-H "Content-Type: application/json" \
-H "apikey: TU_API_KEY" \
-d '{
"phone_number": "5512345678",
"country_code": "52",
"company": "Mi Empresa",
"message": "{{code}} es tu codigo de verificacion de {{company}}. No lo compartas."
}'
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.smsmasivos.com.mx/v2/otp');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'apikey: ' . getenv('API_KEY')
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'phone_number' => '5512345678',
'country_code' => '52',
'company' => 'Mi Empresa',
'message' => '{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.'
]));
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status . ' ' . $response;
import requests
response = requests.post(
'https://api.smsmasivos.com.mx/v2/otp',
headers={
'Content-Type': 'application/json',
'apikey': 'TU_API_KEY'
},
json={
'phone_number': '5512345678',
'country_code': '52',
'company': 'Mi Empresa',
'message': '{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.'
}
)
print(response.status_code, response.json())
const response = await fetch('https://api.smsmasivos.com.mx/v2/otp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.API_KEY
},
body: JSON.stringify({
phone_number: '5512345678',
country_code: '52',
company: 'Mi Empresa',
message: '{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.'
})
});
const data = await response.json();
console.log(response.status, data);
const axios = require('axios');
const { status, data } = await axios.post(
'https://api.smsmasivos.com.mx/v2/otp',
{
phone_number: '5512345678',
country_code: '52',
company: 'Mi Empresa',
message: '{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.'
},
{
headers: {
'Content-Type': 'application/json',
'apikey': process.env.API_KEY
},
validateStatus: () => true
}
);
console.log(status, data);
require 'net/http'
require 'json'
uri = URI('https://api.smsmasivos.com.mx/v2/otp')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path, {
'Content-Type' => 'application/json',
'apikey' => ENV['API_KEY']
})
request.body = {
phone_number: '5512345678',
country_code: '52',
company: 'Mi Empresa',
message: '{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.'
}.to_json
response = http.request(request)
puts "#{response.code} #{JSON.parse(response.body)}"
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("apikey", "TU_API_KEY");
var payload = new
{
phone_number = "5512345678",
country_code = "52",
company = "Mi Empresa",
message = "{{code}} es tu codigo de verificacion de {{company}}. No lo compartas."
};
var response = await client.PostAsJsonAsync(
"https://api.smsmasivos.com.mx/v2/otp",
payload
);
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{(int)response.StatusCode} {result}");
OkHttpClient client = new OkHttpClient();
String json = "{"
+ "\"phone_number\":\"5512345678\","
+ "\"country_code\":\"52\","
+ "\"company\":\"Mi Empresa\","
+ "\"message\":\"{{code}} es tu codigo de verificacion de {{company}}. No lo compartas.\""
+ "}";
Request request = new Request.Builder()
.url("https://api.smsmasivos.com.mx/v2/otp")
.post(RequestBody.create(json, MediaType.parse("application/json")))
.addHeader("apikey", "TU_API_KEY")
.build();
Response response = client.newCall(request).execute();
System.out.println(response.code() + " " + response.body().string());
{ "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, "warning": "channel=whatsapp usa una plantilla oficial fija; se ignoraron message/company.", "code": "483920", "request_id": "550e8400-e29b-41d4-a716-446655440000" }
{ "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" }
{ "success": false, "state": "pending", "error": "otp_incorrect_code", "message": "El código es incorrecto.", "hint": "Código incorrecto. Te quedan 3 intentos.", "next": [ "/v2/otp/verify", "/v2/otp" ], "expires_at": "2026-07-21 12:00:00", "attempts_remaining": 3, "resend_available_in": 18, "code": "sms_07", "request_id": "550e8400-e29b-41d4-a716-446655440000" }
{ "success": false, "state": "none", "error": "otp_insufficient_credits", "message": "Saldo insuficiente para enviar el código.", "hint": "Recarga saldo para enviar el código.", "next": [], "code": "sms_07" }
{ "success": false, "state": "pending", "error": "otp_incorrect_code", "message": "El código es incorrecto.", "hint": "Código incorrecto. Te quedan 3 intentos.", "next": [ "/v2/otp/verify", "/v2/otp" ], "expires_at": "2026-07-21 12:00:00", "attempts_remaining": 3, "resend_available_in": 18, "code": "sms_07", "request_id": "550e8400-e29b-41d4-a716-446655440000" }
{ "success": false, "error": "otp_internal", "message": "Ocurrió un error inesperado.", "hint": "Intenta de nuevo en unos momentos.", "next": [] }
{ "success": false, "state": "none", "error": "otp_send_failed", "message": "No se pudo enviar el código. Intenta de nuevo.", "hint": "No se pudo enviar el código. Intenta de nuevo.", "next": [] }
Verificar Código (OTP)
state: verified). Cada código admite 7 intentos; al agotarlos la verificación se bloquea (locked) hasta rotar el código con POST /v2/otp y code_rotate: true.52.curl -X POST https://api.smsmasivos.com.mx/v2/otp/verify \
-H "Content-Type: application/json" \
-H "apikey: TU_API_KEY" \
-d '{
"phone_number": "5512345678",
"country_code": "52",
"code": "483920"
}'
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.smsmasivos.com.mx/v2/otp/verify');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'apikey: ' . getenv('API_KEY')
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'phone_number' => '5512345678',
'country_code' => '52',
'code' => '483920'
]));
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status . ' ' . $response;
import requests
response = requests.post(
'https://api.smsmasivos.com.mx/v2/otp/verify',
headers={
'Content-Type': 'application/json',
'apikey': 'TU_API_KEY'
},
json={
'phone_number': '5512345678',
'country_code': '52',
'code': '483920'
}
)
if response.status_code == 200:
print('Número verificado')
else:
print(response.status_code, response.json())
const response = await fetch('https://api.smsmasivos.com.mx/v2/otp/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.API_KEY
},
body: JSON.stringify({
phone_number: '5512345678',
country_code: '52',
code: '483920'
})
});
const data = await response.json();
console.log(response.status, data);
const axios = require('axios');
const { status, data } = await axios.post(
'https://api.smsmasivos.com.mx/v2/otp/verify',
{
phone_number: '5512345678',
country_code: '52',
code: '483920'
},
{
headers: {
'Content-Type': 'application/json',
'apikey': process.env.API_KEY
},
validateStatus: () => true
}
);
console.log(status, data);
require 'net/http'
require 'json'
uri = URI('https://api.smsmasivos.com.mx/v2/otp/verify')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path, {
'Content-Type' => 'application/json',
'apikey' => ENV['API_KEY']
})
request.body = {
phone_number: '5512345678',
country_code: '52',
code: '483920'
}.to_json
response = http.request(request)
puts "#{response.code} #{JSON.parse(response.body)}"
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("apikey", "TU_API_KEY");
var payload = new
{
phone_number = "5512345678",
country_code = "52",
code = "483920"
};
var response = await client.PostAsJsonAsync(
"https://api.smsmasivos.com.mx/v2/otp/verify",
payload
);
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{(int)response.StatusCode} {result}");
OkHttpClient client = new OkHttpClient();
String json = "{"
+ "\"phone_number\":\"5512345678\","
+ "\"country_code\":\"52\","
+ "\"code\":\"483920\""
+ "}";
Request request = new Request.Builder()
.url("https://api.smsmasivos.com.mx/v2/otp/verify")
.post(RequestBody.create(json, MediaType.parse("application/json")))
.addHeader("apikey", "TU_API_KEY")
.build();
Response response = client.newCall(request).execute();
System.out.println(response.code() + " " + response.body().string());
{ "success": true, "state": "verified", "action": "verified", "message": "Código verificado correctamente.", "hint": "", "next": [], "request_id": "550e8400-e29b-41d4-a716-446655440000" }
{ "success": false, "error": "otp_missing_param", "message": "Falta un parámetro obligatorio.", "hint": "Envía 'code' (el código que ingresó el usuario).", "next": [] }
{ "success": false, "state": "pending", "error": "otp_incorrect_code", "message": "El código es incorrecto.", "hint": "Código incorrecto. Te quedan 3 intentos.", "next": [ "/v2/otp/verify", "/v2/otp" ], "attempts_remaining": 3 }
{ "success": false, "state": "none", "error": "otp_not_found", "message": "No hay una verificación activa para este número.", "hint": "No hay una verificación activa. Inícia con POST /v2/otp.", "next": [ "/v2/otp" ] }
{ "success": false, "state": "verified", "error": "otp_already_verified", "message": "Este número ya está verificado.", "hint": "Este número ya está verificado.", "next": [] }
{ "success": false, "state": "expired", "error": "otp_code_expired", "message": "El código OTP expiró.", "hint": "El código expiró. Solicita uno nuevo con POST /v2/otp.", "next": [ "/v2/otp" ] }
{ "success": false, "state": "locked", "error": "otp_max_attempts", "message": "Se superó el máximo de intentos de verificación.", "hint": "Se agotaron los intentos. Genera un código nuevo con POST /v2/otp (code_rotate=true).", "next": [ "/v2/otp" ], "attempts_remaining": 0 }
{ "success": false, "error": "otp_internal", "message": "Ocurrió un error inesperado.", "hint": "Intenta de nuevo en unos momentos.", "next": [] }
Consultar Estado de Verificación (OTP)
52.curl -X GET "https://api.smsmasivos.com.mx/v2/otp/status?phone_number=5512345678&country_code=52" \ -H "apikey: TU_API_KEY"
import requests
response = requests.get(
'https://api.smsmasivos.com.mx/v2/otp/status',
headers={'apikey': 'TU_API_KEY'},
params={
'phone_number': '5512345678',
'country_code': '52'
}
)
print(response.json())
const params = new URLSearchParams({
phone_number: '5512345678',
country_code: '52'
});
const response = await fetch(
`https://api.smsmasivos.com.mx/v2/otp/status?${params}`,
{ headers: { apikey: process.env.API_KEY } }
);
const data = await response.json();
console.log(data);
{ "success": true, "state": "pending", "message": "Estado actual del OTP.", "hint": "Hay un código activo. Verifícalo con POST /v2/otp/verify o reenvíalo con POST /v2/otp.", "next": [ "/v2/otp/verify", "/v2/otp" ], "expires_at": "2026-07-21 12:00:00", "attempts_remaining": 5, "resend_available_in": 12, "request_id": "550e8400-e29b-41d4-a716-446655440000" }
{ "success": false, "error": "otp_missing_param", "message": "Falta un parámetro obligatorio.", "hint": "Envía 'phone_number' (hasta 10 dígitos).", "next": [] }
{ "success": false, "error": "otp_internal", "message": "Ocurrió un error inesperado.", "hint": "Intenta de nuevo en unos momentos.", "next": [] }
Invalidar Verificación (OTP)
52.curl -X DELETE https://api.smsmasivos.com.mx/v2/otp \
-H "Content-Type: application/json" \
-H "apikey: TU_API_KEY" \
-d '{
"phone_number": "5512345678",
"country_code": "52"
}'
import requests
response = requests.delete(
'https://api.smsmasivos.com.mx/v2/otp',
headers={
'Content-Type': 'application/json',
'apikey': 'TU_API_KEY'
},
json={
'phone_number': '5512345678',
'country_code': '52'
}
)
print(response.status_code, response.json())
const response = await fetch('https://api.smsmasivos.com.mx/v2/otp', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.API_KEY
},
body: JSON.stringify({
phone_number: '5512345678',
country_code: '52'
})
});
const data = await response.json();
console.log(response.status, data);
{ "success": true, "state": "none", "action": "deleted", "message": "El OTP pendiente fue invalidado.", "hint": "No se envió ningún mensaje.", "next": [ "/v2/otp" ], "request_id": "550e8400-e29b-41d4-a716-446655440000" }
{ "success": false, "error": "otp_missing_param", "message": "Falta un parámetro obligatorio.", "hint": "Envía 'phone_number' (hasta 10 dígitos).", "next": [] }
{ "success": false, "error": "otp_internal", "message": "Ocurrió un error inesperado.", "hint": "No se pudo invalidar el OTP. Intenta de nuevo.", "next": [] }
Objeto de respuesta
Estructura de la respuesta que devuelven estos endpoints.
pending: hay un código activo esperando verificación.pendingQué hizo la API con tu solicitud:
created: primera verificación para el número (o la anterior ya había expirado). HTTP201.resent: reenvió el mismo código vigente. HTTP200.restarted: generó un código nuevo e inició otra ronda (concode_rotate: true, o tras una verificación completada). HTTP200o201.
createdresentrestartedYYYY-MM-DD HH:mm:ss, hora del centro de México).message/company con channel: whatsapp (se ignoran, la plantilla de WhatsApp es fija).code_in_response: true (pruebas/automatización).{ "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, "warning": "channel=whatsapp usa una plantilla oficial fija; se ignoraron message/company.", "code": "483920", "request_id": "550e8400-e29b-41d4-a716-446655440000" }