Chuyển tới nội dung chính

Ký yêu cầu API của bạn

Giới thiệu

Khi gọi một TikTok Shop API, bạn phải đính kèm chữ ký (signature) để xác thực yêu cầu đúng cách. Mọi yêu cầu không chứa chữ ký, hoặc có chữ ký không hợp lệ, sẽ bị từ chối truy cập.

TikTok Shop API sử dụng HMAC-SHA256 làm thuật toán mặc định để tạo chữ ký.

HS256 (HMAC với SHA-256) là một thuật toán đối xứng, nghĩa là chỉ có một khóa riêng (private key) được chia sẻ giữa hai bên. Vì cùng một khóa được dùng để vừa tạo vừa xác thực chữ ký, cần thận trọng để đảm bảo khóa không bị lộ.

Khi TikTok Shop nhận một yêu cầu đã xác thực, nó tái tạo lại chữ ký bằng thông tin xác thực chứa trong yêu cầu. Nếu các chữ ký khớp nhau, dịch vụ sẽ xử lý yêu cầu. Nếu không, nó sẽ từ chối yêu cầu.

Quan trọng: TikTok Shop API sử dụng app secret của bạn làm khóa riêng để xác thực. Bạn có thể lấy app secret từ trang chi tiết app của mình trong Partner Center.

Image

Mã mẫu

Bạn có thể tìm thấy mã mẫu bằng GoLang, Java và Node.js bên dưới. Với các mã mẫu này, bạn không cần đào sâu vào thuật toán. Nếu bạn cần mã mẫu bằng ngôn ngữ khác, vui lòng bôi đen câu và gửi phản hồi cho chúng tôi.

Signature algorithm (Go)

GOWord Wrap

import (    
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"sort"
)

// secret: App secret
func CalSign(req *http.Request, secret string) string {
queries := req.URL.Query()

// extract all query parameters excluding sign and access_token
keys := make([]string, len(queries))
idx := 0
for k := range queries {
// params except 'sign' & 'access_token'
if k != "sign" && k != "access_token" {
keys[idx] = k
idx++
}
}

// reorder the parameters' key in alphabetical order
sort.Slice(keys, func(i, j int) bool {
return keys[i] < keys[j]
})

// Concatenate all the parameters in the format of {key}{value}
input := ""
for _, key := range keys {
input = input + key + queries.Get(key)
}

// append the request path
input = req.URL.Path + input

// if the request header Content-type is not multipart/form-data, append body to the end
mediaType, _, _ := mime.ParseMediaType(req.Header.Get("Content-type"))
if mediaType != "multipart/form-data" {
body, _ := io.ReadAll(req.Body)
input = input + string(body)

req.Body.Close()
// reset body after reading from the original
req.Body = io.NopCloser(bytes.NewReader(body))
}

// wrap the string generated in step 5 with the App secret
input = secret + input + secret

return generateSHA256(input, secret)
}

func generateSHA256(input, secret string) string {
// encode the digest byte stream in hexadecimal and use sha256 to generate sign with salt(secret)
h := hmac.New(sha256.New, []byte(secret))

if _, err := h.Write([]byte(input)); err != nil {
// TODO error log
return ""
}

return hex.EncodeToString(h.Sum(nil))
}

Signature algorithm (Java)

JAVAWord Wrap

package org.example;  

import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.RequestBody;
import okio.Buffer;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {
public String generateSignature(Request request, String secret) {
HttpUrl httpUrl = request.url();
List<String> parameterNameList = new ArrayList<>(httpUrl.queryParameterNames());

// extract all query parameters excluding sign and access_token
parameterNameList.removeIf(param -> "sign".equals(param) || "access_token".equals(param));

// reorder the parameters' key in alphabetical order
Collections.sort(parameterNameList);

// append the request path
StringBuilder parameterStr = new StringBuilder(httpUrl.encodedPath());
for (String parameterName : parameterNameList) {
// Concatenate all the parameters in the format of {key}{value}
parameterStr.append(parameterName).append(httpUrl.queryParameter(parameterName));
}

// if the request header Content-type is not multipart/form-data, append body to the end
String contentType = request.header("Content-Type");
if (!"multipart/form-data".equalsIgnoreCase(contentType)) {
try {
RequestBody requestBody = request.body();
if (requestBody != null) {
Buffer bodyBuffer = new Buffer();
requestBody.writeTo(bodyBuffer);
byte[] bodyBytes = bodyBuffer.readByteArray();
parameterStr.append(new String(bodyBytes, StandardCharsets.UTF_8));
}
} catch (Exception e) {
throw new RuntimeException("failed to generate signature params", e);
}
}

// wrap the string generated in step 5 with the App secret
String signatureParams = secret + parameterStr + secret;

// encode wrapped string using HMAC-SHA256
return generateSHA256(signatureParams, secret);
}

/**
* generate signature by SHA256
* @param signatureParams signature params
* @return signature
*/
public String generateSHA256(String signatureParams, String secret) {
try {
// Get an HmacSHA256 Mac instance and initialize with the secret key
Mac sha256HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256HMAC.init(secretKeySpec);

// Update with input data
sha256HMAC.update(signatureParams.getBytes(StandardCharsets.UTF_8));

// Compute the HMAC and get the result
byte[] hashBytes = sha256HMAC.doFinal();

// Convert to hex string
StringBuilder sb = new StringBuilder();
for (byte hashByte : hashBytes) {
sb.append(String.format("%02x", hashByte & 0xff));
}

return sb.toString();
} catch (Exception e) {
throw new RuntimeException("failed to generate signature result", e);
}
}
}

