Un webhook firmado de Webflow y una llamada REST del lado del servidor pueden compartir una misma aplicación de Next.js en Webflow Cloud, sin que ningún token llegue nunca al navegador.
Si recibes un formulario o cambia un elemento del CMS y otro sistema necesita enterarse, los webhooks ayudan en la parte de envío, donde Webflow llama a tu aplicación. La API REST es la parte de recepción, donde tu aplicación llama a Webflow.
En esta guía, crearemos ambos en una sola aplicación de Next.js en Webflow Cloud. Una ruta POST verifica la firma y la marca de tiempo de Webflow antes de confiar en la carga útil. Una ruta GET lee los elementos de colección publicados con un token que el navegador nunca ve.
¿Qué necesitas para configurar integraciones de webhooks y API REST en Webflow?
Necesitas estos 5 requisitos previos:
- Sitio de Webflow: Cualquier sitio que puedas publicar, además de su ID de sitio desde la configuración del sitio.
- Token del sitio: Créalo en la configuración del sitio, bajo Aplicaciones e integraciones, con los alcances
cms:readysites:write. El primero te permite leer elementos de colección y el segundo te permite registrar el webhook a través de la API. - Next.js 15 y npm: La página bring-your-own-app establece Next.js 15 o superior como requisito mínimo y especifica que Webflow Cloud solo admite el gestor de paquetes npm.
- Node.js 22 o posterior: Las aplicaciones de Webflow Cloud se compilan en Node.js 22 o posterior.
- Una colección para leer: Su ID de colección, obtenido desde la configuración de la colección en el CMS.
El secreto del webhook es la única credencial que aún no tienes. Webflow te lo devuelve cuando registras el webhook en el paso 5.
5 pasos para configurar integraciones de webhooks y API REST en Webflow Cloud
La estructura consiste en dos manejadores de ruta en una sola aplicación de Next.js: una ruta POST que verifica las firmas de los webhooks y una ruta GET que lee elementos de colección con un token del lado del servidor. Un componente de cliente llama a la ruta GET a través de la ruta de montaje de la aplicación.
1. Crea la aplicación Next.js con npm
Prepara el proyecto con el App Router y TypeScript, y usa npm para todas las instalaciones a partir de ahora. Webflow Cloud solo compila con npm, por lo que los pasos con pnpm y yarn no funcionarán allí. Fijar create-next-app@15 mantiene la estructura en una versión que Webflow Cloud ejecuta, y en la versión donde middleware.ts sigue siendo el nombre de archivo estándar.
Ejecuta esto desde el directorio donde guardas tus proyectos:
npx create-next-app@15 webflow-integrations --typescript --app
cd webflow-integrations
npm run dev
Antes del primer commit, confirma que la raíz del proyecto tenga un package-lock.json y no un pnpm-lock.yaml o yarn.lock, y que node -v reporte la versión 22 o superior. Cuando npm run dev imprima la URL local y se cargue la página predeterminada, la estructura estará funcionando.
Deja next.config tranquilo; los documentos de configuración de Webflow dicen que no necesitas añadir un adaptador, una ruta base ni un modo de salida.
2. Almacena las credenciales y los ID como variables de entorno
Coloca todos los valores de los que dependen las rutas en variables de entorno, localmente en .env.local y en la configuración del entorno del proyecto en Webflow Cloud.
La documentación de variables de entorno de Webflow indica: "Tanto las variables de entorno secretas como las no secretas están disponibles para el proceso de compilación de tu aplicación y para la aplicación desplegada en tiempo de ejecución". Los secretos se ocultan en los registros de compilación.
Las cuatro variables y para qué sirve cada una:
- WEBFLOW_SITE_TOKEN: El token del sitio obtenido en los requisitos previos. Márcalo como secreto y nunca le añadas el prefijo
NEXT_PUBLIC_. - WEBFLOW_COLLECTION_ID: La colección que lee la ruta GET.
- WEBFLOW_WEBHOOK_SECRET: El
secretKeyque Webflow devuelve al registrar el webhook en el paso 5. Márcalo como secreto y déjalo vacío hasta ese momento. - NEXT_PUBLIC_BASE_PATH: La ruta de montaje de la aplicación en Webflow Cloud, por ejemplo
/app. Webflow Cloud no crea esta variable, y el código del cliente la necesita porque las peticiones del navegador nunca se reescriben.
Establece NEXT_PUBLIC_BASE_PATH como una cadena vacía localmente y como la ruta de montaje real en Webflow Cloud. Al reiniciar npm run dev se toman los valores locales, y el siguiente despliegue toma los valores de Cloud.
3. Escribe el manejador de rutas del webhook y verifica la firma
Webflow firma los webhooks creados a través de la API con dos cabeceras. x-webflow-timestamp contiene la hora de envío en milisegundos, y x-webflow-signature contiene un HMAC SHA256 hexadecimal de la marca de tiempo, dos puntos y el cuerpo de la solicitud.
El manejador lee el cuerpo sin procesar, rechaza cualquier solicitud de más de cinco minutos de antigüedad, vuelve a calcular la firma con Web Crypto y solo entonces analiza el JSON.
Crea :app/api/webhooks/webflow/route.ts
// app/api/webhooks/webflow/route.ts
import { NextResponse } from 'next/server';
const MAX_AGE_MS = 5 * 60 * 1000;
const encoder = new TextEncoder();
async function hmacHex(secret: string, message: string): Promise<string> {
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
return Array.from(new Uint8Array(signature))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}
export async function POST(request: Request) {
const secret = process.env.WEBFLOW_WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Webhook configuration missing' }, { status: 500 });
}
const rawBody = await request.text();
const signature = request.headers.get('x-webflow-signature') ?? '';
const timestamp = request.headers.get('x-webflow-timestamp') ?? '';
const sentAt = Number(timestamp);
if (!timestamp || !Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) > MAX_AGE_MS) {
return NextResponse.json({ error: 'Stale or missing timestamp' }, { status: 401 });
}
const expected = await hmacHex(secret, `${timestamp}:${rawBody}`);
if (!safeEqual(signature, expected)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
let event: { triggerType?: string; payload?: unknown };
try {
event = JSON.parse(rawBody);
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}
console.log('Webflow webhook accepted', event.triggerType);
return NextResponse.json({ received: true });
}
Los ejemplos de Node de Webflow firman JSON.stringify(req.body), una copia re-serializada del cuerpo analizado. Firmar el texto sin procesar, como hace este manejador, es equivalente byte a byte y evita diferencias en el orden de las claves o el espaciado.
La comparación está escrita a mano porque crypto.subtle.timingSafeEqual existe en el entorno de ejecución de Workers desplegado, pero no bajo next dev local.
Deja export const runtime = 'edge' fuera de este archivo. "Edge" significa dos cosas en Webflow Cloud: la plataforma, Cloudflare Workers, donde ya se ejecuta cada ruta, y el objetivo de tiempo de ejecución de Next.js, que el adaptador OpenNext Cloudflare no admite.
Envía una solicitud sin firmar para confirmar que el manejador la rechaza:
curl -i -X POST http://localhost:3000/api/webhooks/webflow -H 'Content-Type: application/json' -d '{}'
Con un secreto configurado en .env.local, un error 401 con Stale or missing timestamp confirma que el manejador rechaza una solicitud que Webflow no firmó.
4. Llama a la API REST de Webflow desde una ruta del servidor
La ruta GET lee los elementos publicados con el token del sitio, de modo que quien herede el sitio reciba un paquete sin el token. Llama al host de la API de entrega de contenido, api-cdn.webflow.com, que sirve elementos publicados desde una caché, y las respuestas cacheadas no cuentan para el límite de tasa de la API de datos.
Crea :app/api/cms/items/route.ts
// app/api/cms/items/route.ts
import { NextResponse } from 'next/server';
type Item = { id: string; fieldData: Record<string, unknown> };
export async function GET() {
const token = process.env.WEBFLOW_SITE_TOKEN;
const collectionId = process.env.WEBFLOW_COLLECTION_ID;
if (!token || !collectionId) {
return NextResponse.json({ error: 'API configuration missing' }, { status: 500 });
}
const response = await fetch(
`https://api-cdn.webflow.com/v2/collections/${collectionId}/items/live?limit=100`,
{ headers: { Authorization: `Bearer ${token}`, accept: 'application/json' } }
);
if (!response.ok) {
return NextResponse.json(
{ error: 'Webflow API request failed', status: response.status },
{ status: 502 }
);
}
const data = (await response.json()) as { items: Item[] };
return NextResponse.json({
items: data.items.map(({ id, fieldData }) => ({ id, fieldData })),
});
}
La ruta devuelve hasta 100 elementos, el máximo permitido por una llamada de lista; una colección más grande necesita offset paginación. /api/cms/items es pública tal como está escrita, lo cual es adecuado para contenido publicado, y solo lee la colección especificada en el entorno.
A diferencia de un despliegue estándar de Next.js, Webflow Cloud requiere que las peticiones del navegador incluyan la ruta de montaje de la aplicación. La página de "bring-your-own-app" es clara: "Las llamadas fetch del lado del cliente deben incluir manualmente la ruta base para llegar a tus endpoints correctamente". Un componente que hace fetch a /api/cms/items funciona en localhost pero da error 404 tras el despliegue.
app/components/ItemList.tsxprefija la ruta de montaje antes de cada obtención:
// app/components/ItemList.tsx
'use client';
import { useEffect, useState } from 'react';
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
type Item = { id: string; fieldData: { name?: string } };
export function ItemList() {
const [items, setItems] = useState<Item[] | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
fetch(`${basePath}/api/cms/items`)
.then((response) => {
if (!response.ok) throw new Error(String(response.status));
return response.json();
})
.then((data: { items: Item[] }) => setItems(data.items))
.catch(() => setFailed(true));
}, []);
if (failed) return <p>Could not load items.</p>;
if (!items) return <p>Loading...</p>;
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.fieldData.name ?? item.id}</li>
))}
</ul>
);
}
Renderiza en una página y ábrela localmente; deberían aparecer los nombres de los elementos publicados en la colección.ItemList
5. Despliega en Webflow Cloud y registra el webhook a través de la API
Despliega la aplicación en tu proyecto de Webflow Cloud y deja que se ejecute la compilación; luego, registra el webhook a través de la Data API en lugar del panel de control. Los webhooks añadidos en la configuración del sitio llegan sin encabezados de firma, por lo que cada entrega fallaría la verificación del paso 3.
El punto de conexión toma un , como o , y la URL de tu ruta, que es el dominio del sitio, seguido de la ruta de montaje y luego .triggerTypeform_submissioncollection_item_changed/api/webhooks/webflow
Para una aplicación montada en , exporta tu ID de sitio y token, luego regístrala:/app
curl -X POST "https://api.webflow.com/v2/sites/$SITE_ID/webhooks" \
-H "Authorization: Bearer $WEBFLOW_SITE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"triggerType":"form_submission","url":"https://www.example.com/app/api/webhooks/webflow"}'
La respuesta incluye un , que solo se muestra esta vez. Guárdalo como en el entorno de Webflow Cloud y vuelve a desplegar. Una aplicación creada sobre una aplicación OAuth en lugar de un token de sitio firma con el secreto de cliente de la aplicación, así que ese es el valor que debes almacenar ahí.secretKeyWEBFLOW_WEBHOOK_SECRET
Dispara el evento en el sitio (por ejemplo, un envío de formulario de prueba) y luego revisa los registros de la aplicación desplegada en el proyecto de Webflow Cloud. Una línea con el tipo de disparador significa que la firma coincidió y el controlador está activo.Webflow webhook accepted
Desde el mismo despliegue, la página con debería listar la colección a través de la ruta de montaje.ItemList
¿Qué causa que los webhooks de Webflow y las integraciones de la API REST fallen en Webflow Cloud?
La mayoría de los fallos provienen de la configuración de firma; el resto se debe a las reglas de compilación y enrutamiento de Webflow Cloud. Verifica la ruta de firma y la ruta de solicitud del navegador por separado.
Una entrega real de Webflow devuelve 401
Causa: El webhook se creó en el panel de control, por lo que no lleva encabezados de firma y el controlador responde . Cuando los encabezados están presentes y la respuesta es , el secreto almacenado es incorrecto: un webhook de token de sitio firma con su propio , mientras que una aplicación OAuth firma con el secreto de cliente de la aplicación.Stale or missing timestampInvalid signaturesecretKey
Un controlador que firma el cuerpo sin el prefijo o que compara la marca de tiempo en segundos en lugar de milisegundos falla de la misma manera.timestamp:
Solución: Elimina cualquier webhook del panel de control para el mismo disparador y regístralo a través de la API como en el paso 5. Almacena el de esa respuesta, vuelve a desplegar y envía un nuevo evento.secretKey
Solución rápida: Webflow trata cualquier respuesta que no sea 200 como un fallo, reintenta hasta tres veces más con diez minutos de diferencia y desactiva un webhook que sigue fallando hasta que el soporte de Webflow lo reactive.
La aplicación desplegada se rompe después de seguir las instrucciones del tiempo de ejecución Edge en la documentación
Causa: La página de "trae tu propia aplicación" todavía te indica que añadas la directiva de tiempo de ejecución Edge de Next.js a las rutas de la API. La misma página describe la plataforma como una que se despliega "usando el tiempo de ejecución Edge", por lo que un lector que sigue la documentación al pie de la letra añade una directiva que genera una compilación rota. La frase sobre la plataforma se refiere a Cloudflare Workers; la directiva es una característica independiente de Next.js que el adaptador no puede admitir.
Solución: Elimina la línea de cada ruta y vuelve a compilar. Si necesitas lógica que se ejecute antes de una ruta, ponla en . La página de deja claro el límite: "El middleware de tiempo de ejecución de Node.js no es compatible.middleware.ts
Solo el middleware de tiempo de ejecución Edge funciona en el tiempo de ejecución de Workers." No renombres ese archivo a en Next 16, porque se ejecuta en el tiempo de ejecución de Node y no puede ejecutarse aquí.proxy.tsproxy
La verificación de firma falla en next dev pero funciona tras el despliegue
Causa: Alguien reemplazó por de Cloudflare. Existe en el entorno de ejecución de Workers desplegado, pero no en local, por lo que la ruta funciona en producción y falla en el portátil.safeEqualcrypto.subtle.timingSafeEqualnext dev
Solución: Mantén el asistente de JavaScript simple, que hace XOR de cada carácter en lugar de detenerse en la primera discrepancia, y ejecútalo igual en ambos sitios. Prueba cada ruta de firma dos veces: una contra y otra contra la URL desplegada.safeEqualnpm run dev
Una petición fetch del navegador a /api/cms/items devuelve 404 tras el despliegue, pero responde en localhost
Causa: Localmente no hay ruta de montaje, así que un simple se resuelve. En la aplicación desplegada, la ruta reside en la ruta de montaje más , y la ruta simple llega a la página 404 del sitio de Webflow./api/cms/items/api/cms/items
Solución: Comprueba que cada fetch del navegador use el prefijo de ruta de montaje que se muestra en , y que coincida exactamente con la ruta de montaje. La pestaña de red en la página desplegada debería mostrar la ruta de montaje antes de .ItemListNEXT_PUBLIC_BASE_PATH/api
Si sigue dando 404, confirma que la variable se guardó antes de la última compilación, ya que Next.js inserta los valores de en el paquete del cliente durante la compilación.NEXT_PUBLIC_
Qué puedes construir a continuación con webhooks y la API REST de Webflow
Leo del payload verificado y bifurco antes de procesar nada, así que el envío de un formulario y la publicación en el CMS siguen caminos distintos en el receptor. Un evento puede indicar a un índice de búsqueda que reindexe un elemento, y el patrón de ruta GET se extiende a cualquier colección añadiendo paginación .triggerTypecollection_item_changedoffset
Para saber por qué es importante la verificación de firma, consulta la guía de seguridad de webhooks, que cubre las amenazas que bloquea. Para payloads por disparador y la lista completa de eventos, consulta la documentación para desarrolladores de Webflow.
Preguntas frecuentes
¿Puede MD5 desduplicar entregas repetidas de webhooks?
Un manejador puede aplicar hash al cuerpo original para detectar una entrega repetida con , que Cloudflare ofrece en Workers como una extensión no estándar. Úsalo solo para desduplicación. La propia nota de Cloudflare dice: "MD5 se considera un algoritmo débil. No confíes en MD5 para la seguridad", así que no lo uses para el control de acceso.crypto.subtle.digest('MD5', ...)
¿Puede la ruta del webhook escribir un registro de entrega en el disco?
No. Los Workers solo proporcionan a partir de la fecha de compatibilidad 2025-09-01, que es posterior a la fecha en la configuración de ejemplo de Webflow Cloud, y aun así sus archivos no persisten entre peticiones. Registro con en su lugar; su salida aparece en los registros desplegados que comprueba el paso 5.node:fsconsole.log
¿Puedo construir el receptor de webhooks con Astro o Vite en lugar de Next.js?
Sí. Webflow Cloud también es compatible con Astro 5 o superior y Vite 6.1 o superior. Los puertos de verificación de firma no cambian, ya que solo usan Web Crypto, y la regla de ruta de montaje para los fetch del navegador se aplica en cualquier framework.
¿Debe cambiar el secreto del webhook cuando un cliente toma el control del sitio?
En el traspaso, roto el token del sitio y vuelvo a registrar el webhook, lo que genera un nuevo . Pon ambos valores nuevos en el entorno de Webflow Cloud y vuelve a desplegar. Si el secreto almacenado y el del webhook difieren, cada entrega real devolverá un 401 con .secretKeyInvalid signature





