Skip to main content
Socialift
Documentation sections
Technical guide

Receiving webhooks with the SDK

A webhook endpoint does three things: it verifies the request is genuine, returns 200 quickly and queues the heavy work. Meta and TikTok sign the body with HMAC-SHA256, so the signature is checked against the raw bytes; Telegram does not sign the body and only sends an X-Telegram-Bot-Api-Secret-Token header. The socialift.webhooks module has ready-made functions for all three.

Updated:

In short: what should a webhook endpoint do?

For Meta: on GET, return hub.challenge as plain text via verify_challenge(); on POST, read the raw body, check X-Hub-Signature-256 with verify_request(), split it into events with parse_events(), queue them and return 200. For TikTok use verify_tiktok_request() and parse_tiktok_event(); for a Telegram bot, verify_telegram_request(). Filter repeated deliveries with dedupe_key() or event.key.

This page is a technical how-to. For an overview of Socialift's webhook capabilities, see the Webhook integration page.

How do the platforms sign webhooks?

How do the platforms sign webhooks? — Capability matrix by platform
PlatformHeaderVerificationRedeliverySDK functions
Meta (Facebook, Instagram)X-Hub-Signature-256: sha256=<hex>HMAC-SHA256: key is the App Secret, message is the raw bodyImmediately, then with decreasing frequency over 36 hoursverify_challenge, verify_request, parse_events, dedupe_key
TikTokTiktok-Signature: t=<unix>,s=<hex>HMAC-SHA256: key is the Client Secret, message is <t>.<raw body>Up to 72 hours with exponential backoffverify_tiktok_request, parse_tiktok_event
Telegram Bot APIX-Telegram-Bot-Api-Secret-TokenNo signature: compare with the secret_token given to setWebhookRepeats on non-2XX responses and gives up after a reasonable number of attemptsverify_telegram_request

Redelivery rules come from the platforms' documentation and may change — sources are listed at the end of the page.

FastAPI: a Meta webhook — verification and events

meta_webhook.py
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()   # in production: Redis, RQ, Celery, etc.


@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")   # the bare string, not JSON


@app.post("/webhooks/meta")
async def meta_receive(request: Request):
    body = await request.body()                           # raw bytes
    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() returns hub.challenge when hub.mode=subscribe and hub.verify_token match, otherwise None. The response must be exactly the challenge string.

verify_request() needs the raw bytes before your framework parses the body — re-serialised JSON breaks the signature. A missing or empty header yields False, and the comparison uses hmac.compare_digest.

request.headers is case-insensitive. If you pass a plain dict, the key must be X-Hub-Signature-256 or x-hub-signature-256.

Each Page is subscribed separately: await fb.subscribe_page(page_id, page_token, fields=['leadgen', 'feed']). Meta requires the pages_manage_metadata and pages_show_list permissions for this; without the subscription no events arrive, even with the webhook configured at app level.

WebhookEvent: what does parse_events() return?

Meta sends two formats: changes (leadgen, feed, comments) and messaging (for example, Instagram Direct messages). parse_events() turns both into a single stream of WebhookEvent objects.

object_type
The payload's object, for example page or instagram.
field
The field value for changes (leadgen, comments, feed, …); messages for messaging entries and messages_standby for standby.
object_id and time
entry.id (usually a Page or Instagram account ID) and entry.time (unix).
value
The event itself: a changes[].value or a messaging[] item.
leadgen_id, form_id, page_id
Convenience properties for Lead Ads events; when value has no page_id, object_id is returned.
key
A stable key for catching repeated deliveries — the same as dedupe_key(event).

FastAPI: a TikTok webhook

tiktok_webhook.py
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("Access revoked:", event.user_openid)   # delete this user's tokens
    return Response(status_code=200)

TikTok sends one event per request: event.event is the event name, event.user_openid the user and event.create_time a unix timestamp. TikTok delivers content as a JSON string; the SDK parses it into the event.content dict and, if the string is malformed, keeps it as {'raw': ...}.

tolerance (300 seconds by default): if the timestamp differs from the current time by more than that, the request is rejected, which protects against replays of old requests. Keep the server clock in sync; tolerance=0 disables the check and is not recommended.

TikTok's documentation spells the header Tiktok-Signature. request.headers handles that; with a plain dict the SDK looks for the TikTok-Signature or tiktok-signature key.

event.key is a dedupe key based on publish_id, video_id or comment_id, or on a hash of the body when those are absent.

FastAPI: a Telegram bot webhook

telegram_webhook.py
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 chars: A-Z, a-z, 0-9, _ and -


async def register_webhook():                      # once, at deploy time
    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"))     # put it on a queue
    return Response(status_code=200)

The argument order differs from Meta: verify_telegram_request(headers, secret_token). With an empty secret_token the function always returns False, so a webhook never stays open without a secret.

Telegram does not sign the body, so the secret is the only protection: without it anyone who knows the URL can post a fake update. The comparison runs in constant time (compare_digest).

Once a webhook is set, get_updates() stops working. If updates do not arrive, check last_error_message and pending_update_count in await bot.get_webhook_info().

Why return 200 quickly and queue the work?

Platforms retry deliveries. Meta retries a failed delivery immediately and then with decreasing frequency over the next 36 hours. TikTok retries for up to 72 hours with exponential backoff and states plainly that the same event may arrive more than once. Telegram repeats the request after any non-2XX response.

Writing to a CRM, downloading files or calling a slow API inside the handler delays the response; if the platform treats the delivery as failed, it sends the event again and duplicates multiply. The right order is: verify the signature → write the event to a queue (Redis, RQ, Celery or a database table) → return 200 immediately. A separate worker does the processing.

Meta may batch several events (up to 1,000) into one request but does not guarantee it — handle each event individually.

Idempotency: process each event exactly once

worker.py
import asyncio
from socialift.webhooks import WebhookEvent, dedupe_key

seen: set[str] = set()   # in production: a UNIQUE column in the DB or Redis SET NX


async def worker(events: asyncio.Queue):
    while True:
        event: WebhookEvent = await events.get()
        key = dedupe_key(event)                  # same as event.key
        if key not in seen:
            seen.add(key)
            if event.field == "leadgen" and event.leadgen_id:
                print("New lead:", event.leadgen_id, "form:", event.form_id)
        events.task_done()

Meta provides no event ID, so dedupe_key() builds the key from the content: from leadgen_id, mid, comment_id, media_id or post_id when present, otherwise from a hash of value plus object_id and the time. Meta resends exactly the same body, so the fallback catches duplicates too.

An in-memory set is wiped on restart and is not shared between workers. In production write the key to a table with a UNIQUE column (INSERT ... ON CONFLICT DO NOTHING) and process the event only if the insert succeeded.

Production checklist

  • Expose the endpoint over HTTPS only — TikTok requires HTTPS for the callback URL.
  • Verify the signature on the raw bytes before parsing; answer invalid signatures with 403 and log them.
  • Keep the App Secret, Client Secret, verify token and Telegram secret in environment variables.
  • Return 200 immediately and queue the processing.
  • Store the dedupe key in a UNIQUE database column.
  • On Meta, subscribe every Page with subscribe_page().
  • Meta's data deletion callback arrives with a signed_request in the POST body, not with X-Hub-Signature-256 — decode it with socialift.crypto.parse_signed_request(signed_request, app_secret) and answer 400 when it returns None.

Webhook URLs in the Socialift dashboard

If you connect accounts through the Socialift dashboard and add your own Meta or TikTok app under “Kalitlar” (Keys), the dashboard shows a dedicated Webhook URL and verify token for that app once the key is saved — copy them into the platform console.

Every app gets its own URL because Meta and TikTok sign events with that specific app's secret. If you receive webhooks on your own server with the SDK, the examples on this page are all you need.

Frequently asked questions

My Meta webhook signature doesn't match — why?

There are usually three causes: the body is not raw (the framework parsed and rebuilt the JSON), the App Secret belongs to a different app, or a proxy rewrites the body. Pass the result of await request.body() and the secret of the app the webhook is configured in to verify_request().

Meta says “The URL couldn't be validated”. What should I do?

The GET response must be exactly the hub.challenge string — not JSON, but text/plain. Check that hub.verify_token matches the value in the console and that the endpoint is reachable over HTTPS. verify_challenge() returns None on a mismatch, in which case answer 403.

The TikTok signature is correct but verify_tiktok_request() returns False. Why?

Most often it is clock skew: if the server time differs from TikTok's timestamp by more than 300 seconds, the request is rejected. Sync the clock with NTP. Also make sure client_secret is not empty and pass the headers as request.headers.

How do I secure a Telegram webhook?

Pass a secret_token to set_webhook() and check every request with verify_telegram_request(request.headers, secret). Telegram echoes the string in the X-Telegram-Bot-Api-Secret-Token header. The secret is 1–256 characters long and may contain only A-Z, a-z, 0-9, _ and -.

The same event arrived twice — is that a bug?

No, it is expected: Meta, TikTok and Telegram all retry deliveries. Process each event once using dedupe_key() or event.key, and respond quickly.

Do webhook requests count in the Socialift “Statistika” (Usage) page?

No. Webhooks are called by the platforms, so they are not recorded against any key. The SDK helpers (socialift.webhooks) make no network calls at all — they run only on your server.

Sources

  1. Meta: Webhooks — Getting started
  2. Meta: Page subscribed_apps
  3. Meta: Data deletion callback
  4. TikTok: Webhooks overview
  5. TikTok: Webhook signature verification
  6. Telegram Bot API: setWebhook

Related pages