WhatsApp Business API para otp de inicio de sesión en dispositivo nuevo
El escenario envía al usuario una plantilla de autenticación de WhatsApp con un código de verificación de un solo uso cuando un intento de login se marca como proveniente de un dispositivo nuevo.
Descripción del caso de uso
El escenario envía al usuario una plantilla de autenticación de WhatsApp con un código de verificación de un solo uso cuando un intento de login se marca como proveniente de un dispositivo nuevo. El botón copy-code permite pegar el código en tu pantalla de confirmación de acceso.
Ejemplo de plantilla
{{1}} es tu código de verificación. Por tu seguridad, no compartas este código con nadie.
- {{1}}código de verificación de inicio en dispositivo nuevo de un solo uso (dígitos)
- “Copiar código”botón — fijo en la plantilla de Meta

Cuándo usarlo
Usa este escenario cuando un usuario inicia sesión desde un dispositivo o navegador nuevo y debes confirmar que es realmente él antes de abrir una sesión, pero aún no hay chat abierto en WhatsApp — un mensaje de texto libre no está permitido en cold start. Encaja en IAM y apps web o móviles que detectan endpoints no reconocidos, equipos de seguridad de cuenta que bloquean logins no autorizados, e integradores que conectan eventos de huella de dispositivo o motor de riesgo a través de la API 1MSG.
Flujo de trabajo
- Disparador
El usuario inicia sesión desde un dispositivo o navegador nuevo y tu sistema marca el intento.
evento·disparado - Capturar evento
Tu backend genera un código de verificación de un solo uso y obtiene el número de WhatsApp del usuario.
phone:"+…" - Construir y enviar
Se construye un mensaje de plantilla con el código en el cuerpo y el mismo valor en el parámetro del botón copy-code.
POST/sendTemplate - Entregado
El usuario recibe el mensaje de WhatsApp y copia o lee el código.
entregado - Estado registrado
Tu backend valida el código enviado y permite o bloquea la sesión del dispositivo nuevo; los errores de entrega se manejan según las reglas de la plataforma.
status:"read"

Implementación técnica
Requisitos previos
- Clave API de 1MSG · Cómo obtener la clave API
- Cuenta de WhatsApp Business · Cómo conectar WABA
- Plantilla de WhatsApp · Cómo aprobar una plantilla WABA
- Opt-in del cliente · Cómo gestionar el consentimiento
Ejemplos de código
#!/usr/bin/env bash
set -euo pipefail
# === Configuration (replace "___" placeholders) ===
API_BASE_URL="https://api.1msg.io" # production 1MSG API base URL
CHANNEL_ID="___" # channel ID from 1MSG dashboard
API_TOKEN="___" # channel JWT token (Bearer)
TEMPLATE_NAME="___" # approved template name
TEMPLATE_NAMESPACE="___" # template namespace (required — send fails without it)
TEMPLATE_LANGUAGE="___" # template language code, e.g. "en"
# === Test data ===
TEST_PHONE="___" # client phone in international format
TEST_CODE="___" # {{1}} otp code
PHONE_NORM="$(printf '%s' "$TEST_PHONE" | tr -cd '0-9')"
for pair in "CHANNEL_ID=$CHANNEL_ID" "API_TOKEN=$API_TOKEN" \
"TEMPLATE_NAME=$TEMPLATE_NAME" "TEMPLATE_NAMESPACE=$TEMPLATE_NAMESPACE" \
"TEMPLATE_LANGUAGE=$TEMPLATE_LANGUAGE" "TEST_PHONE=$TEST_PHONE" \
"TEST_CODE=$TEST_CODE"; do
val="${pair#*=}"
if [ -z "$val" ] || [ "$val" = "___" ]; then
echo "Missing configuration value: ${pair%%=*}" >&2
exit 1
fi
done
if [ -z "$PHONE_NORM" ]; then
echo "Error: phone number has no digits after normalization" >&2
exit 1
fi
URL="${API_BASE_URL%/}/${CHANNEL_ID}/sendTemplate"
# params carries body and button blocks.
# {{1}} otp code → ${TEST_CODE}
read -r -d '' PAYLOAD <<JSON || true
{
"phone": "${PHONE_NORM}",
"template": "${TEMPLATE_NAME}",
"namespace": "${TEMPLATE_NAMESPACE}",
"language": { "policy": "deterministic", "code": "${TEMPLATE_LANGUAGE}" },
"params": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "${TEST_CODE}" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [ { "type": "text", "text": "${TEST_CODE}" } ]
}
]
}
JSON
RESPONSE="$(curl -s -w '\n%{http_code}' -X POST "$URL" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${API_TOKEN}" \
-d "$PAYLOAD")"
HTTP_CODE="$(printf '%s' "$RESPONSE" | tail -n1)"
BODY="$(printf '%s' "$RESPONSE" | sed '$d')"
case "$BODY" in
*'"sent":true'*) ok=1 ;;
*) ok=0 ;;
esac
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ] && [ "$ok" -eq 1 ]; then
echo "Message sent to client."
echo "API response: $BODY"
else
echo "Send failed. HTTP status: $HTTP_CODE" >&2
echo "$BODY" >&2
exit 1
fi
Respuesta y estado de entrega
HTTP 2xx y JSON "sent": true significan que 1MSG aceptó el mensaje para envío — no que ya llegó al teléfono del cliente. Guarda el campo id (tipo wamid.…) para correlacionar los callbacks de entrega.
{
"sent": true,
"id": "wamid.HBgLMzgwNjM5...",
"message": "Message accepted for delivery"
}sentAceptado para envío — aún no en el teléfono del cliente
idGuárdalo; los callbacks de entrega y
hookInfose basan en él
La entrega llega después, como un callback aparte. Registra un webhook (POST …/webhook) y 1MSG enviará las actualizaciones de estado a tu endpoint HTTPS en un payload hooks[] de nivel superior.
{
"hooks": [
{
"id": "gBGGeSaGViBfAgnlzOSHEwK9O6F",
"type": "message",
"status": "sent",
"timestamp": "1654864094",
"recipient_id": "556123122026"
}
]
}statussent,delivered,read— o un estado de fallo cuando apliqueidCorrelaciona el callback con el
iddevuelto por el envíotimestampSegundos Unix, como cadena
Si prefieres no recibir callbacks, consulta GET {base}/{channel}/hookInfo?messageId=<id> en su lugar. En la práctica la entrega suele completarse en segundos — pero el contrato de la API no lo garantiza, así que nunca bloquees un flujo esperándola.
Errores frecuentes
| Estado | Respuesta | Causa |
|---|---|---|
| 200 | Message was not sent: template is not defined | Falta namespace, template o language en el cuerpo de la petición. |
| 200 | template name (…) does not exist in <language> | La plantilla está aprobada en otro idioma que el solicitado. |
| 200 | Message was not sent: provide chatId, phone, bsuid, or username | Ningún destinatario que el canal pudiera resolver. |
| 403 | access denied | El token es incorrecto, o pertenece a otro canal que el de la URL. |
| 429 | too many requests. please try later | El canal superó su límite de envío. |

