Receber webhooks do WhatsApp numa Edge Function do Supabase
Uma Edge Function do Supabase pode ser diretamente o destino do webhook do número. Responde ao GET de verificação da Meta com o valor de hub.challenge, insere cada mensagem recebida por POST numa tabela Postgres e responde pela Cloud API. Faça o deploy com a verificação de JWT desativada, porque o Supabase exige por predefinição um JWT válido e a Meta não consegue enviá-lo.
Predefinição do Supabase para Edge Functions, que rejeita qualquer pedido sem JWT válido antes de o seu código correr.
A flag do supabase functions deploy que desativa a verificação de JWT de uma função.
Tempo limite de inatividade do pedido numa Edge Function; sem resposta até lá, devolve 504.
O que tem a Edge Function de fazer?
Duas coisas, em dois métodos.
Em GET, responde à verificação da Meta. Leia hub.mode, hub.verify_token e hub.challenge da query string, compare o token com o que indicou como verify token do destino e devolva o challenge como corpo em texto simples, com estado 200. Devolva 403 quando o token não coincide.
Em POST, recebe eventos. Faça o parse do corpo, extraia as mensagens de entry, changes e value, insira-as numa tabela e devolva 200. Mantenha a resposta rápida: a Meta trata uma resposta lenta ou falhada como uma entrega falhada e volta a tentar, o que significa a mesma mensagem duas vezes.
Defina o URL da função como destino do webhook do número no EasyCoexistence, com o mesmo verify token, e a Meta começa 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");
});Porque devolve a função 401 à Meta?
Porque a verificação de JWT está ativa. O Supabase valida um JWT no cabeçalho Authorization antes de o código da Edge Function correr, e essa é a predefinição de todas as funções. A Meta não envia esse cabeçalho, pelo que a plataforma rejeita tanto o GET de verificação como todas as entregas, e o seu código nunca as vê.
Desative-a apenas nesta função. Faça o deploy com supabase functions deploy whatsapp --no-verify-jwt, ou defina verify_jwt = false na secção da função em supabase/config.toml. A documentação do Supabase descreve exatamente este caso: um endpoint público chamado por um fornecedor externo de webhooks.
Com a verificação da plataforma desativada, a função precisa da sua própria. O cabeçalho de assinatura não está disponível, uma vez que a Meta assina com o segredo da app que detém a subscrição. Coloque um segredo longo e aleatório no URL de destino, como parâmetro de query, e rejeite os pedidos que não o tragam, como faz o exemplo em POST.
Como deve a função guardar e responder?
Guardar primeiro, responder depois. Faça upsert pelo id da mensagem do WhatsApp com uma restrição de unicidade, para que as novas tentativas da Meta caiam na mesma linha em vez de criarem um duplicado. Guarde o phone_number_id de receção em cada linha se mais de um número apontar para a função.
Para tudo o que for lento, como chamar um modelo ou outra API antes de responder, entregue o trabalho a EdgeRuntime.waitUntil e devolva a resposta de imediato. A função continua a correr até essa promise terminar, enquanto a Meta já tem o seu 200. O Supabase deixa de esperar aos 150 segundos de inatividade e devolve 504, pelo que um código que faz todo o trabalho antes de responder é o que mais provavelmente causa novas tentativas.
As respostas seguem para o endpoint de mensagens da Cloud API com o phone number id e o token do número, guardados como segredos da função. Nas 24 horas seguintes à última mensagem do cliente, o texto livre é entregue; fora desse prazo, só um template aprovado.
As mensagens enviadas a partir do telemóvel chegam à função?
Sim. Num número Coexistence, a app WhatsApp Business continua a funcionar e, quando alguém responde a partir dela, a Meta entrega um eco dessa mensagem enviada ao mesmo destino, no campo smb_message_echoes e num array message_echoes, e não em messages.
Amplie o parser para ler ambos e guarde uma coluna direction. A tabela passa a conter a conversa completa, as mensagens do cliente e as respostas da empresa, que é o que a maioria das pessoas quer consultar depois: tempos de resposta, conversas sem resposta, a hora da última mensagem recebida por contacto.
Esse último valor é também o dado de que todos os caminhos de envio precisam, porque decide se a resposta pode ser texto livre ou tem de ser um template. Uma view sobre a tabela pode expô-lo por contacto ao resto da sua aplicação.
Erros comuns
- Fazer o deploy com a verificação de JWT ativa. O Supabase rejeita os pedidos da Meta com 401 antes de o código correr, e nenhuma mensagem chega.
- Devolver o challenge embrulhado em JSON. A Meta precisa do valor em bruto como corpo, ou a verificação falha em silêncio.
- Fazer trabalho lento antes de responder. A Meta repete o que vê como falhas, e a mesma mensagem é processada duas vezes.
- Desativar a verificação de JWT sem qualquer controlo próprio. Quem descobrir o URL pode enviar mensagens falsas para a tabela.
Ligue o número em easycoexistence.com, defina o destino do webhook como o URL da Edge Function com o seu verify token e guarde o phone number id e o token como segredos da função. A partir de US$ 9 por número por mês, descendo para US$ 1 à medida que adiciona números, com os primeiros 3 dias gratuitos.
Perguntas frequentes
Isto funciona no plano gratuito do Supabase?
As Edge Functions correm no plano gratuito, com um limite de 150 segundos de tempo real, contra 400 segundos nos planos pagos. Um código de webhook que responde depressa fica muito aquém de ambos.
Devo usar a service role key na função?
Apenas do lado do servidor, como aqui, lida do ambiente da função. Ignora a row level security, pelo que nunca deve chegar a um browser.
Uma função pode servir vários números?
Sim. Aponte o destino do webhook de cada número para ela e ramifique pelo phone_number_id de cada payload.
O EasyCoexistence vê as mensagens?
Não. A Meta entrega cada webhook diretamente à sua função, e o EasyCoexistence nunca guarda o conteúdo das conversas.
Continue a ler
Pronto para começar?
Configure a Coexistência do WhatsApp em minutos, não em meses. A aplicação continua a funcionar no telemóvel.
Começar período experimental gratuitoVerificado em