TikTok Shop

TikTok Shop Open API 202309: shop authorisation, HMAC-SHA256 signing, shop_cipher. Reads orders, products, packages and finance, writes price and stock.

TikTok Shop is the in-app marketplace inside TikTok, covering live shopping, short video product tags, the Shop tab and affiliate creator selling. It is the fastest growing new sales channel in this catalogue and it behaves unlike any of the others: orders originate from content rather than from search, a large share of volume flows through creators taking commission, and the fulfilment model is often TikTok's own logistics rather than the seller's. The developer surface is the Partner Center Open API, a signed REST API with a version stamped into every path. This page covers the 202309 versions of the Order, Product, Fulfillment, Return and Refund and Finance APIs, which are the current stable set for a seller integration.

At a glance

Warning

Nothing on this page was read from partner.tiktokshop.com itself. Every path, parameter and algorithm comes from EcomPHP/tiktokshop-php, an open source client that targets API version 202309 and whose source was read directly on 2026-09-21. Paths and the signing algorithm are quoted from that source and are reliable. Sample response bodies are reconstructed from the platform's published field names, not copied from official examples, and must be confirmed against Partner Center before a loader is written against them.

What it is

TikTok Shop launched in Indonesia and the United Kingdom in 2021 and has since opened in the United States, Vietnam, Thailand, Malaysia, the Philippines, Singapore, Spain, Ireland, Germany, France, Italy, Japan, Mexico and Brazil, among others. Each country is a separate shop with separate onboarding, separate compliance and, usually, a separate seller entity.

The channel is social commerce rather than search commerce. A product sells because a creator featured it in a video or a live stream, so the demand signal is spiky in a way that marketplace inventory systems are not designed for, and a stock connector that syncs nightly will oversell during a live event. The affiliate programme is central: creators pick products from an open marketplace, sell them for a commission, and the commission comes out of the seller's settlement. Any settlement mapping that ignores affiliate commission will misstate margin badly.

Two facts matter for an Indian brand. TikTok has been banned in India since June 2020, so there is no TikTok Shop India and no domestic route. An Indian brand on TikTok Shop is therefore selling cross border, most commonly into the US or the UK, either through a local entity or through TikTok's cross border seller programme. Cross border shops are exactly the case where the shop_cipher parameter described below becomes mandatory, so this is not an edge case for our data set, it is the normal case.

Indonesia is worth noting separately. TikTok Shop Indonesia was closed by regulation in October 2023 and returned through a merger with Tokopedia under GoTo, which means Indonesian TikTok Shop data may sit behind Tokopedia's systems rather than the standard Open API. Confidence low on the current Indonesian arrangement.

API access

  1. Register a developer account at Partner Center. This requires company details and is reviewed.
  2. Create an app. The app type is the important choice: a marketplace app can be installed by any seller from the TikTok Shop App Store and is the route for a multi tenant connector; a custom app is bound to the developer's own shops and is the route for a brand integrating its own store. Custom apps skip the app review that marketplace apps must pass.
  3. The app is issued an app key and an app secret. These are the durable credentials and they sign every request.
  4. Request the API scopes the app needs. Scope groups follow the API families: order, product, fulfilment, return and refund, finance, logistics, promotion, analytics, affiliate. Scopes are granted per app and shown to the seller at authorisation.
  5. Each seller authorises the app for one or more shops, producing an authorisation code that is exchanged for an access token.

Versioning is unusual and worth understanding before writing a client. The version is a path segment between the API family and the resource, so the same resource exists at several versions simultaneously. 202309 is the current stable set for the core seller APIs. A client should hold the version as configuration, not as a constant, because TikTok ships new dated versions alongside old ones and deprecates the old ones on announced dates rather than breaking them in place.

Official SDKs: TikTok publishes a Postman collection and an API explorer in Partner Center. First party SDKs were not verifiable from outside the portal. The open source clients in PHP, Python, Node and Go are community maintained; the PHP one used as the source for this page tracks 202309.

Base URL for every API call: https://open-api.tiktokglobalshop.com/

