# Guide to TikTok Shop Webhooks: Features and Best Practices

TikTok Shop webhooks notify your systems when commerce activity happens on a connected shop: an order changes status, a package updates, a product's status changes. For any app in the TikTok Shop ecosystem, whether an order management tool, a fulfillment integration, or a catalog sync, webhooks are how you react without polling the Partner API.

This guide covers how TikTok Shop webhooks work, the events you can subscribe to, how to verify the signature (which arrives in the Authorization header, not a signature-specific one), the delivery behavior, and the best practices that keep a shop integration reliable in production.

## What are TikTok Shop webhooks?

TikTok Shop webhooks are HTTP POSTs delivered to a URL you configure per app, each carrying a JSON payload about a shop event. The webhook signature scheme is distinct from TikTok Shop's API request signing, and the signature is delivered in the standard `Authorization` header rather than a custom signature header.

Each payload carries a numeric `type`, a `tts_notification_id`, a `shop_id`, a `timestamp`, and a `data` object. The numeric `type` is unreliable to branch on; use the subscribed event type instead.

## TikTok Shop webhook features

| Feature | Details |
| --- | --- |
| Configuration | Partner Center (per app) or the Events API |
| Signature location | `Authorization` header (no `Bearer` prefix) |
| Signature scheme | lowercase-hex HMAC-SHA256 over `app_key + raw body`, keyed with `app_secret` |
| Endpoint requirements | HTTPS on a domain (no IP, no custom port), TLS 1.2+ |
| Acknowledgement | HTTP 200 with an empty body within 3 seconds |
| Rejected signature | Endpoint returns 401 |
| Delivery | At-least-once, 4 retries after failure (2 min, 30 min, 3 h, 12 h) |
| Deduplication | `tts_notification_id` |
| SDKs | App-specific downloads from Partner Center (not on npm/PyPI) |

## Common events

TikTok Shop groups events by resource. Order, package, and product events are the ones most integrations start with:

| Event | Fires when |
| --- | --- |
| `ORDER_STATUS_CHANGE` | An order moves to a new status |
| `PACKAGE_UPDATE` | A package (fulfillment unit) is updated |
| `PRODUCT_STATUS_CHANGE` | A product's status changes |

Subscribe only to the events you process, and branch on the subscribed `event_type` rather than the numeric `type` field in the payload. Availability of specific events can vary by app scope and market, so check your app's configuration in Partner Center for the events you're entitled to.

## Setting up TikTok Shop webhooks

Configure webhooks per shop in Partner Center (App & Service > your app > Basic Information > Developing > Webhook URL / Event subscriptions), or manage them via the Events API:

```bash
curl -X PUT "https://open-api.tiktokglobalshop.com/event/202309/webhooks" \
  -H "Content-Type: application/json" \
  -d '{ "event_type": "ORDER_STATUS_CHANGE", "address": "https://your-app.example.com/webhook" }'

```

Your endpoint must be HTTPS on a domain (no raw IP, no custom port) with TLS 1.2 or higher, and must return HTTP 200 with an empty body within 3 seconds. A 401 signals that you rejected the signature.

## Securing TikTok Shop webhooks

The signature arrives in the `Authorization` header (with no `Bearer` prefix) as a lowercase-hex HMAC-SHA256. The signing base is your `app_key` concatenated with the raw request body, and the key is your `app_secret`. Compute it over the exact raw body and compare in constant time.

```javascript
const crypto = require("crypto");

const APP_KEY = process.env.TTS_APP_KEY;
const APP_SECRET = process.env.TTS_APP_SECRET;

function verify(rawBody, authHeader) {
  const base = APP_KEY + rawBody; // app_key + raw body
  const expected = crypto.createHmac("sha256", APP_SECRET).update(base).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(authHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const auth = req.headers["authorization"];
  if (!verify(req.body.toString("utf8"), auth)) {
    return res.sendStatus(401);
  }

  res.status(200).send(""); // empty 200 within 3 seconds
  processQueue.add(JSON.parse(req.body)); // process asynchronously
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

APP_KEY = os.environ["TTS_APP_KEY"]
APP_SECRET = os.environ["TTS_APP_SECRET"].encode()

def verify(raw_body: str, auth_header: str) -> bool:
    base = (APP_KEY + raw_body).encode()
    expected = hmac.new(APP_SECRET, base, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, auth_header or "")

```

