TikTok Shop API Integration Guide: OAuth, Signing, Webhooks and Rate Limits
TikTok Shop API integration from the official docs: OAuth tokens, HMAC request signing, webhook verification, rate limits and the errors to expect.
Long Nguyen
Lập trình viên Fullstack · Kỹ sư AI · Nhà nghiên cứu
How a TikTok Shop API integration fits together
A TikTok Shop API integration is four moving parts spread across three different hosts: seller authorization, token exchange, signed business API calls, and inbound webhooks. Most failed first attempts come from calling the wrong host or signing the wrong bytes, not from the endpoints themselves.
| Step | Host or mechanism | What it does |
|---|---|---|
| 1. Seller authorizes your app | services.tiktokshop.com (rest of world) or services.us.tiktokshop.com (US), path /open/authorize?service_id=... |
Returns a temporary auth_code to your redirect URL |
| 2. Token exchange | auth.tiktok-shops.com, /api/v2/token/get and /api/v2/token/refresh |
Trades the code for an access token and a refresh token |
| 3. Business API calls | open-api.tiktokglobalshop.com, /{category}/{version}/{resource} |
Products, orders, fulfillment, returns, finance, webhook configuration |
| 4. Webhooks | An HTTPS endpoint you host | Pushes order, package, product and authorization events to you |
The token host and the business host are different, and the token exchange is a GET that carries your app secret in the query string. That is unusual enough to trip up developers used to a standard OAuth 2.0 POST, so it gets its own section below.
Getting TikTok Shop API access: Partner Center, app and scopes
Everything starts in TikTok Shop Partner Center. Under App & Service you create the app, and its detail page shows three values you will use constantly: app_key, app_secret and service_id. The service_id builds the seller authorization link; the key and secret identify your app, and the secret signs every request, so it belongs on your server only.
Scopes deserve more thought than they usually get:
- You enable scopes for the app in Partner Center. TikTok's authorization guide warns that enabling scopes you do not need can lengthen app review and lower the rate at which sellers approve your authorization.
- The scopes a seller actually approved come back in the
granted_scopesfield of the token response. Error105005(access denied) means either the app or that token lacks the scope the endpoint needs. If the token is the problem, the seller has to reauthorize before you get a token that carries it. - If your app has an IP allow list configured, calls from an address that is not on it fail with
36009033.
My recommendation: request only the scopes for the features you ship first, then add scopes per feature. A lean first authorization screen converts better, and a later scope change means asking every connected seller to reauthorize, so it is cheaper to plan scope groups than to discover them one at a time.
For testing, Partner Center provides Seller Center development shops and an API testing tool. Under Development Kits there is also a Webhook Log that shows what TikTok Shop sent and what your server answered, which is the fastest way to debug a webhook endpoint.
TikTok Shop OAuth: authorization link, auth_code and token exchange
- Send the seller to your authorization link. Add a
statevalue that is unguessable and stored server-side, and check it when the seller comes back (CSRF protection). - TikTok redirects the seller to your Redirect URL with
?code=...&state=.... If the seller rejects, they are still redirected, but withcode=null&error=auth_denied. - Exchange the
code(calledauth_codein the token API) immediately. It expires after 30 minutes and works once. Reusing or delaying it gives error36004004. - Store the returned tokens, the shop identity and the granted scopes.
The token endpoints have three quirks worth knowing before you write the client:
- Both the exchange and the refresh are
GETrequests, withapp_key,app_secretand the code or refresh token in the query string. grant_typemust be exactlyauthorized_codefor the exchange (not the standardauthorization_code) andrefresh_tokenfor the refresh.- Success is
code: 0in the JSON body, so check the body, not only the HTTP status.
import requests
AUTH_HOST = "https://auth.tiktok-shops.com"
def _token_call(path, params):
resp = requests.get(f"{AUTH_HOST}{path}", params=params, timeout=15)
payload = resp.json()
if payload.get("code") != 0: # success is code 0 in the body
raise RuntimeError(f"token call failed: code={payload.get('code')} "
f"message={payload.get('message')} "
f"request_id={payload.get('request_id')}")
return payload["data"]
def exchange_auth_code(app_key, app_secret, auth_code):
return _token_call("/api/v2/token/get", {
"app_key": app_key,
"app_secret": app_secret,
"auth_code": auth_code,
"grant_type": "authorized_code", # not "authorization_code"
})
def refresh_tokens(app_key, app_secret, refresh_token):
return _token_call("/api/v2/token/refresh", {
"app_key": app_key,
"app_secret": app_secret,
"refresh_token": refresh_token,
"grant_type": "refresh_token",
})
The response carries access_token, access_token_expire_in and refresh_token_expire_in as Unix timestamps, plus open_id, seller_name, seller_base_region, user_type and granted_scopes. The access token is valid for 7 days by default. The refresh token expires when the authorization duration the seller granted ends, so it is not a permanent credential.
Practical consequences for your design:
- Store both expiry timestamps per shop and refresh on a schedule well before the access token expires, instead of waiting for a
105002(expired credentials) response in the middle of an order sync. - Treat every refresh response as authoritative and replace both tokens with what comes back.
- Because the app secret travels in the query string, keep these two URLs out of access logs, APM traces and error reports.
- Use
user_typeto confirm which kind of user authorized you (seller, creator or partner) before you route a token to seller-scoped endpoints.
How to sign TikTok Shop API requests (HMAC-SHA256)
Every business API call needs a sign query parameter. Requests without a valid signature are denied. TikTok's signing guide defines the algorithm, and it is a string-building exercise with six steps:
- Take all query parameters except
signandaccess_token, and sort the keys alphabetically. - Concatenate them as
{key}{value}with no separators. - Prepend the request path, including category, version and resource (for example
/authorization/202309/shops). - Unless the content type is
multipart/form-data, append the request body. - Wrap the whole string in your app secret on both sides:
secret + string + secret. - Compute HMAC-SHA256 of that string using the app secret as the key, and hex-encode the result.
import hashlib, hmac
from urllib.parse import urlsplit, parse_qsl
def sign_request(url, body, app_secret, content_type="application/json"):
parts = urlsplit(url)
params = {k: v for k, v in parse_qsl(parts.query, keep_blank_values=True)
if k not in ("sign", "access_token")}
base = parts.path + "".join(f"{k}{params[k]}" for k in sorted(params))
if not content_type.startswith("multipart/form-data"):
base += body.decode("utf-8") # the exact bytes you will send
wrapped = app_secret + base + app_secret
return hmac.new(app_secret.encode(), wrapped.encode(),
hashlib.sha256).hexdigest()
To check any implementation, use the worked example from the guide: app secret e59af819cc, path /authorization/202309/shops, app_key=29a39d and timestamp=1623812664. The final signed string is e59af819cc/authorization/202309/shopsapp_key29a39dtimestamp1623812664e59af819cc and the signature is b596b73e0cc6de07ac26f036364178ab16b0a907af13d43f0a0cd2345f582dc8. The function above reproduces it exactly.
When TikTok returns a signature error, one of these is almost always the cause:
| Mistake | Why it breaks the signature |
|---|---|
| Re-serializing the JSON body after signing | The guide says to sign the exact bytes you send. Changed key order, whitespace or escaping produces a different hash. Serialize once, sign those bytes, send those bytes. |
| Putting the access token in the signature | For API version 202309 and later the token goes in the x-tts-access-token header and is not part of the signed string. Legacy access_token query parameters are excluded too. |
| Using plain SHA-256 | It must be HMAC-SHA256 keyed with the app secret. |
| Forgetting the path or the version segment | Sign the path exactly as it appears after the host. |
| Wrong timestamp | It must be a 10-digit Unix timestamp in seconds, within 5 minutes before to 30 seconds after the platform's clock. Milliseconds fail. |
| Wrong app key or secret | The most common cause overall: a key from one app with the secret from another. |
Note that shop_cipher, when an endpoint uses it, is a normal query parameter and is included in the signed string.
Your first call: Get Authorized Shops and shop_cipher
The recommended first call is Get Authorized Shops, GET /authorization/202309/shops. It tells you which shops the seller authorized for your app and returns the identifiers you need for everything else, including the encrypted shop_cipher.
import json, time, requests
from urllib.parse import urlencode
API_HOST = "https://open-api.tiktokglobalshop.com"
def call_api(method, path, app_key, app_secret, access_token,
query=None, body=None):
query = {"app_key": app_key, "timestamp": str(int(time.time())),
**(query or {})}
raw = b"" if body is None else json.dumps(body, separators=(",", ":")).encode()
url = f"{API_HOST}{path}?{urlencode(query)}"
query["sign"] = sign_request(url, raw, app_secret)
resp = requests.request(
method, f"{API_HOST}{path}", params=query, data=raw, timeout=20,
headers={"x-tts-access-token": access_token,
"content-type": "application/json"})
return resp.status_code, resp.json()
# First call: which shops did this seller authorize?
status, data = call_api("GET", "/authorization/202309/shops",
APP_KEY, APP_SECRET, access_token)
Business calls use the common parameters app_key, sign and timestamp in the query, plus the x-tts-access-token and content-type headers. The category segment of the path groups the API by business area:
| Area | Category segment | Example resources |
|---|---|---|
| Authorization | authorization |
shops |
| Product | product |
products, categories, inventory |
| Order | order |
orders |
| Fulfillment | fulfillment |
packages, shipping_documents |
| Return and refund | return_refund |
returns, refunds, cancellations |
| Logistics | logistics |
warehouses, delivery_options, shipping_providers |
| Finance | finance |
payments, settlements |
| Events (webhooks) | event |
webhooks |
The trap here is that shop_cipher is endpoint-specific. Some endpoints require it (missing it returns 106013) and others reject it as unnecessary (a 36009004 response whose message says it is not required). Do not guess: copy the requirement from each endpoint's reference page and keep a per-endpoint flag in your client wrapper.
TikTok Shop webhooks: setup, verification and retries
Webhooks push events to an HTTPS URL you register. You can configure them in Partner Center (App & Service, then your app, Basic Information, Developing) or per shop through the Events API with PUT /event/202309/webhooks and a body containing address and event_type. Both write to the same underlying configuration, so pick one source of truth. TikTok's webhook configuration guide sets the delivery rules:
| Requirement | Rule |
|---|---|
| URL | HTTPS, TLS 1.2 or later, a domain name (no IP address), no custom port |
| Acknowledgement | Return HTTP 200 with an empty body within 3 seconds |
| Rejecting a bad signature | Return 401. It counts as a failed delivery, not an acknowledgement |
| Delivery guarantee | At least once, so duplicates will happen |
| Retries | 2 minutes after the first failure, then 30 minutes, 3 hours and 12 hours after the previous failure. Retrying stops after the fourth retry fails |
Verifying the signature
The webhook signature is not the API request signature. TikTok puts it in the Authorization header (no Bearer prefix) as a lowercase hex HMAC-SHA256 digest of app_key followed by the raw request body, keyed with your app secret. Verify against the raw bytes before parsing, and compare in constant time.
import hashlib, hmac
def verify_webhook(app_key, app_secret, raw_body, authorization):
expected = hmac.new(app_secret.encode(), app_key.encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, (authorization or "").strip().lower())
The official example is reproducible: app key abcdef, app secret 123 and the sample order payload from the webhook overview produce 5dec0f11ec2f6783b8deee53c9ffbf8d024302f7c7e7fa55a35d17629031ac05. The function above matches it, and fails if a single byte of the body changes.
Events worth subscribing to
The event list is long, so match it to what your app does:
| event_type | Use it to |
|---|---|
ORDER_STATUS_CHANGE |
Sync new orders and status changes |
RECIPIENT_ADDRESS_UPDATE, PACKAGE_UPDATE |
Keep shipping and fulfillment data current |
CANCELLATION_STATUS_CHANGE, RETURN_STATUS_CHANGE, REVERSE_STATUS_UPDATE |
Handle cancellations, returns and buyer refund requests |
PRODUCT_STATUS_CHANGE, PRODUCT_AUDIT_STATUS_CHANGE, PRODUCT_CREATION, PRODUCT_INFORMATION_CHANGE, PRODUCT_CATEGORY_CHANGE |
Track listing state and audit results |
SELLER_DEAUTHORIZATION |
Stop calling the API for that shop and mark the connection inactive |
UPCOMING_AUTHORIZATION_EXPIRATION |
Warn the seller and send them through reauthorization. Sent 30 days before expiry, then daily until reauthorized |
Design the listener as authenticate, persist, acknowledge, then process asynchronously. Deduplicate on tts_notification_id, and do not branch only on the numeric type field, because the documentation does not publish a full numeric mapping for every topic. Use the subscribed event_type and the topic's payload schema instead. Most importantly, TikTok itself says not to treat webhooks as the only source of truth: keep a scheduled poll that reconciles orders, packages, products, returns and cancellations, so a missed delivery after the 12-hour retry ladder never becomes a lost order.
TikTok Shop API rate limits: what is documented and how to handle 429
You will find fixed numbers for TikTok Shop limits in third-party articles. Ignore them. TikTok's rate-limit page says the platform uses dynamic QPS allocation based on factors including the number of authorized shops, the type of endpoint and live platform load, and it does not expose a single fixed QPS figure. If an endpoint's reference page publishes its own limit, that one takes precedence.
What the documentation does give you is the isolation model and a starting point:
- The smallest isolation unit is app ID times authorized shop. The same app has separate capacity for each shop, and the same shop has separate capacity for each app, so one busy shop should not starve another shop's queue. Some endpoints are limited per app instead.
- More authorized shops generally means more total capacity, though not linearly and not instantly.
- A 429 is not always your fault: endpoint-level and platform-level protective throttling can trigger it even at low volume.
| Endpoint type | Suggested starting rate per app and shop |
|---|---|
| Heavy write or complex analytics | 0.2 to 1 request/second |
| Standard write | 1 to 3 requests/second (use idempotency before enabling retries) |
| Standard read or sync | 3 to 10 requests/second |
| Lightweight read or batch | 5 to 20 requests/second |
TikTok labels these as conservative starting points for client-side throttling, not guaranteed quotas. Ramp up by 20 to 30 percent every 10 to 15 minutes and stop when throttling appears.
Treat either HTTP 429 or business code 36009002 as rate limiting, and log both. A 503 is a different thing (service unavailable), so give it a short wait rather than a rate-limit penalty. The documented retry pattern is exponential backoff with jitter that never retries sooner than a Retry-After header. The docs give a 1 second base delay, a 60 second local cap and around 5 retries as example values, so tune them to your workload.
import random
def backoff_seconds(retry, retry_after=None, base=1.0, cap=60.0):
wait = min(base * (2 ** retry) + random.uniform(0, 0.5), cap)
return max(wait, retry_after) if retry_after is not None else wait
Two habits reduce throttling more than any backoff code: prefer batch endpoints (typically counted once per HTTP request, not per item, unless the endpoint says otherwise) and prefer incremental sync with change-time filters over full pulls. If your product connects many sellers, build one limiter per app-shop pair, and put each shop's requests on its own queue. Multi-seller scheduling like this is where most TikTok Shop integrations grow from a weekend script into a real system, and it is the kind of work covered by TikTok Shop API integration for social commerce sellers.
API versions: why 202309 matters
TikTok Shop versions each API separately, and a version name is the year and month it launched. New versions generally roll out monthly, but not every API changes every month, so different endpoints in your integration can legitimately sit on different versions.
- Since the 202309 style, the version is a path segment:
https://open-api.tiktokglobalshop.com/{category}/{version}/{resource}. Legacy APIs passedversionas a query parameter. - The access token moved from the query string to the
x-tts-access-tokenheader at 202309. - After a newer version ships, the previous one stays available for at least 2 months. Permanent retirement is announced in the changelog at least 2 months ahead.
- An invalid or retired version returns
36009014, although some gateway responses still show36009004with an invalid-version message. Check the message text as well as the code.
Versioning has business consequences too. TikTok Shop added an On Hold order status: for one hour after payment, buyers can cancel without seller approval, sellers cannot act on the order, and address fields come back empty. Only v202309 order responses include the is_on_hold_order field, and apps on legacy versions cannot reliably identify those orders. If an order sync still runs on a legacy version, that is the first thing to migrate.
Pin versions explicitly in your client, check the changelog on a schedule, and read the reference for the target version (path, headers, fields, scopes) before you upgrade one endpoint.
Common TikTok Shop API errors and what they mean
| Code | Meaning | First thing to check |
|---|---|---|
106001 |
Invalid sign |
Signed path, exact body bytes, timestamp, app secret |
105002 |
Access token expired | Run the refresh flow, then retry once |
105005 |
Access denied for missing scope | App scopes in Manage API, then the token's granted_scopes |
101000 |
Invalid query or header (token or cipher) | Token user_type for the endpoint, and whether the token belongs to the shop in shop_cipher |
106013 |
shop_cipher missing |
Get it from Get Authorized Shops |
36009004 |
Reused for many request-validation failures | Read the message: missing signature, bad token header, bad app key, timestamp window, unwanted shop_cipher |
36009002 |
Rate limited | Back off, honor Retry-After |
36009010 |
Invalid HTTP method | Use exactly the method on the endpoint reference |
36009014 |
Invalid API version | Version segment in the path |
36009033 |
IP not on the allow list | App and Service settings in Partner Center |
36004004 |
Invalid auth code | Code already used, older than 30 minutes, or wrong |
Two rules keep debugging short. Never branch programmatically on 36009004 alone; combine it with the message keyword. And log the full response with its request_id every time, because that is the identifier TikTok support needs.
Production checklist for a TikTok Shop integration
- Store
app_secret, access tokens and refresh tokens encrypted, and scrub token-endpoint URLs from logs. - Refresh tokens on a schedule using stored expiry timestamps; track each shop's authorization end date separately.
- Serialize request bodies once and sign the same bytes you send.
- Keep a per-endpoint config for
shop_cipher, HTTP method and API version. - Run one rate limiter and queue per app-shop pair, use batch endpoints, and back off on both 429 and
36009002. - Verify webhook signatures on the raw body, answer 200 fast, persist first, process later, and deduplicate on
tts_notification_id. - Subscribe to
SELLER_DEAUTHORIZATIONandUPCOMING_AUTHORIZATION_EXPIRATIONso shop access never dies silently. - Reconcile orders, packages, products and returns by scheduled polling, so no webhook is the single source of truth.
- Watch the changelog for version retirements and new required fields.
If you would rather have this built and maintained than assemble it yourself, you can request a free quote and describe the TikTok Shop workflow you need.
CÂU HỎI THƯỜNG GẶP
Câu hỏi thường gặp
How do I get access to the TikTok Shop API?
Register in TikTok Shop Partner Center, create an app under App & Service, and enable the API scopes your features need. The app detail page gives you the app_key, app_secret and service_id. Each seller then authorizes your app through an authorization link, and you exchange the resulting auth_code for an access token and refresh token.
How long does a TikTok Shop access token last?
The default validity of an access token is 7 days, and the response includes access_token_expire_in as a Unix timestamp. The refresh token expires when the authorization duration the seller granted ends. Refresh on a schedule before expiry using the refresh endpoint at auth.tiktok-shops.com.
Why does the TikTok Shop API say my signature is invalid?
The usual causes are a wrong app key or secret, including the access token in the signed string, re-serializing the JSON body after signing, leaving out the request path, using plain SHA-256 instead of HMAC-SHA256, or a timestamp outside the window of 5 minutes before to 30 seconds after the platform's clock. Sign the exact bytes you send.
What are the TikTok Shop API rate limits?
There is no single fixed limit. TikTok uses dynamic QPS allocation based on factors such as the number of authorized shops, endpoint type and platform load, and endpoint reference pages take precedence when they state a limit. Rate limiting appears as HTTP 429 or business code 36009002. Use per app-shop queues, batch endpoints, and exponential backoff with jitter that honors Retry-After.
Should I use TikTok Shop webhooks or polling?
Use both. Webhooks give near real-time events, but delivery is at least once, must be acknowledged with an empty 200 within 3 seconds, and retrying stops after the fourth retry failure. TikTok recommends reconciling critical data such as orders, packages, products and returns with scheduled API polling.