Authorisation and token endpoints sit on a different host: https://auth.tiktok-shops.com/

Authentication

There are three separate secrets in play and confusing them is the usual cause of a failed first integration. The app secret signs requests and never travels in a request. The access token identifies the authorised seller and travels in a header. The shop cipher identifies which of the seller's shops a call applies to and travels as a query parameter.

Step 1: send the seller to the authorisation page

GET https://auth.tiktok-shops.com/oauth/authorize
  ?app_key={app_key}
  &state={nonce}

The seller signs in, picks the shops to authorise and approves the scope list. TikTok redirects to the app's configured callback URL with an auth_code and the state we sent. Verify state against the stored nonce before proceeding.

Step 2: exchange the code for tokens

curl -X GET "https://auth.tiktok-shops.com/api/v2/token/get\
?app_key=$APP_KEY\
&app_secret=$APP_SECRET\
&auth_code=$AUTH_CODE\
&grant_type=authorized_code"

Note the details, all confirmed from the client source on 2026-09-21: the grant type string is authorized_code, not the usual authorization_code; the app secret is sent as a query parameter rather than in a Basic authorisation header; and this call is not signed, unlike every Open API call.

{
  "code": 0,
  "message": "success",
  "request_id": "20260921103311ABCDEF0123456789",
  "data": {
    "access_token": "ROW_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "access_token_expire_in": 1758999999,
    "refresh_token": "ROW_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "refresh_token_expire_in": 1790450000,
    "open_id": "7020000000000000000",
    "seller_name": "Example Brand",
    "seller_base_region": "US",
    "granted_scopes": ["order.info", "product.info"]
  }
}

Sample reconstructed from published field names, medium confidence.

Warning

access_token_expire_in and refresh_token_expire_in are absolute Unix timestamps of the expiry moment, not durations in seconds. Treating them as a number of seconds to add to now, which is what every other OAuth implementation in this catalogue would have you do, produces tokens that appear to be valid for fifty years. Compare them against the current epoch directly.

Lifetimes are in the order of days for the access token and a year for the refresh token. Confidence medium on the exact figures, which were not readable from the portal; read them from the returned timestamps at runtime rather than hard coding them.

Step 3: refresh

curl -X GET "https://auth.tiktok-shops.com/api/v2/token/refresh\
?app_key=$APP_KEY\
&app_secret=$APP_SECRET\
&refresh_token=$REFRESH_TOKEN\
&grant_type=refresh_token"

The refresh response returns a new refresh token as well as a new access token. Persist both. Because the access token life is measured in days rather than months, a scheduled refresh job is mandatory; a connector that only refreshes on a 401 will fail the first call of every batch.

Step 4: resolve the shop cipher

An access token belongs to a seller, which may hold several shops. Calls are addressed to one shop, identified by a shop_cipher, an opaque string. Fetch it once after authorisation and store it against the connection.

GET https://open-api.tiktokglobalshop.com/authorization/202309/shops

GET /seller/202309/shops returns the active shop list and GET /seller/202309/permissions returns the granted permissions. Both confirmed from the client source.

shop_cipher is required on most calls and must be omitted from a few, notably the authorisation and seller calls that discover it in the first place. It also participates in the signature, so it cannot be appended after signing.

Step 5: sign every request

This is the part that has no analogue elsewhere in this catalogue. The algorithm, read directly from the client source on 2026-09-21:

  1. Take all query parameters, excluding sign, access_token and x-tts-access-token, and sort them alphabetically by key.
  2. Concatenate them as {key}{value} with no separators.
  3. Prepend the request path, that is the URI path only, without the host or the query string.
  4. If the request is not a GET and the content type is not multipart, append the raw request body.
  5. Wrap the whole string with the app secret at both ends: app_secret + string + app_secret.
  6. Compute HMAC-SHA256 over that string using the app secret as the key, and hex encode it. That value is the sign query parameter.

Common parameters added to every call: app_key, timestamp as a Unix epoch in seconds, shop_cipher where applicable, version where the client carries it as a parameter, and sign. Required headers: x-tts-access-token with the access token, and content-type, defaulting to application/json.

