Receiving a webhook is easy.
Building a webhook pipeline that survives duplicate deliveries, delayed events, missing messages, invalid signatures, and seller authorization changes is the real engineering work.
This guide explains how to build a production-ready pipeline for TikTok Shop Customer Service messages—from HTTPS ingress to history reconciliation.
First, Understand the Scope
TikTok Shop Customer Service API is designed for conversations between buyers and sellers in a TikTok Shop.
It is not an API for reading ordinary TikTok direct messages.
Before implementation, confirm that:
- Your application has access to the required Customer Service API scopes.
- The seller has authorized your application.
- The target shop is correctly mapped to your internal workspace or tenant.
- Your webhook endpoint is publicly accessible over HTTPS.
Customer Service API access is inactive by default and requires approval. See the official Customer Service API overview and app features documentation.
If these prerequisites are missing, changing webhook code will not solve the problem.
Recommended Architecture
A reliable implementation separates webhook acknowledgement from business processing:
TikTok Shop
|
v
HTTPS webhook ingress
|
+-- Verify signature using raw request body
|
+-- Insert event into a durable inbox
|
+-- Return HTTP 200 within 3 seconds
|
v
Message queue
|
v
Normalize, deduplicate, and route
|
v
Customer service workspace
^
|
History reconciliation worker
The webhook request should not wait for:
- CRM updates
- AI-generated replies
- Media downloads
- Ticket creation
- Search indexing
- External notifications
Persist the event, acknowledge it, and process it asynchronously.
Subscribe to the New Message Event
For incoming customer service messages, subscribe to NEW_MESSAGE, identified as event type 14.
You can configure the subscription in TikTok Shop Partner Center or through the webhook configuration API.
The official event reference is available in the New Message webhook documentation.
Useful fields from the payload include:
tts_notification_idshop_idmessage_idconversation_idindexcreate_timetypevisibility- Sender information
Preserve these identifiers before converting the payload into your internal message model. They are essential for deduplication, routing, ordering, and incident investigation.
Verify the Signature Before Parsing
TikTok Shop sends the webhook signature in the Authorization header.
According to the official webhook overview, the signature is generated with HMAC-SHA256 using:
- Signature content:
app_keyfollowed by the raw webhook body - Signing key: the application's
app_secret
The important word here is raw.
If your framework parses and serializes the JSON before verification, whitespace or character encoding changes can invalidate an otherwise legitimate signature.
A Node.js implementation can look like this:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyTikTokShopWebhook(
rawBody: Buffer,
authorization: string,
appKey: string,
appSecret: string,
): boolean {
const expected = createHmac("sha256", appSecret)
.update(appKey)
.update(rawBody)
.digest("hex");
const actual = authorization.trim().toLowerCase();
if (!/^[a-f0-9]{64}$/.test(actual)) {
return false;
}
const actualBuffer = Buffer.from(actual, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
return (
actualBuffer.length === expectedBuffer.length &&
timingSafeEqual(actualBuffer, expectedBuffer)
);
}
Use constant-time comparison and never log app_secret.
If you use Express, configure raw-body handling specifically for the webhook route:
import express from "express";
const app = express();
app.post(
"/webhooks/tiktok-shop",
express.raw({ type: "application/json" }),
async (req, res) => {
const authorization = req.header("authorization") ?? "";
const rawBody = req.body as Buffer;
if (
!verifyTikTokShopWebhook(
rawBody,
authorization,
process.env.TIKTOK_SHOP_APP_KEY!,
process.env.TIKTOK_SHOP_APP_SECRET!,
)
) {
return res.status(401).send();
}
const payload = JSON.parse(rawBody.toString("utf8"));
await webhookInbox.insertIfAbsent({
notificationId: payload.tts_notification_id,
shopId: payload.shop_id,
payload,
});
return res.status(200).send();
},
);
Make sure another global JSON middleware does not consume this route first.
Acknowledge Within Three Seconds
TikTok Shop requires a successful webhook response to:
- Use HTTP status
200 - Contain an empty response body
- Arrive within three seconds
The endpoint must support HTTPS with TLS 1.2 or newer. TikTok Shop also documents retry behavior when delivery fails or times out. See the official webhook configuration guide.
That creates a strict boundary:
Webhook request = authenticate + persist + acknowledge
Worker job = process + normalize + route + enrich
Do not acknowledge an event before it reaches durable storage. Otherwise, a process crash between acknowledgement and persistence can permanently lose the message.
Make Every Delivery Idempotent
Webhook delivery is at-least-once behavior in practice. Your system must expect the same event to arrive more than once.
Use tts_notification_id as the first idempotency key:
CREATE TABLE webhook_inbox (
notification_id TEXT PRIMARY KEY,
shop_id TEXT NOT NULL,
payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processed_at TIMESTAMPTZ
);
At the normalized message layer, also enforce uniqueness on message_id.
These keys protect different stages:
-
tts_notification_idprevents duplicate webhook processing. -
message_idprevents duplicate customer messages. -
conversation_ididentifies the message stream. -
shop_iddetermines tenant or workspace routing.
A duplicate webhook should still receive 200. Duplication is an expected delivery condition, not an application error.
Do Not Trust Arrival Order
Network delivery order is not guaranteed.
A newer message may arrive before an older one, and a retry may arrive after both. Use the message index and conversation_id to track ordering instead of assuming that request arrival time reflects conversation order.
A simplified worker check might look like this:
if (
state.lastIndex !== null &&
event.index > state.lastIndex + 1
) {
await scheduleHistoryReconciliation(event.conversationId);
}
Preserve create_time, but do not use it as your only ordering mechanism.
Reconcile Missing Messages Through History
Webhooks should not be treated as the complete source of truth. TikTok Shop explicitly recommends using API queries when applications need to recover or validate state.
Use:
GET /customer_service/202309/conversations/{conversation_id}/messages
The Get Conversation Messages documentation specifies that:
- The required scope is
seller.customer_service. -
page_sizehas a maximum value of 10. - Pagination uses
next_page_token. - Fetching messages does not mark them as read.
Trigger reconciliation when:
- A message index gap is detected.
- A worker repeatedly fails to process an event.
- A shop reconnects after authorization problems.
- Monitoring detects an inactive conversation stream.
- Support reports a missing message.
Insert recovered messages through the same idempotent normalization pipeline used for webhook events.
Keep Routing Separate from Authentication
A valid signature proves that the request came from TikTok Shop. It does not tell you which internal customer owns the shop.
Maintain an explicit mapping:
shop_id
-> seller authorization
-> internal tenant
-> customer service workspace
If shop_id is unknown, store the event in an unroutable state instead of discarding it.
This makes delayed provisioning, authorization changes, and mapping mistakes recoverable.
Track Authorization as Its Own Lifecycle
Seller authorization can expire or be revoked independently of webhook delivery.
Treat these as different failure domains:
- Webhook authentication
- Seller access-token validity
- API scope approval
- Shop-to-tenant routing
- Downstream message processing
For example, a webhook may be valid while the history API request fails because the seller token is no longer usable.
Do not classify every API failure as a webhook or network failure.
Monitor the Pipeline, Not Just the Endpoint
A webhook endpoint returning 200 does not prove that messages are reaching the customer service workspace.
Useful production metrics include:
- Webhook acknowledgement latency at p95 and p99
- Invalid signature count
- Duplicate notification count
- Durable inbox insertion failures
- Queue age
- Processing retry count
- Conversation index gaps
- History reconciliation count
- Unroutable
shop_idcount - End-to-end message delivery latency
Logs should include safe correlation fields such as:
tts_notification_idshop_idconversation_idmessage_id- Worker job ID
- Internal tenant ID
Do not log access tokens, application secrets, or full authorization headers.
Common Failure Modes
| Symptom | Likely cause | What to check |
|---|---|---|
Webhook returns 401
|
Signature mismatch | Raw body handling, app key, app secret, header parsing |
Webhook returns 200, but no ticket appears |
Async processing failure | Inbox row, queue job, worker logs, tenant routing |
| Same message appears twice | Missing idempotency | Unique constraints for notification and message IDs |
| Conversation has missing messages | Delivery gap or processing failure | Message index and history reconciliation |
| Message reaches the wrong workspace | Incorrect shop mapping |
shop_id ownership and authorization mapping |
| History API fails | Scope or token problem | Seller authorization and seller.customer_service
|
| Fetching history does not update unread status | Expected behavior | Handle read state through the appropriate API separately |
Production Acceptance Test
Before launch, perform a real end-to-end test:
- Authorize a real test shop.
- Send a buyer message through TikTok Shop.
- Confirm that the webhook signature is accepted.
- Confirm that the raw event reaches durable storage.
- Confirm that the endpoint responds with an empty
200within three seconds. - Confirm that the worker creates exactly one normalized message.
- Replay the webhook and verify that no duplicate message is created.
- Simulate an index gap and confirm that history reconciliation runs.
- Confirm that the message is routed to the correct workspace.
- Inspect the webhook delivery record in Partner Center.
A manually constructed HTTP request can test your endpoint mechanics, but it cannot prove that seller authorization, event subscription, TikTok delivery, and production routing all work together.
Final Checklist
Before going live, verify that:
- Customer Service API access is approved.
- The seller has authorized the application.
-
NEW_MESSAGEis subscribed. - The endpoint uses HTTPS and TLS 1.2 or newer.
- Signatures are verified against the raw request body.
- Events are durably stored before acknowledgement.
- Empty
200responses are returned within three seconds. -
tts_notification_idandmessage_idare deduplicated. - Message ordering uses conversation identifiers and indexes.
- History reconciliation handles missing events.
- Shop routing and authorization lifecycles are observable.
- A real Shop-to-workspace acceptance test has passed.
Production webhook engineering is less about receiving JSON and more about maintaining consistency across two systems that communicate asynchronously.
Once acknowledgement, idempotency, ordering, reconciliation, and authorization are treated as first-class components, the integration becomes much easier to operate.
Originally published on UnifyPort.
Disclosure: AI-assisted drafting and editing were used in preparing this article. The technical content was reviewed against the referenced documentation.
Top comments (0)