Una única ruta POST en Webflow Cloud puede verificar cada número de teléfono contra una API de búsqueda antes de permitir que el envío continúe, para que tus vendedores dejen de marcar números con errores.
Un pattern atributo en un tel input detecta si falta un dígito. Pero no puede decirte si el número es uno que una operadora realmente podría asignar. Los equipos de ventas aprenden esto tras cada llamada fallida, una vez que el cliente potencial ya ha sido calificado y asignado.
Una API de búsqueda cierra esa brecha desde tu propio endpoint. En esta guía, crearemos una pequeña aplicación de Next.js en Webflow Cloud que expone una ruta POST. La ruta verifica el formato del número y consulta a un proveedor de validación telefónica usando una clave que nunca llega al navegador.
¿Qué necesitas para añadir validación telefónica basada en API a un formulario en Webflow?
Necesitas estos seis elementos antes de empezar:
- Formulario de Webflow: Un sitio con un formulario que incluya un campo de entrada de teléfono, publicado en una URL de prueba o listo para estarlo.
- Aplicación de Next.js en Webflow Cloud: Un proyecto conectado que cumpla con los requisitos de "bring-your-own-app", incluyendo Next.js 15 o superior, para su despliegue en Webflow Cloud.
- npm: La misma página lo indica claramente: "Actualmente, Webflow Cloud solo admite el gestor de paquetes npm". Los pasos realizados con pnpm o yarn no funcionarán aquí.
- Cuenta de proveedor: Un proveedor de validación telefónica con un endpoint de búsqueda vía HTTPS, además de su clave API y la página de referencia para dicho endpoint.
- Permisos: Acceso para configurar variables de entorno en el proyecto de Webflow Cloud y para publicar el sitio de Webflow, ya que la compilación afecta a ambos.
- Orígenes del formulario: Los orígenes exactos de prueba y producción desde los que se sirve tu formulario, esquema incluido, porque la ruta los compara como cadenas de texto.
Con estas piezas listas, la compilación puede conectar el formulario a la ruta de búsqueda protegida. Los ejemplos utilizan /tools como ruta de montaje.
5 pasos para añadir validación telefónica basada en API a formularios de Webflow en Webflow Cloud
Cinco pasos conectan el campo de teléfono a un Route Handler POST, protegen la clave del proveedor, montan el endpoint correctamente, interceptan los envíos y verifican el flujo completo de validación.
La construcción avanza desde los identificadores de campo a través de la configuración de la ruta y el entorno hasta el script de la página y las pruebas de ida y vuelta.
1. Establece identificadores para el campo de teléfono y el formulario
Asegúrate de que el input de teléfono tenga el nombre phone y el elemento del formulario tenga el ID lead-form, porque el script hace referencia a ambos mediante esas cadenas exactas. Configura el input como un campo de teléfono para que los teclados móviles compatibles muestren un teclado numérico.
El número que llega a la ruta es lo que el visitante haya escrito, así que establece el marcador de posición con un ejemplo internacional como +14155550123. La ruta elimina espacios, puntos, paréntesis y guiones, pero exige un signo más al principio.
Prefiero pedir el signo más que adivinar un país, porque adivinar un país por defecto es la razón por la que un número de teléfono válido del Reino Unido se rechaza como un número de EE. UU. mal formado. Si tu audiencia es solo nacional, una pequeña pista de prefijo en la etiqueta no cuesta nada y te ahorra un ticket de soporte.
Publica en una URL de prueba e inspecciona la entrada en el navegador. Deberías ver name="phone" en la entrada y id="lead-form" en el formulario; si falta alguno, el script de interceptación se cerrará antes de aplicar la validación del teléfono.
2. Crea el controlador de ruta de validación en tu aplicación Next.js
Añade app/api/validate-phone/route.ts al proyecto de Next.js conectado a tu proyecto de Webflow Cloud como un controlador de ruta simple, que se ejecuta en Cloudflare Workers tal como está escrito aquí.
La ruta realiza cuatro acciones en un orden fijo: rechaza las solicitudes cuyo encabezado Origin no coincida con el origen permitido, extrae el número del cuerpo JSON y elimina los caracteres de formato, lo prueba con el formato internacional E.164 con un signo más y el código de país, y solo entonces realiza una llamada al proveedor. La comprobación de origen evita que los navegadores de otros sitios utilicen tu ruta.
Los clientes que no son navegadores pueden suplantar el encabezado Origin, así que utiliza un límite de gasto del proveedor o un límite de tasa en la ruta para protegerte contra abusos. La prueba E.164 descarta errores tipográficos obvios antes de que cuesten una consulta.
Pega esto en app/api/validate-phone/route.ts:
// app/api/validate-phone/route.ts
import { NextResponse } from 'next/server';
const E164 = /^\+[1-9]\d{1,14}$/;
type Verdict = { valid: boolean; reason?: string };
function reply(body: Verdict, status = 200) {
return NextResponse.json(body, { status });
}
// PROVIDER EDIT 1: map your provider's response to a verdict.
// Return true or false only when the provider actually answered. Return null when the verdict
// field is missing, so an auth or quota error sent with HTTP 200 fails open instead of closed.
function isValid(data: unknown): boolean | null {
const valid = (data as { valid?: unknown } | null)?.valid;
return typeof valid === 'boolean' ? valid : null;
}
export async function POST(request: Request) {
const origin = request.headers.get('origin');
if (!process.env.ALLOWED_ORIGIN || origin !== process.env.ALLOWED_ORIGIN) {
return reply({ valid: false, reason: 'forbidden' }, 403);
}
let phone = '';
try {
const body = (await request.json()) as { phone?: string };
phone = String(body.phone ?? '').replace(/[\s().-]/g, '');
} catch {
return reply({ valid: false, reason: 'bad_request' }, 400);
}
if (!E164.test(phone)) {
return reply({ valid: false, reason: 'format' });
}
const apiUrl = process.env.PHONE_VALIDATION_API_URL;
const apiKey = process.env.PHONE_VALIDATION_API_KEY;
if (!apiUrl || !apiKey) {
return reply({ valid: false, reason: 'not_configured' }, 500);
}
try {
const url = new URL(apiUrl);
// PROVIDER EDIT 2: the query parameter name from your provider's reference.
url.searchParams.set('number', phone);
const upstream = await fetch(url, {
// PROVIDER EDIT 3: the auth scheme from your provider's reference.
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(4000), // a hung provider falls back instead of stalling the form
});
if (!upstream.ok) {
return reply({ valid: false, reason: 'upstream' }, 502);
}
const verdict = isValid(await upstream.json());
if (verdict === null) {
return reply({ valid: false, reason: 'upstream' }, 502);
}
return verdict ? reply({ valid: true }) : reply({ valid: false, reason: 'invalid' });
} catch {
return reply({ valid: false, reason: 'upstream' }, 502);
}
}
Los tres puntos PROVIDER EDIT son donde los proveedores reales difieren, y algunos difieren en la forma, no solo en los nombres. Twilio Lookup coloca el número en la ruta de la URL y utiliza autenticación básica HTTP; Veriphone llama al veredicto phone_valid; Abstract lo anida bajo phone_validation.is_valid.
Por qué preferir encabezados para las claves de API de los proveedores
Algunos proveedores solo aceptan la clave como un parámetro de consulta, donde termina en los registros de acceso, así que prefiere uno que la acepte en un encabezado. Y algunos devuelven un HTTP 200 ante una clave incorrecta o una cuota agotada, por lo que isValid devuelve null en lugar de false cuando falta el campo de veredicto: ese caso se abre como una interrupción en lugar de decirle a cada visitante que su número es incorrecto.
Ahora tienes una ruta que responde { valid: true } o { valid: false, reason }. Dos razones, format y invalid, describen el número en sí y son las que bloquea el navegador.
Las otras cuatro (forbidden, bad_request, not_configured, upstream) describen la configuración o la disponibilidad, y el script de la página permite que esos envíos pasen para evitar perder un cliente potencial durante una interrupción del proveedor.
Prueba localmente con ALLOWED_ORIGIN=http://localhost:3000 en .env.local, ejecuta npm run dev, y haz un POST a {"phone":"hello"} con curl incluyendo un encabezado Origin: http://localhost:3000 ; la respuesta debería ser {"valid":false,"reason":"format"}.
3. Configura las variables de entorno en Webflow Cloud
Los valores se establecen por entorno, por lo que los formularios de prueba y producción servidos desde dominios diferentes necesitan su propio valor ALLOWED_ORIGIN . Añade tres variables al entorno de Webflow Cloud en el que se despliega tu aplicación y marca la clave de API como secreta.
Webflow Cloud expone tanto las variables secretas como las no secretas a la compilación y a la aplicación desplegada en tiempo de ejecución, y redacta los secretos de los registros de compilación, por lo que la clave del proveedor no aparece allí.
Establece estos valores en cada entorno que sirva el formulario:
| Variable | Valor | Secreto |
|---|---|---|
PHONE_VALIDATION_API_URL |
La URL del punto final de búsqueda de la referencia de tu proveedor, sin parámetros de consulta | No |
PHONE_VALIDATION_API_KEY |
La clave de API emitida por tu proveedor | Sí |
ALLOWED_ORIGIN |
El origen desde el que se sirve el formulario, por ejemplo https://www.example.com |
No |
| Variable → Valor → Secreto |
|---|
PHONE_VALIDATION_API_URL |
| La URL del punto final de búsqueda de la referencia de tu proveedor, sin parámetros de consulta |
| No |
PHONE_VALIDATION_API_KEY |
| La clave de API emitida por tu proveedor |
| Sí |
ALLOWED_ORIGIN |
El origen desde el que se sirve el formulario, por ejemplo https://www.example.com |
| No |
El valor ALLOWED_ORIGIN debe coincidir exactamente con el encabezado Origin del navegador y contener solo el esquema y el host. Incluye https://, y no añadas una barra diagonal al final ni ninguna ruta. Los navegadores envían el encabezado Origin en ese formato exacto, y la ruta lo compara con igualdad estricta. Una vez guardadas las variables, vuelve a desplegar: un valor cambiado llega a la aplicación solo después de un despliegue.
Cuando finalice el despliegue, solicita https://your-staging-domain/<mount path>/api/validate-phone con un POST de curl básico y sin encabezado Origin. Un 403 con {"valid":false,"reason":"forbidden"} es el resultado que buscas; demuestra que la ruta está activa en la ruta de montaje y que la puerta de origen funciona antes de que cualquier navegador la toque.
4. Configura el script de interceptación para la página publicada
Registrar el detector de envío en la fase de captura en hace que se ejecute antes que los detectores vinculados al formulario en fases posteriores del evento. En la configuración de entrega de página que utilizas, carga el script cerca del final del cuerpo del documento y establece en la ruta donde está montada tu aplicación.documentMOUNT_PATH
Debido a que la llamada al proveedor es asíncrona, el script siempre detiene el primer envío, consulta la ruta y luego llama a para disparar un segundo evento de envío. Una bandera en el formulario le indica al detector que se haga a un lado en esa segunda pasada para que el flujo de envío existente pueda continuar.requestSubmit()data-phone-checked
Edita , luego pega:MOUNT_PATH
<script>
(function () {
var MOUNT_PATH = '/tools'; // the path your Webflow Cloud app is mounted on
var form = document.getElementById('lead-form');
var input = form ? form.querySelector('input[name="phone"]') : null;
if (!form || !input) return;
var error = document.createElement('div');
error.setAttribute('role', 'alert');
error.style.display = 'none';
input.insertAdjacentElement('afterend', error);
function showError(message) {
error.textContent = message;
error.style.display = 'block';
}
function clearError() {
error.textContent = '';
error.style.display = 'none';
}
function passThrough(reason) {
if (reason) console.warn('Phone check skipped:', reason);
form.dataset.phoneChecked = 'true';
form.requestSubmit();
}
document.addEventListener('submit', function (event) {
if (event.target !== form) return;
if (form.dataset.phoneChecked === 'true') {
form.dataset.phoneChecked = '';
return; // second pass: allow other submission handling to continue
}
event.preventDefault();
event.stopImmediatePropagation();
clearError();
fetch(MOUNT_PATH + '/api/validate-phone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: input.value })
})
.then(function (response) { return response.json(); })
.then(function (data) {
if (data.reason === 'format') {
showError('Enter the number in international format, like +14155550123.');
return;
}
if (data.reason === 'invalid') {
showError('That number does not look valid. Check it and try again.');
return;
}
passThrough(data.reason);
})
.catch(function () {
passThrough('network');
});
}, true);
})();
</script>
La página ahora contiene un detector que bloquea exactamente dos veredictos y deja pasar todo lo demás con una advertencia en la consola. Dale estilo al inyectado mediante una clase si lo prefieres, o asígnale el mismo aspecto que a los otros estados de error del formulario; el atributo hace que los lectores de pantalla anuncien el mensaje sin trabajo adicional. Publica el sitio, escribe en el campo de teléfono y envía.divrole="alert"hello
Deberías ver aparecer el mensaje de formato debajo del campo de entrada y el envío no debería completarse; escribe un número real en formato E.164 y el flujo de envío configurado debería continuar.
5. Prueba el ciclo completo en el sitio publicado
El cuerpo de respuesta de la ruta identifica los fallos con mayor precisión que el estado visible del formulario. Primero hago esto en un entorno de prueba y luego repito el caso válido una vez en producción, ya que lo único que cambia entre ambos es y esa es la variable con más probabilidades de estar mal.ALLOWED_ORIGIN,
Prueba estos cuatro casos:
- Entrada mal formada: Envía y espera un 200 con , el mensaje en línea y que el script no dispare un segundo evento de envío.
555-1234reason: "format" - Bien formado pero inválido: Envía un número con prefijo de signo más de la longitud correcta que ningún operador pueda asignar, como uno con un código de área no asignado, y espera del proveedor, que aparecerá como el segundo mensaje.
reason: "invalid" - Número válido: Envía un número al que puedas contestar, espera , y luego confirma que el envío llega a donde sea que aterricen tus formularios.
{"valid":true} - Proveedor no disponible: Borra temporalmente la clave de API en el entorno de prueba, actualiza el despliegue, envía un número válido y espera una advertencia en la consola además de un envío que procede.
Estas comprobaciones cubren ahora números mal formados, inválidos, válidos y temporalmente inverificables a lo largo de todo el flujo de envío. Después de la cuarta comprobación, restaura la clave y vuelve a desplegar, luego ejecuta de nuevo el caso válido para confirmar que la ruta está llamando al proveedor de nuevo.
Cada solicitud de validación también cuenta para el límite de uso mensual de Webflow Cloud del sitio, listado en la página de precios de Webflow, por lo que un formulario que recibe miles de envíos al día merece que revises ese límite antes del lanzamiento. Antes de lanzar, establece también un límite de gasto para el proveedor o aplica un límite de tasa en la ruta.
El filtro bloquea errores de visitantes que ejecutan JavaScript. Los visitantes con JavaScript desactivado y los bots que publican directamente en el punto final del formulario se lo saltan, por lo que el filtro no ofrece una garantía absoluta del lado del servidor.
Cuando los cuatro casos se comportan como se indica, el formulario detecta errores tipográficos y números imposibles de visitantes reales, y permite que el flujo de envío existente continúe en lugar de bloquear el formulario cuando el proveedor tiene un mal día.
¿Qué causa que la validación telefónica por API falle en Webflow Cloud?
Un 404 apunta a una ruta de montaje faltante, mientras que un 403 en un solo entorno apunta a una discrepancia de origen. Otros síntomas comunes incluyen una compilación rota por la directiva de tiempo de ejecución en el borde y envíos que pasan sin verificar porque el script nunca interceptó.
El registro de despliegue y la respuesta de la ruta separan esos fallos en un despliegue fallido, un 404 en la ruta, un 403 en un solo entorno o números basura llegando a tus envíos.
La compilación de Webflow Cloud falla en una ruta que funciona localmente
Causa: Una ruta que contiene export const runtime = 'edge' falla durante la compilación de Webflow Cloud. Webflow Cloud despliega Next.js a través del adaptador OpenNext Cloudflare, y dicho adaptador no es compatible con el destino de tiempo de ejecución edge de Next.js.
Esta línea suele añadirse porque la página de integración de aplicaciones propias en la documentación para desarrolladores de Webflow también indica añadir la directiva de tiempo de ejecución edge a las rutas de la API.
El término "edge" cumple dos funciones en esa página. Webflow Cloud se ejecuta en Cloudflare Workers, que es una plataforma edge, y la página lo describe así: "Webflow Cloud despliega tu aplicación usando el tiempo de ejecución Edge". Esto permite un "alojamiento rápido y distribuido globalmente".
El destino de tiempo de ejecución edge de Next.js es algo distinto: un modo de compilación por ruta que OpenNext no puede procesar. Un desarrollador que lee la frase sobre la plataforma y luego sigue la instrucción de la directiva está haciendo lo que dicen los documentos y enviando una compilación defectuosa.
Solución: Elimina la línea de route.ts y de cualquier otro archivo donde se haya copiado, luego vuelve a desplegar. Si la compilación sigue fallando, busca runtime = 'edge' en todo el proyecto y elimina cualquier copia.
El navegador recibe un error 404 de /api/validate-phone
Causa: Una solicitud enviada a la raíz del sitio produce este 404. Una aplicación de Webflow Cloud se monta en una ruta de tu dominio, y una ruta definida en app/api/validate-phone/route.ts solo se sirve en <mount path>/api/validate-phone.
El script en la página de Webflow no tiene forma de conocer esa ruta por sí mismo, por eso MOUNT_PATH es un literal que debes editar. El mismo 404 aparece dentro de la aplicación cuando un componente cliente intenta obtener su propia ruta con una URL relativa a la raíz: Webflow Cloud inyecta la ruta base durante la compilación, y las llamadas fetch del lado del cliente no se reescriben para incluirla.
La página de integración de aplicaciones propias lo indica claramente: "Las llamadas fetch del lado del cliente deben incluir manualmente la ruta base para acceder correctamente a tus endpoints".
Solución: En la página de Webflow, configura MOUNT_PATH con la ruta donde está montada la aplicación, con una barra inclinada al principio y sin barra al final, y vuelve a publicar. Dentro de la aplicación, usa NEXT_PUBLIC_BASE_PATH en el entorno como origen de la ruta de montaje y añade el prefijo a las llamadas fetch del cliente.
Confirma abriendo la URL completa de la ruta en una pestaña del navegador: un GET normalmente devolverá un 405 porque el controlador solo exporta POST; cualquier respuesta distinta a 404 indica que la ruta se resuelve correctamente.
La ruta devuelve un 403 en un entorno pero funciona en otro
Causa: Un error 403 aislado en un entorno suele significar que su valor ALLOWED_ORIGIN difiere del formato de esquema y host requerido por la comprobación de igualdad estricta de la ruta.
Revisa primero el entorno afectado. Las solicitudes sin encabezado Origin, provenientes de curl o scripts del lado del servidor, se rechazan a propósito. Mantén un límite de gasto del proveedor o un límite de tasa de rutas, ya que los clientes que no son navegadores pueden suplantar el encabezado Origin.
Solución: Corrige el valor del entorno afectado para que contenga solo el esquema y el host, incluyendo https:// y sin barra inclinada ni ruta al final. Vuelve a desplegar y lee el cuerpo de la respuesta. {"valid":false,"reason":"forbidden"} confirma que se activó el filtro de origen en lugar de un error de formato.
Para el desarrollo local, configura ALLOWED_ORIGIN=http://localhost:3000 en .env.local, ejecuta npm run dev, y envía a curl un encabezado Origin: http://localhost:3000.
Si un formulario se sirve desde varios dominios, guárdalos separados por comas, divide la variable en la ruta y comprueba includes() en lugar de usar una igualdad estricta.
Todos los envíos se procesan, incluso los números obviamente inválidos
Causa: Es posible que el script no haya detectado el envío o que haya permitido deliberadamente que el envío continuara. Un oyente añadido con form.addEventListener('submit', ...) en la fase de propagación podría no ejecutarse lo suficientemente pronto como para detener otros oyentes de envío.
El listener de la fase de captura en document existe para ejecutarse antes que los listeners vinculados al formulario en fases de eventos posteriores, por lo que si alguien lo simplificó a un listener de formulario común, es posible que la comprobación se omita.
La otra ruta es intencionada: todos los motivos, excepto format y invalid, permiten que el envío continúe. Un PHONE_VALIDATION_API_KEY faltante en ese entorno devuelve not_configured, un error del proveedor devuelve upstream, y ambos se tratan como "no perder el lead".
Solución: Abre la consola del navegador y envía un número falso. Si no hay un POST a la ruta, significa que el listener no está interceptando: restaura document.addEventListener('submit', handler, true) y confirma que el ID del formulario y el nombre del campo coincidan con lead-form y phone.
Un POST seguido de Phone check skipped: not_configured significa que las variables no están configuradas en el entorno al que accede la página. Phone check skipped: upstream significa que el proveedor rechazó la llamada; compara el nombre del parámetro y el encabezado de autenticación con la referencia de tu proveedor, ya que esas son las dos líneas que varían entre servicios.
Qué puedes construir después con la validación de teléfonos y Webflow
Una vez que confirmes que un número es real y asignable antes de que llegue a cualquier bandeja de entrada, puedes extender el mismo formato al resto del registro del lead y luego enviar el lead verificado a tu CRM de la misma forma que lo haces en formularios de Webflow a HubSpot.
La API REST de Webflow y una colección CMS proporcionan los bloques de construcción para conectar la misma aplicación con contenido de leads estructurado. Para los entornos y las API de CMS, la documentación para desarrolladores de Webflow cubre el siguiente nivel.
Preguntas frecuentes
¿Puede un mismo script validar campos de teléfono en varios formularios de la misma página?
Sí. Asigna a cada formulario un ID distinto y agrúpalos en una matriz. Para cada formulario, busca su propio input[name="phone"], elemento de error y estado de envío, luego compara event.target con ese formulario. Mantén un indicador data-phone-checked independiente por formulario para que un segundo envío exitoso en uno no pueda saltarse la validación en otro.
¿Puedo usar Astro o Vite en lugar de Next.js para la ruta de validación?
Astro, sí; Vite, no como está documentado. Una aplicación Astro 6 o 7 en Webflow Cloud puede servir un endpoint que mantenga el mismo contrato de solicitud y respuesta, leyendo la clave del proveedor desde locals.runtime.env en lugar de process.env. Webflow documenta las aplicaciones Vite como compatibles, pero no describe endpoints de servidor para ellas. Independientemente del framework que lo sirva, el endpoint debe leer el encabezado Origin, normalizar y probar el número de teléfono, llamar al proveedor con una variable de entorno y devolver el formato de veredicto existente. El script de la página de Webflow puede permanecer sin cambios mediante el despliegue en Webflow Cloud de cualquiera de los dos frameworks.
¿Puedo mostrar mensajes diferentes para otros veredictos del proveedor?
Sí. Amplía los motivos de Verdict en la ruta al mapear la respuesta del proveedor, luego añade ramas coincidentes antes de passThrough(data.reason) en el script de la página. Mantén los veredictos específicos del número separados de los fallos de configuración y disponibilidad para que puedas bloquear problemas de entrada accionables mientras permites que los envíos continúen cuando el servicio de validación no pueda proporcionar una respuesta fiable.
¿Pueden los clics repetidos iniciar más de una solicitud de validación de teléfono?
Sí. El script detiene cada evento de envío, pero no desactiva el control de envío ni rastrea una búsqueda ya en curso. Añade un indicador de "en curso" por formulario antes de fetch(), ignora envíos adicionales mientras ese indicador esté activo y límpialo después de cualquiera de las rutas de respuesta. Mantén data-phone-checked reservado para el segundo envío exitoso.