A worked example of the string to sign for a product search:

{app_secret}/product/202309/products/searchapp_key{app_key}page_size50shop_cipher{cipher}timestamp1758456000{"status":"ACTIVE"}{app_secret}

The app secret appears at both ends, the path comes before the sorted parameters, and the JSON body is appended verbatim. Sign the exact bytes that will be sent; re-serialising the body after signing changes the hash.

An authenticated call

curl -X POST "https://open-api.tiktokglobalshop.com/order/202309/orders/search\
?app_key=$APP_KEY&timestamp=1758456000&shop_cipher=$SHOP_CIPHER&page_size=50&sign=$SIGN" \
  -H "x-tts-access-token: $ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -d '{"order_status":"AWAITING_SHIPMENT"}'

Multi account

One app serves many sellers. Each seller has its own access token and refresh token, and each shop under that seller has its own cipher. Our connection row is therefore a triple of app, refresh token and shop cipher, and a seller with a US shop and a UK shop is two rows sharing one token.

Objects we can read

Every path below is https://open-api.tiktokglobalshop.com/{family}/{version}/{resource}, with {version} currently 202309. The family prefixes, confirmed from the client source, are order, product, fulfillment, return_refund, finance, logistics, authorization, seller, promotion, event and analytics.

Orders

The external order reference endpoints are unusual and useful: they let a connector write its own identifier onto a TikTok order and search by it later, which removes the need for a local mapping table.

POST /orders/search takes page_size and page_token as query parameters, with sorting via sort_field and sort_order, and the filter in the request body. Body filters include the order status, creation time bounds and update time bounds. For an incremental sync, filter on the update time bound rather than the creation time.

Pagination is token based: the response carries next_page_token, which is passed back as page_token. An empty or absent token means the end. Token pagination is stable across a changing result set, unlike offset pagination, so this is one of the better paging models in the catalogue.

{
  "code": 0,
  "message": "Success",
  "request_id": "20260921103311ABCDEF0123456789",
  "data": {
    "next_page_token": "b2Zmc2V0PTUw",
    "total_count": 1,
    "orders": [
      {
        "id": "576462281664466148",
        "status": "AWAITING_SHIPMENT",
        "create_time": 1758440000,
        "update_time": 1758440600,
        "paid_time": 1758440100,
        "buyer_email": "g8b***@tiktok.com",
        "buyer_message": "",
        "shipping_type": "TIKTOK",
        "fulfillment_type": "FULFILLMENT_BY_SELLER",
        "payment_method_name": "Credit Card",
        "is_cod": false,
        "warehouse_id": "7010000000000000000",
        "payment": {
          "currency": "USD",
          "sub_total": "20.00",
          "shipping_fee": "5.00",
          "seller_discount": "1.00",
          "platform_discount": "0.00",
          "tax": "1.65",
          "total_amount": "25.65",
          "original_total_product_price": "20.00",
          "original_shipping_fee": "5.00"
        },
        "recipient_address": {
          "name": "J*** D***",
          "phone_number": "(+1)123***4567",
          "full_address": "123 Main St, Brooklyn, NY 11205",
          "region_code": "US",
          "postal_code": "11205",
          "district_info": [
            { "address_level": "L1", "address_level_name": "State", "address_name": "New York" }
          ]
        },
        "packages": [{ "id": "1153342283122016801" }],
        "line_items": []
      }
    ]
  }
}

Sample reconstructed from published field names, medium confidence. Points for the loader:

  • Every response is wrapped in code, message, request_id and data. code of 0 means success. A non zero code arrives with HTTP 200, so a client that only checks the HTTP status will treat every business error as a success. Check code on every call.
  • Timestamps are Unix epoch seconds, not milliseconds.
  • All money values are decimal strings, not numbers. Parse them as decimals, never as floats.
  • recipient_address is masked. The buyer name and phone arrive partially redacted and the email is a TikTok relay address. Unmasked address data is released only to the fulfilment flow and, on some markets, only for a limited window after the order. Treat what arrives as PII anyway, and do not expect to build a customer identity from it.
  • shipping_type distinguishes TikTok managed shipping from seller shipping; fulfillment_type distinguishes seller fulfilment from TikTok's own. Both are needed for our fulfilment_type.
  • Order statuses run UNPAID, ON_HOLD, AWAITING_SHIPMENT, PARTIALLY_SHIPPING, AWAITING_COLLECTION, IN_TRANSIT, DELIVERED, COMPLETED and CANCELLED. Confidence medium on the exact enumeration.

