DEV Community

unifyport for UnifyPort

Posted on • Originally published at unifyport.ai

TikTok Shop Customer Service Webhooks: A Production-Ready Implementation Guide

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
Enter fullscreen mode Exit fullscreen mode

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_id
  • shop_id
  • message_id
  • conversation_id
  • index
  • create_time
  • type
  • visibility
  • 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_key followed 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)
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
  },
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

At the normalized message layer, also enforce uniqueness on message_id.

These keys protect different stages:

  • tts_notification_id prevents duplicate webhook processing.
  • message_id prevents duplicate customer messages.
  • conversation_id identifies the message stream.
  • shop_id determines 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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The Get Conversation Messages documentation specifies that:

  • The required scope is seller.customer_service.
  • page_size has 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
Enter fullscreen mode Exit fullscreen mode

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_id count
  • End-to-end message delivery latency

Logs should include safe correlation fields such as:

  • tts_notification_id
  • shop_id
  • conversation_id
  • message_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:

  1. Authorize a real test shop.
  2. Send a buyer message through TikTok Shop.
  3. Confirm that the webhook signature is accepted.
  4. Confirm that the raw event reaches durable storage.
  5. Confirm that the endpoint responds with an empty 200 within three seconds.
  6. Confirm that the worker creates exactly one normalized message.
  7. Replay the webhook and verify that no duplicate message is created.
  8. Simulate an index gap and confirm that history reconciliation runs.
  9. Confirm that the message is routed to the correct workspace.
  10. 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_MESSAGE is 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 200 responses are returned within three seconds.
  • tts_notification_id and message_id are 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)