Signature algorithm (Node.js)

TYPESCRIPTWord Wrap

import crypto from "crypto";  
import localVarRequest from "request";
const excludeKeys = ["access_token", "sign"] as const;
export const generateSign = (
requestOption: localVarRequest.Options,
app_secret: string
) => {
let signString = "";
// step1: Extract all query parameters excluding sign and access_token. Reorder the parameter keys in alphabetical order:
const params = requestOption.qs || {};
const sortedParams = Object.keys(params)
.filter((key) => !excludeKeys.includes(key as any))
.sort()
.map((key) => ({ key, value: params[key] }));
//step2: Concatenate all the parameters in the format {key}{value}:
const paramString = sortedParams
.map(({ key, value }) => `${key}${value}`)
.join("");

signString += paramString;

//step3: Append the string from Step 2 to the API request path:
// @ts-ignore
const pathname = new URL(requestOption!.uri!||'').pathname;

signString = `${pathname}${paramString}`;

//step4: If the request header content-type is not multipart/form-data, append the API request body to the string from Step 3:
if (
requestOption.headers?.["content-type"] !== "multipart/form-data" &&
requestOption.body &&
Object.keys(requestOption.body).length
) {
const body = JSON.stringify(requestOption.body);
signString += body;
}

//step5: Wrap the string generated in Step 4 with the app_secret:
signString = `${app_secret}${signString}${app_secret}`;

//step6: Encode your wrapped string using HMAC-SHA256:
const hmac = crypto.createHmac("sha256", app_secret);
hmac.update(signString);
const sign = hmac.digest("hex");

return sign;
};

Signature algorithm (Python)

PYTHONWord Wrap

import hmac  
import hashlib
from urllib.parse import urlparse
import json

def generate_sign(request_option, app_secret):
"""
Generate HMAC-SHA256 signature
:param request_option: Request options dictionary containing qs (query params), uri (path), headers, body etc.
:param app_secret: Secret key for signing
:return: Hexadecimal signature string
"""
# Step 1: Extract and filter query parameters, exclude "access_token" and "sign", sort alphabetically
params = request_option.get('qs', {})
exclude_keys = ["access_token", "sign"]
sorted_params = [
{"key": key, "value": params[key]}
for key in sorted(params.keys())
if key not in exclude_keys
]

# Step 2: Concatenate parameters in {key}{value} format
param_string = ''.join([f"{item['key']}{item['value']}" for item in sorted_params])
sign_string = param_string

# Step 3: Append API request path to the signature string
uri = request_option.get('uri', '')
pathname = urlparse(uri).path if uri else ''
sign_string = f"{pathname}{param_string}"

# Step 4: If not multipart/form-data and request body exists, append JSON-serialized body
content_type = request_option.get('headers', {}).get('content-type', '')
body = request_option.get('body', {})
if content_type != 'multipart/form-data' and body:
body_str = json.dumps(body) # JSON serialization ensures consistency
sign_string += body_str

# Step 5: Wrap signature string with app_secret
wrapped_string = f"{app_secret}{sign_string}{app_secret}"

# Step 6: Encode using HMAC-SHA256 and generate hexadecimal signature
hmac_obj = hmac.new(
app_secret.encode('utf-8'),
wrapped_string.encode('utf-8'),
hashlib.sha256
)
sign = hmac_obj.hexdigest()
return sign

Phân tích chi tiết từng bước

Dưới đây là phân tích chi tiết từng bước về cách thuật toán chữ ký hoạt động:

📌 Lưu ý: Bạn có thể bỏ qua và đi thẳng đến các phần signature algorithm nếu không cần phần phân tích từng bước. Chúng tôi đã cung cấp mã mẫu bằng [Go](#Signature algorithm (Go)), [Java](#Signature algorithm (Java)) và [Node.js](#Signature algorithm (Node.js))

  1. Lấy ví dụ, giả sử bạn muốn gọi endpoint Get Authorized Shopsapp_secret của bạn là e59af819cc:

HTTPWord Wrap

curl -X GET \  
'https://open-api.tiktokglobalshop.com/authorization/202309/shops?app_key=29a39d&sign=bc721f0e0182914e3487b81df204de37a352fc3aa96947efda6dc1e5dd0d5290&timestamp=1623812664' \
-H 'x-tts-access-token: TTP_pwSm2AAAAABmmtFz1xlyKMnwg74T2GJ5s0uQbS8jPjb_GkdFVCxPqzQXSyuyfXdQa0AqyDsea2tYFNVf4XeqgZHFfPyv0Vs659QqyLYfsGzanZ5XZAin3_ZkcIxxS0_In6u6XDeU96k' \
-H 'content-type: application/json'
  1. Trích xuất tất cả query parameter ngoại trừ signaccess_token. Sắp xếp lại các khóa parameter theo thứ tự bảng chữ cái:

GOWord Wrap

keys := make([]string, len(queries))      
idx := 0
for k := range queries {
// params except 'sign' & 'access_token'
if k != "sign" && k != "access_token" {
keys[idx] = k
idx++
}
}
sort.Slice(keys, func(i, j int) bool {
return keys[i] < keys[j]
})

Các khóa query đã được sắp xếp lại là:

PLAINTEXTWord Wrap

keys = []string{      
"app_key",
"timestamp"
}

📌 Lưu ý: Một số API endpoint sẽ yêu cầu query parameter shop_cipher, khi đó nó sẽ được đưa vào danh sách khóa query đã sắp xếp lại. Vui lòng gọi Get Authorized Shops để lấy shop_cipher tương ứng của một shop.

  1. Nối tất cả các parameter theo định dạng {key}{value}:

GOWord Wrap

// Concatenate all the parameters in the format of {key}{value}    
input := ""
for _, key := range keys {
input = input + key + queries[key]
}

Chuỗi kết quả sẽ là:

PLAINTEXTWord Wrap

app_key29a39dtimestamp1623812664
  1. Nối chuỗi từ Bước 3 vào đường dẫn (path) của yêu cầu API. Path cho Get Authorized Shops/authorization/202309/shops:

PLAINTEXTWord Wrap

input = path + input

Chuỗi kết quả sẽ là:

PLAINTEXTWord Wrap

/authorization/202309/shopsapp_key29a39dtimestamp1623812664

📌 Lưu ý: Nếu content-type trong request header không phải multipart/form-data, hãy nối thêm phần body của yêu cầu API vào chuỗi.

PLAINTEXTWord Wrap

input = input + body

Ví dụ, endpoint Update Shop Webhook bao gồm các parameter address và event_type trong body của yêu cầu API (cũng như parameter shop_cipher trong query parameter). Điều này sẽ cho kết quả như sau:

PLAINTEXTWord Wrap

/event/202309/webhooksapp_key68xu9ks5p4i8shop_cipherROW_xkMbgAAAeVAQra0eZWebFQq5aIKtimestamp1696909648{    "address":"https://partner.tiktokshop.com",    "event_type": "PACKAGE_UPDATE"}
  1. Bọc chuỗi được tạo ở Bước 4 bằng app_secret của bạn:

PLAINTEXTWord Wrap

input = app_secret + input + app_secret

Chuỗi cuối cùng sẽ là:

PLAINTEXTWord Wrap

e59af819cc/authorization/202309/shopsapp_key29a39dtimestamp1623812664e59af819cc
  1. Mã hóa chuỗi đã bọc của bạn bằng HMAC-SHA256:

GOWord Wrap

import (        
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"sort"
)
/**
** input: string we created in step 5
** secret: App secret
**/
func generateSHA256(input, secret string) string{
// encode the digest byte stream in hexadecimal and use sha256 to generate sign with salt(secret)
h := hmac.New(sha256.New, []byte(secret))

if _, err := h.Write([]byte(input)); err != nil{
// TODO error log
return ""
}

return hex.EncodeToString(h.Sum(nil))
}

Chữ ký (sign) kết quả là:

PLAINTEXTWord Wrap

b596b73e0cc6de07ac26f036364178ab16b0a907af13d43f0a0cd2345f582dc8

Các lỗi thường gặp

Đâu là nguyên nhân phổ biến nhất khi gặp lỗi "signature is invalid" trong quá trình gọi API?

  • Thường thì lỗi này xảy ra khi nhà phát triển dùng sai app key và secret để tạo chữ ký. Điều cốt yếu là phải xác minh rằng app key và secret khớp chính xác.
  • Đảm bảo signaccess_token không được đưa vào danh sách khóa query đã sắp xếp lại (Bước 2).
  • Luôn đảm bảo bạn đang dùng phương thức chữ ký HMAC-SHA256 (khác với SHA-256 thông thường).
  • Đảm bảo rằng timestamp nằm trong phạm vi 5 phút so với thời điểm hiện tại khi nền tảng nhận được yêu cầu. Timestamp phải được biểu diễn dưới dạng Unix timestamp 10 chữ số.