Order items

line_items[] on the order. Each line carries its own identifier, the product and SKU identifiers, the seller's own SKU code, the sale and original prices, the discounts split between seller funded and platform funded, the package the line was assigned to, and its own display status.

{
  "id": "576462281664466149",
  "product_id": "1729592969712207000",
  "product_name": "Organic Cotton T Shirt",
  "sku_id": "1729592969712300000",
  "sku_name": "Black, M",
  "seller_sku": "TSHIRT-BLK-M",
  "sku_image": "https://p16-oec.ibyteimg.com/example.jpeg",
  "currency": "USD",
  "original_price": "20.00",
  "sale_price": "19.00",
  "seller_discount": "1.00",
  "platform_discount": "0.00",
  "display_status": "AWAITING_SHIPMENT",
  "package_id": "1153342283122016801",
  "package_status": "TO_FULFILL",
  "tracking_number": "",
  "shipping_provider_name": "",
  "item_tax": [{ "tax_type": "SALES_TAX", "tax_amount": "1.65", "tax_rate": "0.0825" }]
}

Three identifier levels matter: product_id is the listing, sku_id is the variant, and seller_sku is the seller's own code, which is what joins to our products table. seller_sku can be empty if the seller never set one.

Note that quantity is not a field on the line. One line_items[] entry is one unit, so an order of three of the same variant appears as three entries with three different id values. Group by sku_id to get a quantity, and do not assume the array length equals the number of distinct products.

Products and listings

All paths confirmed from the client source on 2026-09-21.

The compliance endpoints are not optional background detail. Several markets, notably the European Union under the General Product Safety Regulation, require a responsible person and manufacturer record attached to a listing before it can go live. A connector that creates listings into an EU shop needs those records first.

{
  "code": 0,
  "message": "Success",
  "data": {
    "next_page_token": "",
    "total_count": 1,
    "products": [
      {
        "id": "1729592969712207000",
        "title": "Organic Cotton T Shirt",
        "status": "ACTIVE",
        "create_time": 1750000000,
        "update_time": 1758000000,
        "skus": [
          {
            "id": "1729592969712300000",
            "seller_sku": "TSHIRT-BLK-M",
            "price": { "currency": "USD", "sale_price": "19.00", "tax_exclusive_price": "19.00" },
            "inventory": [{ "warehouse_id": "7010000000000000000", "quantity": 24 }]
          }
        ]
      }
    ]
  }
}

Sample reconstructed from published field names, medium confidence. Product status values include ACTIVE, DRAFT, PENDING, FAILED, SELLER_DEACTIVATED, PLATFORM_DEACTIVATED, FREEZE and DELETED; confidence medium on the exact list. The distinction between seller deactivated and platform deactivated matters: one is the seller's choice, the other is an enforcement action, and only the second should raise an alert.

Inventory

Stock lives on the SKU, per warehouse, as skus[].inventory[] {warehouse_id, quantity}. There is no separate inventory read endpoint for a single SKU; use POST /product/202309/inventory/search for a bulk view or read it from the product.

Warehouses are configured in Seller Center and listed through the Logistics API. A seller using TikTok's own fulfilment has a warehouse per TikTok fulfilment centre, and that stock is reported but not writable by the seller.

Shipments and tracking

The Fulfillment API is the richest part of this surface, because TikTok models packages as first class objects separate from orders. An order can be split into several packages, and several orders can be combined into one package.