## TikTok Shop webhook limitations and pain points

### The signature is in the Authorization header, over app_key + body

The Problem: Two things trip people up: the signature lives in `Authorization` (which developers often reserve for bearer tokens), and the signing base is `app_key + raw body`, not the body alone. Miss either and every delivery fails verification.

Why It Happens: TikTok Shop's webhook signing differs from its API request signing and reuses the `Authorization` header for the digest.

Workarounds:

* Read the digest from `Authorization` (no `Bearer` prefix) and sign `app_key + raw body` with `app_secret`.
* Verify against the exact raw body and compare in constant time.

How Hookdeck Can Help: Hookdeck verifies the TikTok Shop signature at the edge, so your app receives pre-verified events and doesn't have to special-case the header or the concatenated signing base.

### The three-second empty-200 window

The Problem: You must return an empty-bodied 200 within 3 seconds. Any synchronous processing eats the budget, and a slow response counts as a failure that triggers retries.

Why It Happens: TikTok Shop caps how long it waits so one slow endpoint doesn't hold up delivery.

Workarounds:

* Verify, enqueue, and return an empty 200 immediately; process off a queue.
* Do no blocking work (API calls, DB writes) before responding.

How Hookdeck Can Help: Hookdeck acknowledges TikTok Shop within milliseconds and durably queues events, decoupling the 3-second window from your processing time.

### At-least-once delivery with a fixed retry ladder

The Problem: Delivery is at-least-once with 4 retries after a failure (at 2 minutes, 30 minutes, 3 hours, and 12 hours), then TikTok Shop gives up. The same event arrives more than once, and after the ladder is exhausted it's gone.

Why It Happens: Retries prioritize eventual delivery, and the ladder is capped to avoid retrying forever.

Workarounds:

* Dedupe on `tts_notification_id`.
* Reconcile against the Partner API for anything you can't afford to miss once the ladder is exhausted.
* Make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates on delivery and durably queues events with retry schedules you control, so duplicates don't double-process and a 12-hour ladder isn't your only safety net. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### The numeric type field is unreliable

The Problem: Payloads carry a numeric `type`, but branching on it is fragile. Handlers keyed to the number can misclassify events.

Why It Happens: The numeric `type` isn't a stable public contract; the subscribed event type is.

Workarounds:

* Branch on the subscribed `event_type`, not the numeric `type`.
* Validate the payload shape per event type before acting.

How Hookdeck Can Help: Hookdeck's filters and transformations let you route and normalize on the fields you trust, so downstream handlers aren't reasoning about the numeric `type`.

## Best practices

### Verify the Authorization-header signature correctly

Read the digest from `Authorization` (no `Bearer`), sign `app_key + raw body` with `app_secret`, lowercase-hex, and compare in constant time against the raw body.

### Return an empty 200 within three seconds

Acknowledge immediately with an empty body and defer processing to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Dedupe on `tts_notification_id`

Use `tts_notification_id` as your idempotency key so at-least-once delivery doesn't double-process.

### Branch on the subscribed event type

Ignore the numeric `type` and route on the `event_type` you subscribed to.

### Reconcile with the Partner API

The retry ladder gives up after 12 hours. Reconcile periodically so a permanently failed delivery doesn't leave you out of sync.

## Conclusion

TikTok Shop webhooks put order, package, and product activity on a push channel your app can act on, but the details are specific: a signature in the `Authorization` header computed over `app_key + raw body`, a hard 3-second empty-200 window, at-least-once delivery on a fixed 4-step retry ladder, and a numeric `type` field you shouldn't trust. Get any of those wrong and you either reject valid events or double-process duplicates.

That leaves verification, fast acknowledgement, deduplication, and reconciliation on you. [Hookdeck](https://hookdeck.com) verifies the signature, deduplicates on `tts_notification_id`, and durably queues every event at the edge, so your integration processes verified, unique shop events and never races the 3-second window.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle TikTok Shop webhooks reliably in minutes.