EntrarComeçar teste grátis
Meta Tech Provider

Receba webhooks do WhatsApp em uma Edge Function do Supabase

Uma Edge Function do Supabase pode ser diretamente o destino do webhook do número. Ela responde ao GET de verificação da Meta com o valor de hub.challenge, insere cada mensagem recebida por POST em uma tabela do Postgres e responde pela Cloud API. Faça o deploy com a verificação de JWT desativada, porque o Supabase exige um JWT válido por padrão e a Meta não consegue enviar um.

verify_jwt = true

Padrão do Supabase para Edge Functions: rejeita qualquer requisição sem um JWT válido antes de o seu handler rodar.

--no-verify-jwt

A flag do supabase functions deploy que desativa a verificação de JWT de uma função.

150 s

Tempo limite de inatividade de uma Edge Function; sem resposta até lá, ela retorna 504.

O que a Edge Function precisa fazer?

Duas coisas, em dois métodos.

No GET, ela responde à verificação da Meta. Leia hub.mode, hub.verify_token e hub.challenge na query string, compare o token com o que você informou como verify token do destino e devolva o challenge como corpo em texto simples, com status 200. Retorne 403 quando o token não bater.

No POST, ela recebe os eventos. Faça o parse do corpo, extraia as mensagens de entry, changes e value, insira-as em uma tabela e retorne 200. Mantenha a resposta rápida: a Meta trata uma resposta lenta ou com falha como entrega que falhou e repete o envio, o que significa a mesma mensagem duas vezes.

Defina a 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.

supabase/functions/whatsapp/index.ts
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 que a função retorna 401 para a Meta?

Porque a verificação de JWT está ativada. O Supabase valida um JWT no header Authorization antes de o handler de uma Edge Function rodar, e esse é o padrão de toda função. A Meta não envia esse header, então a plataforma rejeita tanto o GET de verificação quanto cada entrega, e o seu código nunca as vê.

Desative a verificação só para esta função. Faça o deploy com supabase functions deploy whatsapp --no-verify-jwt ou defina verify_jwt = false na seção da função em supabase/config.toml. A documentação do Supabase descreve exatamente este caso: um endpoint público chamado por um provedor de webhook externo.

Com a checagem da plataforma desligada, a função precisa ter a sua própria. O header de assinatura não está disponível para você, já que a Meta assina com o app secret do app dono da assinatura. Coloque um segredo longo e aleatório na URL do destino como parâmetro de query e rejeite requisições sem ele, como o exemplo faz no POST.

Como a função deve armazenar e responder?

Armazene primeiro, responda depois. Faça upsert pelo id da mensagem do WhatsApp, com uma constraint de unicidade, para que as repetições da Meta caiam na mesma linha em vez de criar duplicata. Guarde o phone_number_id que recebeu a mensagem em cada linha se mais de um número apontar para a função.

Para qualquer trabalho lento, como chamar um modelo ou outra API antes de responder, entregue o trabalho ao EdgeRuntime.waitUntil e devolva a resposta de imediato. A função continua rodando até essa promise terminar, enquanto a Meta já recebeu o 200. O Supabase para de esperar após 150 segundos de inatividade e retorna 504, então um handler que faz todo o trabalho antes de responder é o que mais provoca repetições.

As respostas vão 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. Dentro de 24 horas da última mensagem do cliente, texto livre é entregue; fora dessa janela, só um template aprovado.

As mensagens enviadas pelo celular chegam à função?

Sim. Em um número com Coexistência, o app do WhatsApp Business continua funcionando e, quando alguém responde por ele, a Meta entrega um eco dessa mensagem enviada ao mesmo destino, no campo smb_message_echoes e em um array message_echoes, e não em messages.

Estenda o parser para ler os dois e guardar uma coluna de direção. A tabela passa a conter a conversa completa, as falas do cliente e as respostas da empresa, que é o que a maioria quer consultar depois: tempos de resposta, conversas sem resposta, o horário da última mensagem recebida por contato.

Esse último valor também é o dado de que todo caminho de envio precisa, pois ele decide se a resposta pode ser texto livre ou tem de ser um template. Uma view sobre a tabela pode expô-lo por contato para o resto do seu app.

Erros comuns

  • Fazer o deploy com a verificação de JWT ativada. O Supabase rejeita as requisições da Meta com 401 antes de o handler rodar, e nenhuma mensagem chega.
  • Devolver o challenge dentro de JSON. A Meta precisa do valor puro como corpo, ou a verificação falha em silêncio.
  • Fazer trabalho lento antes de responder. A Meta repete o que vê como falha, e a mesma mensagem é processada duas vezes.
  • Desativar a verificação de JWT sem nenhuma checagem própria. Qualquer pessoa que descobrir a URL pode inserir mensagens falsas na tabela.
Fazendo isso com o EasyCoexistence

Conecte o número em easycoexistence.com, defina o destino do webhook como a 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 conectado por mês, caindo até US$ 1 conforme você adiciona números, com os 3 primeiros dias grátis.

Perguntas frequentes

Isso funciona no plano gratuito do Supabase?

As Edge Functions rodam no plano gratuito, com limite de 150 segundos de tempo total de execução contra 400 segundos nos planos pagos. Um handler de webhook que responde rápido fica bem dentro de ambos.

Devo usar a service role key na função?

Só no servidor, como aqui, lida do ambiente da função. Ela ignora a Row Level Security, então nunca pode chegar a um navegador.

Uma função pode atender vários números?

Sim. Aponte o destino do webhook de cada número para ela e separe a lógica pelo phone_number_id de cada payload.

O EasyCoexistence vê as mensagens?

Não. A Meta entrega cada webhook direto à sua função, e o EasyCoexistence nunca armazena o conteúdo das conversas.

Continue lendo

Pronto para começar?

Configure o WhatsApp Coexistence em minutos, não em meses. O aplicativo continua funcionando no celular.

Começar teste grátis

Verificado em

Webhooks do WhatsApp em Edge Functions do Supabase