All confirmed from the client source. GET /orders/{order_id}/tracking is the one that matters for our shipments.events[]: it returns the carrier scan history for an order, which most marketplaces in this catalogue do not provide at all.

Returns and cancellations

The Return and Refund API covers both, under the return_refund family. Cancellations are pre-shipment, returns are post-delivery, and they are separate object types with separate endpoints.

All confirmed from the client source. POST /refunds/calculate is worth noting: it returns what a proposed refund would actually cost the seller after platform contributions, before committing to it.

Return records carry a return identifier, the order it belongs to, a return type distinguishing refund only from return and refund from replacement, a status, the buyer's reason, the refund amount broken into subtotal, shipping and tax, and the line items being returned. Confidence medium on the field names; the return schema was not readable from the portal.

Payments and settlements

All confirmed from the client source. The model is three layered and maps cleanly onto our settlements table: a statement is a settlement period, its transactions are the per order detail, and a withdrawal is the money actually leaving TikTok for the seller's bank.

The per order endpoint is the one to use for joining revenue to orders. It returns the revenue, the fees and the net settlement for a single order without downloading a whole statement, which makes order level margin reporting cheap.

Fee categories to expect include the platform commission, a transaction or payment fee, affiliate commission where a creator drove the sale, shipping fee contributions in both directions, and promotional co-funding. Affiliate commission is the one that distinguishes TikTok Shop from every other channel here and it must be carried into settlements.fees[] as its own type rather than folded into commission.

Customers

No customer endpoint, and less customer data than any other marketplace in this catalogue. What exists is a masked recipient name, a masked phone number, a relay email, and the address. There is no stable buyer identifier exposed to the seller, so customers.customer_id cannot be populated reliably and repeat purchase analysis is not possible from this channel alone.

Locations

Warehouses come from the Logistics API family, logistics, and are referenced by warehouse_id on inventory records and on orders. A shop has at least one warehouse; cross border sellers usually have one per origin country plus any TikTok fulfilment centres in use.

Writing back: listings, price and stock

Price

POST https://open-api.tiktokglobalshop.com/product/202309/products/{product_id}/prices/update
x-tts-access-token: {token}
content-type: application/json

{
  "skus": [
    { "id": "1729592969712300000", "price": { "amount": "17.99", "currency": "USD" } }
  ]
}

Prices are set per SKU, in a batch scoped to one product. The response is synchronous and reports per SKU success or failure, so no polling is needed. Request body shape reconstructed, medium confidence; the path is confirmed.

Stock

POST https://open-api.tiktokglobalshop.com/product/202309/products/{product_id}/inventory/update
x-tts-access-token: {token}
content-type: application/json

{
  "skus": [
    {
      "id": "1729592969712300000",
      "inventory": [{ "warehouse_id": "7010000000000000000", "quantity": 24 }]
    }
  ]
}

Quantity is absolute, not a delta, and is set per warehouse. Both write endpoints are scoped to a single product, so a catalogue wide price change is one call per product rather than one call per batch of SKUs. That is the main throughput constraint on write back here.

Warning

Live selling breaks nightly stock syncs. A creator live stream can sell several hundred units of one SKU in minutes, so a connector that pushes stock once a day will oversell. For any seller who runs live events, push stock on every inbound order event and subscribe to the order status webhook rather than relying on a schedule.

Listing content

Full listing lifecycle is available: POST /products creates, PUT /products/{id} replaces, POST /products/{id}/partial_edit changes a subset, and activate, deactivate, delete and recover manage state. Images must be uploaded first through POST /product/202309/images/upload, which returns an image identifier used in the product body; external image URLs are not accepted.

Before creating, call GET /product/202309/prerequisites to check the shop is eligible to list, POST /categories/recommend to resolve a category from the title, and GET /categories/{category_id}/attributes and /rules to discover the required attributes and any category restrictions. Building a listing without reading the category rules first is the most common source of create failures.

Webhooks and notifications

Webhooks are registered per shop, per event type, through the event family.

All confirmed from the client source. Registration takes the event type and the destination URL, and the API is idempotent per event type: registering the same event type again replaces the destination rather than adding a second one.

