Guide to TikTok Webhooks: Features and Best Practices
TikTok for Developers webhooks notify your application about account and content activity: an authorization is removed, a video upload fails, a video publish completes, a data-portability download is ready. If you're building on TikTok's Login Kit, Content Posting, or Data Portability APIs, webhooks are how you react to these events without polling.
This guide covers how TikTok for Developers webhooks work, the events you'll handle, how to verify the TikTok-Signature, and the best practices for production.
This covers TikTok for Developers (developers.tiktok.com), Login Kit / Content Posting / Data Portability. It is not TikTok Shop, which is a separate partner platform with a different signature scheme and its own guide.
What are TikTok webhooks?
TikTok for Developers webhooks are JSON POSTs delivered over HTTPS to the callback URL registered in your developer app. Each is signed with a TikTok-Signature header formatted t=<unix_ts>,s=<sig>. The signature is an HMAC-SHA256 (hex) over <timestamp> + "." + raw_body, keyed with your app's client_secret.
TikTok webhook features
| Feature | Details |
|---|---|
| Configuration | Developer portal: HTTPS callback URL + subscribed events |
| Signature header | TikTok-Signature, formatted t=<ts>,s=<sig> |
| Signature scheme | HMAC-SHA256 (hex) over <timestamp>.<raw_body>, keyed with client_secret |
| Replay | Reject stale timestamps (no explicit tolerance documented) |
| Payload | client_key, event, create_time (epoch seconds), user_openid, content (a serialized JSON string) |
| Delivery | Best-effort with retries up to 72h (exponential backoff); duplicates possible |
| SDK | None |
Common events
TikTok for Developers has exactly four events (verified spellings):
| Event | Fires when |
|---|---|
authorization.removed | A user removes your app's authorization |
video.upload.failed | A video upload fails |
video.publish.completed | A video publish completes |
portability.download.ready | A data-portability download is ready |
Note the exact spellings: it's video.publish.completed (not .complete), and there is no video.publish.failed. The content field is a serialized JSON string, parse it separately.
Setting up TikTok webhooks
In the developer portal, configure an HTTPS callback URL and subscribe to events. Your app's client_secret is the HMAC key.
Securing TikTok webhooks
The TikTok-Signature header is t=<ts>,s=<sig>. To verify, build <timestamp> + "." + raw_body, compute an HMAC-SHA256 with your client_secret, hex-encode it, and compare against the s= value. Verify against the raw body, and reject implausibly old timestamps to guard against replay.
const crypto = require("crypto");
const CLIENT_SECRET = process.env.TIKTOK_CLIENT_SECRET;
function verify(rawBody, signatureHeader) {
const params = Object.fromEntries(
(signatureHeader || "").split(",").map((kv) => kv.split("="))
);
const timestamp = params.t;
// Reject stale deliveries (no explicit window documented; pick a few minutes)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const message = `${timestamp}.${rawBody.toString()}`;
const expected = crypto.createHmac("sha256", CLIENT_SECRET).update(message).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(params.s || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.headers["tiktok-signature"])) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast
const body = JSON.parse(req.body);
const content = JSON.parse(body.content || "{}"); // content is a JSON string
processQueue.add({ event: body.event, content }); // async
});
The same check in Python:
import hashlib
import hmac
import os
import time
CLIENT_SECRET = os.environ["TIKTOK_CLIENT_SECRET"].encode()
def verify(raw_body: bytes, signature_header: str) -> bool:
params = dict(kv.split("=", 1) for kv in (signature_header or "").split(",") if "=" in kv)
timestamp = params.get("t", "")
if abs(time.time() - int(timestamp)) > 300:
return False
message = (timestamp + ".").encode() + raw_body
expected = hmac.new(CLIENT_SECRET, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, params.get("s", ""))
TikTok webhook limitations and pain points
It's not TikTok Shop
The Problem: TikTok for Developers and TikTok Shop are different platforms with different signature schemes. Applying one's verification to the other fails.
Why It Happens: They're separate products (developers.tiktok.com vs the TikTok Shop partner portal).
Workarounds:
- Use this scheme (
TikTok-Signature,client_secret,ts.body) for TikTok for Developers; use TikTok Shop's own scheme for that platform.
How Hookdeck Can Help: Hookdeck verifies each source with the right scheme at the edge, so your app isn't coupled to which TikTok platform sent an event.
The signed message is timestamp.body
The Problem: The HMAC is over <timestamp> + "." + raw_body, not the body alone, and there's no explicit replay window documented. Signing the body by itself fails, and skipping the timestamp check leaves you open to replay.
Why It Happens: TikTok binds the timestamp into the signed message.
Workarounds:
- Build
timestamp + "." + body, verify over the raw body, and enforce your own freshness window.
How Hookdeck Can Help: Hookdeck verifies the signature and freshness at the edge, so your app receives pre-verified, fresh events.
The four exact event names
The Problem: There are only four events, and the spellings are easy to get wrong (video.publish.completed, not .complete; there's no video.publish.failed). A handler keyed to a guessed name never fires.
Why It Happens: TikTok's event catalog is small and specifically spelled.
Workarounds:
- Use the exact four names and handle unknown events defensively.
How Hookdeck Can Help: Hookdeck's filters route on the exact event value you receive.
content is a serialized JSON string, and duplicates happen
The Problem: The content field is a JSON string (not a nested object), so you parse it separately, and best-effort delivery with 72h retries means duplicates.
Why It Happens: TikTok nests content as a string and delivers at-least-once.
Workarounds:
JSON.parsethecontentstring, and dedupe on an event identifier (make handlers idempotent).
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so retries don't double-process. See our guide to webhook idempotency.
Best practices
Verify over timestamp.body with client_secret
Build <timestamp>.<raw_body>, HMAC-SHA256 with your client_secret, hex, compare against s= in constant time, and reject stale timestamps.
Use the four exact event names
Handle authorization.removed, video.upload.failed, video.publish.completed, and portability.download.ready; don't guess spellings.
Parse the content string and dedupe
JSON.parse the content field, and dedupe across the 72-hour retry window.
Acknowledge fast, process asynchronously
Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.
Make TikTok webhooks production-ready
Hookdeck verifies TikTok-Signature, deduplicates, and durably queues every event
Conclusion
TikTok for Developers webhooks (not TikTok Shop) are verified with a TikTok-Signature HMAC-SHA256 over <timestamp>.<raw_body>, keyed with your client_secret. There are exactly four events with specific spellings, the content field is a serialized JSON string, and delivery is best-effort with 72h retries. Verify over the raw body, enforce freshness, use the exact event names, and dedupe.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle TikTok webhooks reliably in minutes.