Получение вебхуков WhatsApp в Supabase Edge Function
Supabase Edge Function может напрямую служить адресом вебхука для номера. Она отвечает на проверочный GET от Meta значением hub.challenge, записывает каждое сообщение из POST в таблицу Postgres и отвечает через Cloud API. Разверните её с отключённой проверкой JWT: по умолчанию Supabase требует валидный JWT, а Meta отправить его не может.
Значение Supabase по умолчанию для Edge Functions: любой запрос без валидного JWT отклоняется ещё до запуска вашего обработчика.
Флаг supabase functions deploy, который отключает проверку JWT для функции.
Тайм-аут простоя запроса для Edge Function; если ответа нет, возвращается 504.
Что должна делать Edge Function?
Две вещи, на двух методах.
На GET она отвечает на проверку Meta. Прочитайте hub.mode, hub.verify_token и hub.challenge из строки запроса, сравните токен с тем, который вы указали как verify token адреса вебхука, и верните challenge простым текстом со статусом 200. Если токен не совпадает, верните 403.
На POST она принимает события. Разберите тело запроса, достаньте сообщения из entry, changes и value, запишите их в таблицу и верните 200. Отвечайте быстро: медленный или неудачный ответ Meta считает сбоем доставки и повторяет отправку, из-за чего одно и то же сообщение приходит дважды.
Укажите URL функции как адрес вебхука номера в EasyCoexistence с тем же verify token, и Meta начнёт доставку.
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");
});Почему функция возвращает Meta ошибку 401?
Потому что включена проверка JWT. Supabase проверяет JWT в заголовке Authorization до запуска обработчика Edge Function, и это поведение по умолчанию для каждой функции. Meta такой заголовок не отправляет, поэтому платформа отклоняет и проверочный GET, и каждую доставку, а ваш код их не видит.
Отключите проверку только для этой функции. Либо разверните её командой supabase functions deploy whatsapp --no-verify-jwt, либо задайте verify_jwt = false в секции функции в supabase/config.toml. Документация Supabase описывает именно этот случай: публичный эндпоинт, который вызывает внешний провайдер вебхуков.
Когда проверка платформы отключена, функции нужна собственная. Заголовок с подписью вам недоступен: Meta подписывает запросы секретом приложения, которому принадлежит подписка. Поместите в URL адреса длинный случайный секрет как параметр запроса и отклоняйте запросы без него, как это сделано в примере на POST.
Как функции сохранять сообщения и отвечать?
Сначала сохраняйте, потом отвечайте. Делайте upsert по идентификатору сообщения WhatsApp с уникальным ограничением, чтобы повторные отправки Meta попадали в ту же строку, а не создавали дубликат. Если на функцию указывает больше одного номера, храните в каждой строке phone_number_id получателя.
Если работа долгая, например вызов модели или другого API перед ответом, передайте её в EdgeRuntime.waitUntil и сразу верните ответ. Функция продолжит работу, пока этот промис не завершится, а Meta уже получит свой 200. Supabase перестаёт ждать после 150 секунд простоя и возвращает 504, так что обработчик, который делает всю работу до ответа, чаще всего и вызывает повторные отправки.
Ответы уходят на эндпоинт сообщений Cloud API с идентификатором номера и токеном, которые хранятся как секреты функции. В течение 24 часов после последнего сообщения клиента доставляется свободный текст, после этого только одобренный шаблон.
Доходят ли до функции сообщения, отправленные с телефона?
Да. На номере Coexistence приложение WhatsApp Business продолжает работать, и когда кто-то отвечает из него, Meta доставляет эхо этого исходящего сообщения на тот же адрес, в поле smb_message_echoes и в массиве message_echoes, а не в messages.
Расширьте разбор так, чтобы он читал оба варианта, и сохраняйте столбец direction. Тогда таблица содержит всю переписку: реплики клиента и ответы бизнеса. Именно это чаще всего и нужно запрашивать потом: время ответа, диалоги без ответа, время последнего входящего сообщения по каждому контакту.
Это последнее значение нужно и любому пути отправки, потому что от него зависит, можно ли ответить свободным текстом или нужен шаблон. Представление над таблицей может отдавать его по каждому контакту остальной части вашего приложения.
Распространённые ошибки
- Развёртывание с включённой проверкой JWT. Supabase отклоняет запросы Meta с кодом 401 до запуска обработчика, и ни одно сообщение не приходит.
- Возврат challenge в обёртке JSON. Meta нужно чистое значение в теле ответа, иначе проверка молча не проходит.
- Долгая работа до ответа. Meta повторяет отправку того, что считает сбоем, и одно и то же сообщение обрабатывается дважды.
- Отключение проверки JWT без собственной проверки. Любой, кто найдёт URL, сможет записывать в таблицу поддельные сообщения.
Подключите номер на easycoexistence.com, укажите URL Edge Function как адрес вебхука с вашим verify token и сохраните идентификатор номера и токен как секреты функции. От US$ 9 за подключённый номер в месяц, до US$ 1 по мере добавления номеров, первые 3 дня бесплатно.
Часто задаваемые вопросы
Работает ли это на бесплатном тарифе Supabase?
Edge Functions работают на бесплатном тарифе, с ограничением по времени выполнения 150 секунд против 400 секунд на платных. Обработчик вебхука, который отвечает быстро, укладывается в любой из этих лимитов с большим запасом.
Нужно ли использовать ключ service role в функции?
Только на стороне сервера, как здесь, читая его из окружения функции. Он обходит защиту на уровне строк, поэтому никогда не должен попадать в браузер.
Может ли одна функция обслуживать несколько номеров?
Да. Укажите её как адрес вебхука для каждого номера и разбирайте запросы по phone_number_id в теле каждого запроса.
Видит ли EasyCoexistence сообщения?
Нет. Meta доставляет каждый вебхук прямо в вашу функцию, а EasyCoexistence никогда не хранит содержимое переписки.
Читайте дальше
Готовы начать?
Настройте WhatsApp Coexistence за несколько минут, а не месяцев. Приложение продолжит работать на телефоне.
Начать бесплатный пробный периодПроверено по состоянию на