# 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.

```javascript
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:

```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.parse` the `content` string, 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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle TikTok webhooks reliably in minutes.