Verification

The signature algorithm, read directly from the client source on 2026-09-21, is different from the request signing algorithm and simpler:

  1. Read the signature from the Authorization header of the incoming request. It is not in a custom X- header.
  2. Build the string to sign as app_key + raw_request_body, using the exact raw bytes received.
  3. Compute HMAC-SHA256 over that string with the app secret as the key, hex encoded.
  4. Compare with a constant time equality check, not string equality.

The delivered body carries four properties: type, the event type; shop_id, the shop the event belongs to; timestamp; and data, the event specific body.

{
  "type": "ORDER_STATUS_UPDATE",
  "shop_id": "7010000000000000000",
  "timestamp": 1758440600,
  "data": {
    "order_id": "576462281664466148",
    "order_status": "AWAITING_SHIPMENT",
    "update_time": 1758440600
  }
}

ORDER_STATUS_UPDATE is the event type constant confirmed in the client source. The full catalogue, which covers reverse order status, package updates, recipient address changes, product status changes, seller deactivation and upcoming authorisation expiry, is published in Partner Center and could not be read on 2026-09-21. Confidence low on the complete list.

Note

Subscribe to the authorisation expiry event if it is available. Because TikTok access tokens expire in days rather than months, an expiry warning event is worth more here than on any other platform in this catalogue.

Retry policy is not published to us. Assume at least once delivery, deduplicate on the event identifier or on the resource identifier plus timestamp, and respond quickly with a 200 before doing any work.

Recommended polling alongside webhooks: orders on the update time filter every 5 to 15 minutes, products nightly, returns and cancellations hourly, statements daily.

Rate limits and pagination

TikTok Shop publishes per API rate limits in Partner Center, expressed as queries per second per app and per shop. Those figures could not be read on 2026-09-21 and are not reproduced here rather than guessed. Pull them from Partner Center before setting the connector's limiter.

What can be stated: errors arrive as a non zero code inside an HTTP 200 body, so rate limit rejections are invisible to HTTP level retry logic and must be detected from the business code. Limits are enforced per app as well as per shop, so a multi tenant connector shares one pool across sellers and must schedule centrally. Every call is an HMAC over a string that includes the body, so re-signing on retry with a new timestamp is fine but caching a signature across calls is not.

Pagination is token based across the search endpoints: pass page_size, read next_page_token from the response, pass it back as page_token, and stop when it is empty. This is stable across concurrent writes, which is why date windows can be wider here than on the offset paginated platforms.

Mapping to the unified model

orders

order_items

listings and inventory

shipments, returns and settlements

Gaps and open questions

  • Nothing was read from partner.tiktokshop.com. Paths, the two signing algorithms and the authorisation endpoints come from an open source client targeting 202309 and are reliable; sample response bodies are reconstructed and must be confirmed.
  • Exact access token and refresh token lifetimes were not readable. Read them from the returned absolute expiry timestamps rather than assuming.
  • Published rate limits could not be read and are deliberately not reproduced. They must be pulled from Partner Center.
  • The full webhook event type catalogue could not be read. Only ORDER_STATUS_UPDATE is confirmed. Retry policy and delivery guarantees are unpublished to us.
  • How long unmasked recipient data stays available, and whether it requires a separate permission, is unverified. This determines whether address data can be stored at all under our retention rules.
  • The return and refund object schema, and the order and product status enumerations, are medium confidence.
  • Sandbox coverage per API is unverified. Whether the sandbox supports the full order lifecycle including settlement is unknown.
  • The Indonesian market operates under the Tokopedia merger and may not be reachable through the standard Open API. Confidence low.
  • Affiliate and creator data has its own API family and its own scopes and is not covered here. It needs a separate pass, because affiliate commission is a material cost line that the finance endpoints only report after the fact.

Sources

Official, but returned no usable content to our fetcher on 2026-09-21 and listed for the human reader: TikTok Shop Partner Center, the authorisation documentation page and the Open API overview page. Both documentation pages rendered as an empty JavaScript shell.

Read successfully, all on 2026-09-21: