Приём вебхуков с помощью SDK
Endpoint вебхука делает три вещи: проверяет подлинность запроса, быстро возвращает 200 и ставит тяжёлую работу в очередь. Meta и TikTok подписывают тело HMAC-SHA256, поэтому подпись проверяется по сырым байтам; Telegram тело не подписывает, а только присылает заголовок X-Telegram-Bot-Api-Secret-Token. В модуле socialift.webhooks есть готовые функции для всех трёх случаев.
Обновлено:
Коротко: что должен делать endpoint вебхука?
Для Meta: на GET верните hub.challenge обычным текстом через verify_challenge(); на POST прочитайте сырое тело, проверьте X-Hub-Signature-256 через verify_request(), разбейте на события через parse_events(), поставьте в очередь и верните 200. Для TikTok — verify_tiktok_request() и parse_tiktok_event(), для Telegram-бота — verify_telegram_request(). Повторные доставки отсекайте по dedupe_key() или event.key.
Эта страница — техническая инструкция. Общее описание возможностей вебхуков Socialift — на странице «Интеграция вебхуков».
Как платформы подписывают вебхуки?
| Платформа | Заголовок | Проверка | Повторная доставка | Функции SDK |
|---|---|---|---|---|
| Meta (Facebook, Instagram) | X-Hub-Signature-256: sha256=<hex> | HMAC-SHA256: ключ — App Secret, сообщение — сырое тело | Сразу, затем с убывающей частотой в течение 36 часов | verify_challenge, verify_request, parse_events, dedupe_key |
| TikTok | Tiktok-Signature: t=<unix>,s=<hex> | HMAC-SHA256: ключ — Client Secret, сообщение — <t>.<сырое тело> | До 72 часов с экспоненциальным backoff | verify_tiktok_request, parse_tiktok_event |
| Telegram Bot API | X-Telegram-Bot-Api-Secret-Token | Подписи нет: сравнение с secret_token из setWebhook | Повторяет при ответе не 2XX и прекращает после разумного числа попыток | verify_telegram_request |
Правила повторной доставки взяты из документации платформ и могут меняться — источники в конце страницы.
FastAPI: вебхук Meta — подтверждение и события
import asyncio
import json
import os
from fastapi import FastAPI, Request, Response
from socialift.webhooks import parse_events, verify_challenge, verify_request
app = FastAPI()
events: asyncio.Queue = asyncio.Queue() # в продакшене: Redis, RQ, Celery и т. п.
@app.get("/webhooks/meta")
async def meta_verify(request: Request):
challenge = verify_challenge(dict(request.query_params), os.environ["META_VERIFY_TOKEN"])
if challenge is None:
return Response(status_code=403)
return Response(challenge, media_type="text/plain") # не JSON, а именно строка
@app.post("/webhooks/meta")
async def meta_receive(request: Request):
body = await request.body() # сырые байты
if not verify_request(os.environ["FACEBOOK_APP_SECRET"], body, request.headers):
return Response(status_code=403)
for event in parse_events(json.loads(body)):
events.put_nowait(event)
return Response(status_code=200)verify_challenge() возвращает hub.challenge, если совпали hub.mode=subscribe и hub.verify_token, иначе None. Ответом должна быть ровно строка challenge.
verify_request() нужны сырые байты до разбора тела фреймворком — пересериализованный JSON ломает подпись. Нет заголовка или он пустой — результат False; сравнение идёт через hmac.compare_digest.
request.headers не зависит от регистра. Если передаёте обычный dict, ключ должен быть X-Hub-Signature-256 или x-hub-signature-256.
Каждую страницу подписывают отдельно: await fb.subscribe_page(page_id, page_token, fields=['leadgen', 'feed']). Meta требует для этого разрешения pages_manage_metadata и pages_show_list; без подписки события не придут, даже если webhook настроен на уровне приложения.
WebhookEvent: что возвращает parse_events()?
Meta присылает два формата: changes (leadgen, feed, comments) и messaging (например, сообщения Instagram Direct). parse_events() превращает оба в единый поток WebhookEvent.
object_type- Поле
objectиз payload: например,pageилиinstagram. field- Значение
fieldдляchanges(leadgen,comments,feedи т. д.);messagesдля записейmessagingиmessages_standbyдляstandby. object_idиtimeentry.id(обычно ID страницы или аккаунта Instagram) иentry.time(unix).value- Само событие:
changes[].valueили элементmessaging[]. leadgen_id,form_id,page_id- Удобные свойства для событий Lead Ads; если в
valueнетpage_id, возвращаетсяobject_id. key- Стабильный ключ для отсева повторных доставок — то же, что
dedupe_key(event).
FastAPI: вебхук TikTok
import json
import os
from fastapi import FastAPI, Request, Response
from socialift.webhooks import parse_tiktok_event, verify_tiktok_request
app = FastAPI()
@app.post("/webhooks/tiktok")
async def tiktok_receive(request: Request):
body = await request.body()
if not verify_tiktok_request(os.environ["TIKTOK_CLIENT_SECRET"], body, request.headers):
return Response(status_code=403)
event = parse_tiktok_event(json.loads(body))
if event.event == "authorization.removed":
print("Доступ отозван:", event.user_openid) # удалите токены пользователя
return Response(status_code=200)TikTok присылает одно событие на запрос: event.event — имя события, event.user_openid — пользователь, event.create_time — unix-время. Поле content TikTok передаёт JSON-строкой; SDK разбирает его в словарь event.content, а если строка битая — оставляет её в {'raw': ...}.
tolerance (по умолчанию 300 секунд): если метка времени отличается от текущего времени больше чем на столько секунд, запрос отклоняется — это защита от повторной отправки старого запроса. Держите часы сервера синхронизированными; tolerance=0 отключает проверку и не рекомендуется.
В документации TikTok заголовок называется Tiktok-Signature. С request.headers проблем нет; если передаёте обычный dict, SDK ищет ключ TikTok-Signature или tiktok-signature.
event.key — ключ дедупликации по publish_id, video_id или comment_id, а если их нет — по хешу тела.
FastAPI: вебхук Telegram-бота
import os
from fastapi import FastAPI, Request, Response
from socialift import Telegram
from socialift.webhooks import verify_telegram_request
app = FastAPI()
SECRET = os.environ["TELEGRAM_WEBHOOK_SECRET"] # 1–256 символов: A-Z, a-z, 0-9, _ и -
async def register_webhook(): # один раз при деплое
async with Telegram.from_env() as bot:
await bot.set_webhook("https://example.uz/webhooks/telegram",
secret_token=SECRET, allowed_updates=["message"])
@app.post("/webhooks/telegram")
async def telegram_receive(request: Request):
if not verify_telegram_request(request.headers, SECRET):
return Response(status_code=403)
update = await request.json()
print(update.get("message", {}).get("text")) # поставьте в очередь
return Response(status_code=200)Порядок аргументов отличается от Meta: verify_telegram_request(headers, secret_token). При пустом secret_token функция всегда возвращает False — чтобы webhook не остался открытым без секрета.
Telegram не подписывает тело, поэтому секрет — единственная защита: без него любой, кто знает адрес, может прислать поддельный update. Сравнение выполняется за постоянное время (compare_digest).
После установки webhook get_updates() не работает. Если обновления не приходят, смотрите last_error_message и pending_update_count в await bot.get_webhook_info().
Зачем быстро отвечать 200 и уносить работу в очередь?
Платформы повторяют доставку. Meta повторяет неудачную доставку сразу, а затем с убывающей частотой в течение 36 часов. TikTok повторяет до 72 часов с экспоненциальным backoff и прямо пишет, что одно событие может прийти несколько раз. Telegram повторяет запрос после ответа, отличного от 2XX.
Запись в CRM, загрузка файлов или медленный вызов API внутри обработчика задерживают ответ; если платформа сочтёт доставку неудачной, она пришлёт событие снова и дублей станет больше. Правильный порядок: проверить подпись → записать событие в очередь (Redis, RQ, Celery или таблица в БД) → сразу 200. Обработкой занимается отдельный воркер.
Meta может объединять несколько событий (до 1000) в один запрос, но не гарантирует этого — обрабатывайте каждое событие отдельно.
Идемпотентность: обработать каждое событие один раз
import asyncio
from socialift.webhooks import WebhookEvent, dedupe_key
seen: set[str] = set() # в продакшене: UNIQUE-колонка в БД или Redis SET NX
async def worker(events: asyncio.Queue):
while True:
event: WebhookEvent = await events.get()
key = dedupe_key(event) # то же, что event.key
if key not in seen:
seen.add(key)
if event.field == "leadgen" and event.leadgen_id:
print("Новый лид:", event.leadgen_id, "форма:", event.form_id)
events.task_done()Meta не даёт ID события, поэтому dedupe_key() собирает ключ из содержимого: по leadgen_id, mid, comment_id, media_id или post_id, если они есть, иначе по хешу value, object_id и времени. Meta повторно присылает точно такое же тело, поэтому и запасной вариант ловит дубль.
set в памяти очищается при перезапуске и не делится между воркерами. В продакшене пишите ключ в таблицу с UNIQUE-колонкой (INSERT ... ON CONFLICT DO NOTHING) и обрабатывайте событие, только если вставка прошла.
Чек-лист для продакшена
- Endpoint доступен только по HTTPS — TikTok требует HTTPS для callback URL.
- Проверяйте подпись по сырым байтам до разбора тела; на неверную подпись отвечайте 403 и пишите в лог.
- Храните App Secret, Client Secret, verify token и секрет Telegram в переменных окружения.
- Возвращайте 200 сразу, обработку ставьте в очередь.
- Храните ключ дедупликации в UNIQUE-колонке базы.
- В Meta подписывайте каждую страницу через
subscribe_page(). - Callback удаления данных Meta приходит не с
X-Hub-Signature-256, а сsigned_requestв теле POST — разбирайте его черезsocialift.crypto.parse_signed_request(signed_request, app_secret)и отвечайте 400, если вернулсяNone.
Адреса вебхуков в кабинете Socialift
Если вы подключаете аккаунты через кабинет Socialift и добавили своё Meta- или TikTok-приложение в разделе «Kalitlar» (ключи), после сохранения ключа кабинет покажет отдельные Webhook URL и verify token для этого приложения — их нужно скопировать в консоль платформы.
У каждого приложения свой адрес, потому что Meta и TikTok подписывают событие секретом именно этого приложения. Если вы принимаете вебхуки на своём сервере через SDK, достаточно примеров с этой страницы.
Частые вопросы
Подпись вебхука Meta не совпадает — в чём причина?
Обычно причин три: тело не сырое (фреймворк разобрал JSON и собрал заново), используется App Secret другого приложения или прокси меняет тело. Передавайте в verify_request() результат await request.body() и секрет того приложения, в котором настроен webhook.
Meta пишет «The URL couldn't be validated». Что делать?
Ответом на GET должна быть ровно строка hub.challenge — не JSON, а text/plain. Проверьте, что hub.verify_token совпадает со значением в консоли и endpoint доступен по HTTPS. verify_challenge() при несовпадении возвращает None — тогда отвечайте 403.
Подпись TikTok верная, но verify_tiktok_request() возвращает False. Почему?
Чаще всего из-за расхождения часов: если время сервера отличается от метки TikTok больше чем на 300 секунд, запрос отклоняется. Синхронизируйте часы через NTP. Также проверьте, что client_secret не пустой, и передавайте заголовки как request.headers.
Как защитить вебхук Telegram?
Передайте secret_token в set_webhook() и проверяйте каждый запрос через verify_telegram_request(request.headers, secret). Telegram возвращает эту строку в заголовке X-Telegram-Bot-Api-Secret-Token. Секрет — от 1 до 256 символов, допускаются только A-Z, a-z, 0-9, _ и -.
Одно событие пришло дважды — это ошибка?
Нет, это ожидаемо: Meta, TikTok и Telegram повторяют доставку. Обрабатывайте событие один раз по dedupe_key() или event.key и отвечайте быстро.
Учитываются ли вебхуки в «Statistika» (статистике) Socialift?
Нет. Вебхуки вызывают платформы, поэтому они не записываются в использование какого-либо ключа. А хелперы SDK (socialift.webhooks) вообще не ходят в сеть — они работают только на вашем сервере.