Recibe webhooks de WhatsApp en una Edge Function de Supabase
Una Edge Function de Supabase puede ser directamente el destino del webhook del número. Responde al GET de verificación de Meta con el valor de hub.challenge, inserta cada mensaje recibido por POST en una tabla de Postgres y contesta por la Cloud API. Despliégala con la verificación de JWT desactivada, porque Supabase exige un JWT válido por defecto y Meta no puede enviarlo.
El valor por defecto de Supabase para las Edge Functions: rechaza cualquier petición sin un JWT válido antes de que se ejecute tu handler.
El flag de supabase functions deploy que desactiva la verificación de JWT de una función.
Tiempo máximo de inactividad de una petición a una Edge Function; si no hay respuesta antes, devuelve 504.
¿Qué tiene que hacer la Edge Function?
Dos cosas, según el método.
En GET, responde a la verificación de Meta. Lee hub.mode, hub.verify_token y hub.challenge de la query string, compara el token con el que introdujiste como verify token del destino y devuelve el challenge como cuerpo de texto plano con estado 200. Devuelve 403 si el token no coincide.
En POST, recibe eventos. Procesa el cuerpo, extrae los mensajes de entry, changes y value, insértalos en una tabla y devuelve 200. Mantén la respuesta rápida: Meta trata una respuesta lenta o fallida como una entrega fallida y reintenta, lo que significa el mismo mensaje dos veces.
Configura la URL de la función como destino del webhook del número en EasyCoexistence, con el mismo verify token, y Meta empieza a entregar.
import { createClient } from "npm:@supabase/supabase-js@2";
const db = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
Deno.serve(async (req) => {
const url = new URL(req.url);
if (req.method === "GET") {
const ok = url.searchParams.get("hub.verify_token") === Deno.env.get("VERIFY_TOKEN");
return new Response(ok ? url.searchParams.get("hub.challenge") : "forbidden", { status: ok ? 200 : 403 });
}
if (url.searchParams.get("key") !== Deno.env.get("URL_SECRET")) return new Response("no", { status: 401 });
const body = await req.json();
const rows = (body.entry ?? []).flatMap((e: any) => e.changes ?? [])
.flatMap((c: any) => (c.value?.messages ?? []).map((m: any) => ({
wamid: m.id, sender: m.from, kind: m.type, text: m.text?.body ?? null,
phone_number_id: c.value.metadata?.phone_number_id, raw: m,
})));
if (rows.length) await db.from("whatsapp_messages").upsert(rows, { onConflict: "wamid" });
return new Response("ok");
});¿Por qué la función devuelve 401 a Meta?
Porque la verificación de JWT está activada. Supabase valida un JWT en la cabecera Authorization antes de que se ejecute el handler de una Edge Function, y ese es el valor por defecto de todas las funciones. Meta no envía esa cabecera, así que la plataforma rechaza tanto el GET de verificación como cada entrega, y tu código nunca llega a verlos.
Desactívala solo para esta función. Despliega con supabase functions deploy whatsapp --no-verify-jwt, o define verify_jwt = false en la sección de la función en supabase/config.toml. La documentación de Supabase describe exactamente este caso: un endpoint público al que llama un proveedor externo de webhooks.
Con la comprobación de la plataforma desactivada, la función necesita la suya. La cabecera de firma no está disponible para ti, ya que Meta firma con el secreto de la app que es dueña de la suscripción. Pon un secreto largo y aleatorio en la URL del destino como parámetro de query y rechaza las peticiones que no lo traigan, como hace el ejemplo en POST.
¿Cómo debe guardar y responder la función?
Primero guarda, después responde. Haz un upsert por el id del mensaje de WhatsApp con una restricción única, para que los reintentos de Meta caigan en la misma fila en lugar de crear un duplicado. Guarda el phone_number_id receptor en cada fila si más de un número apunta a la función.
Para cualquier tarea lenta, como llamar a un modelo u otra API antes de responder, pasa el trabajo a EdgeRuntime.waitUntil y devuelve la respuesta de inmediato. La función sigue ejecutándose hasta que esa promesa termina, mientras Meta ya tiene su 200. Supabase deja de esperar a los 150 segundos de inactividad y devuelve 504, así que un handler que hace todo su trabajo antes de responder es el que más probablemente provoque reintentos.
Las respuestas van al endpoint de mensajes de la Cloud API con el phone number id y el token del número, guardados como secretos de la función. Dentro de las 24 horas desde el último mensaje del cliente se entrega texto libre; fuera de ellas, solo una plantilla aprobada.
¿Los mensajes enviados desde el teléfono llegan a la función?
Sí. En un número con Coexistencia la app de WhatsApp Business sigue funcionando, y cuando alguien responde desde ella Meta entrega un eco de ese mensaje saliente al mismo destino, bajo el campo smb_message_echoes y un array message_echoes en lugar de messages.
Amplía el parser para leer ambos y guarda una columna direction. La tabla contiene entonces la conversación completa, las líneas del cliente y las respuestas del negocio, que es lo que la mayoría quiere consultar después: tiempos de respuesta, hilos sin contestar, la hora del último mensaje entrante por contacto.
Ese último valor es además el dato que necesita cualquier vía de envío, porque decide si una respuesta puede ser texto libre o tiene que ser una plantilla. Una vista sobre la tabla puede exponerlo por contacto para el resto de tu app.
Errores comunes
- Desplegar con la verificación de JWT activada. Supabase rechaza las peticiones de Meta con 401 antes de que se ejecute el handler, y no llega ningún mensaje.
- Devolver el challenge envuelto en JSON. Meta necesita el valor sin formato como cuerpo, o la verificación falla en silencio.
- Hacer trabajo lento antes de responder. Meta reintenta lo que ve como fallos, y el mismo mensaje se procesa dos veces.
- Desactivar la verificación de JWT sin ninguna comprobación propia. Cualquiera que encuentre la URL puede publicar mensajes falsos en la tabla.
Conecta el número en easycoexistence.com, configura su destino de webhook con la URL de la Edge Function y tu verify token, y guarda el phone number id y el token como secretos de la función. Desde US$ 9 por número al mes, bajando a US$ 1 a medida que agregas números, con los primeros 3 días gratis.
Preguntas frecuentes
¿Funciona en el plan gratuito de Supabase?
Las Edge Functions se ejecutan en el plan gratuito, con un límite de 150 segundos de tiempo real frente a 400 segundos en los planes de pago. Un handler de webhook que responde rápido queda muy por debajo de ambos.
¿Debo usar la service role key en la función?
Solo en el servidor, como aquí, leída del entorno de la función. Salta la seguridad a nivel de fila, así que nunca debe llegar a un navegador.
¿Puede una función atender varios números?
Sí. Apunta el destino del webhook de cada número a ella y ramifica según el phone_number_id de cada payload.
¿EasyCoexistence ve los mensajes?
No. Meta entrega cada webhook directamente a tu función, y EasyCoexistence nunca almacena el contenido de las conversaciones.
Sigue leyendo
¿Listo para empezar?
Configura WhatsApp Coexistence en minutos, no en meses. La app sigue funcionando en el teléfono.
Empezar prueba gratisVerificado el