isaackogan / tiktok-shop-api
PHP client for the TikTok Shop API (TikTok Shop Partner API): request signing, OAuth, multi-shop support and typed webhooks. Unofficial. Not affiliated with or endorsed by TikTok or ByteDance.
Requires
- php: >=8.2
- ext-curl: *
- ext-json: *
- ext-mbstring: *
- guzzlehttp/guzzle: ^7.3
- guzzlehttp/psr7: ^1.7 || ^2.0
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A PHP client for the TikTok Shop API (the TikTok Shop Partner API / Open API). It covers every endpoint in the published spec, signs every request, ships an OAuth helper, and parses webhooks into typed event classes you narrow with instanceof.
Unofficial. Not affiliated with or endorsed by TikTok or ByteDance.
- Every TikTok Shop API endpoint, grouped like TikTok's own docs:
$shop->orders,$shop->products,$shop->fulfillment,$shop->finance, and 15 more. - Typed models. Generated from the OpenAPI spec, so requests and responses are PHP classes with getters and setters. 64-bit IDs are never rounded.
- Request signing (HMAC-SHA256),
app_key,timestamp,shop_cipherand thex-tts-access-tokenheader added for you. - OAuth helper for the authorization URL, code exchange and token refresh.
- Webhooks: verify the signature, then get a typed event class for any of the webhook types.
- PHP 8.2+ with
ext-curl,ext-jsonandext-mbstring, on Guzzle 7.
Request signing follows TikTok's documentation and has not been verified against the live API. If TikTok rejects a signature, please open an issue at https://github.com/isaackogan/TikTok-Shop-Api-SDKs/issues.
Install
composer require isaackogan/tiktok-shop-api
Needs PHP 8.2 or newer, with ext-curl, ext-json and ext-mbstring.
The package is developed in the TikTok-Shop-Api-SDKs monorepo and mirrored to isaackogan/TikTok-Shop-Api-PHP, which is the repository Packagist reads. Please open issues in the main repository.
Authorize a shop (OAuth)
use TikTokShop\Api\TikTokShopAuth; $auth = new TikTokShopAuth(appKey: 'YOUR_APP_KEY', appSecret: 'YOUR_APP_SECRET', serviceId: 'YOUR_SERVICE_ID'); // 1. Send the seller to this page. Use TikTokShopAuth::PARTNER or CREATOR for the other kinds, // and TikTokShopAuth::ROW outside the United States. $url = $auth->authorizationUrl(TikTokShopAuth::SELLER, TikTokShopAuth::US, state: 'a-random-csrf-token'); header('Location: ' . $url); // 2. TikTok redirects back to your redirect URL with ?code=... $tokens = $auth->exchangeCode($_GET['code']); echo $tokens->getAccessToken(), ' expires at ', $tokens->accessTokenExpireIn, PHP_EOL; echo $tokens->sellerName, ' (', $tokens->sellerBaseRegion, ')', PHP_EOL;
Refresh the tokens, and store them:
use TikTokShop\Api\TikTokShopAuth; use TikTokShop\Api\TokenSet; $auth = new TikTokShopAuth(appKey: 'YOUR_APP_KEY', appSecret: 'YOUR_APP_SECRET'); // Store the tokens: toArray() holds the real values, so encrypt it at rest. $stored = json_encode($tokens->toArray()); // Later: refresh before the access token expires. $tokens = TokenSet::fromArray(json_decode($stored, true)); $tokens = $auth->refresh($tokens->getRefreshToken());
TokenSet holds secrets. The access and refresh tokens are not properties: var_dump and print_r show ***, var_export and (array) show neither token, and serialize() refuses. Read them with getAccessToken() and getRefreshToken(); toArray() and json_encode() give the real values for storage.
Make your first call
use TikTokShop\Api\TikTokShop; $shop = new TikTokShop(appKey: 'YOUR_APP_KEY', appSecret: 'YOUR_APP_SECRET', accessToken: 'THE_SELLER_ACCESS_TOKEN'); // The shop cipher comes from the authorized-shops call. $response = $shop->authorization->getAuthorizedShops(); foreach ($response->getData()->getShops() as $authorized) { echo $authorized->getName(), ' ', $authorized->getRegion(), ' ', $authorized->getCipher(), PHP_EOL; }
With a shop cipher, the operations that need one (most of them) work:
use TikTokShop\Api\TikTokShop; $shop = new TikTokShop( appKey: 'YOUR_APP_KEY', appSecret: 'YOUR_APP_SECRET', accessToken: 'THE_SELLER_ACCESS_TOKEN', shopCipher: 'THE_SHOP_CIPHER', ); // Every request is signed, and shop_cipher is added to the operations that take one. $orders = $shop->orders->getOrderList(20); foreach ($orders->getData()->getOrders() as $order) { echo $order->getId(), PHP_EOL; }
The generated methods take their parameters positionally, in the order of the generated signature. The shop_cipher parameter is not among them: the client adds it.
Several shops (withShop)
use TikTokShop\Api\TikTokShop; $shop = new TikTokShop(appKey: 'YOUR_APP_KEY', appSecret: 'YOUR_APP_SECRET', accessToken: 'TOKEN_A', shopCipher: 'CIPHER_A'); // Another shop, sharing the HTTP client. $shop is not changed. $other = $shop->withShop(accessToken: 'TOKEN_B', shopCipher: 'CIPHER_B'); // An argument you leave out keeps this client's value; null (or '') clears it. $sameShopNewToken = $shop->withShop(accessToken: 'TOKEN_A2'); $noShop = $shop->withShop(shopCipher: null);
Errors
use TikTokShop\Api\TikTokShop; use TikTokShop\Api\TikTokShopException; $shop = new TikTokShop(appKey: 'YOUR_APP_KEY', appSecret: 'YOUR_APP_SECRET', accessToken: 'THE_SELLER_ACCESS_TOKEN'); try { $shop->authorization->getAuthorizedShops(); } catch (TikTokShopException $e) { // TikTok's code (or the HTTP status), its request ID for support, and the HTTP status (null if the request never got a reply). error_log($e->getTikTokCode() . ' ' . $e->getMessage() . ' ' . $e->getRequestId() . ' ' . $e->getHttpStatus()); }
Every failure is a TikTokShopException, with getTikTokCode(), getMessage(), getRequestId(), getHttpStatus() and the original exception as getPrevious():
| What happened | getTikTokCode() |
getHttpStatus() |
|---|---|---|
TikTok's reply has a non-zero code, even on HTTP 200 |
TikTok's code | the status |
| A non-2xx reply | TikTok's non-zero code if the body has one, else the HTTP status | the status |
A 2xx reply that can't be read: not a JSON object, a code that isn't an integer, a body that doesn't fit its model, or a token reply without data |
-1 | the status |
| No HTTP response at all: connection refused, DNS, TLS, or a timeout | -1 | null |
Messages and previous exceptions never contain the request URL, so a logged exception does not leak the signature.
Webhooks
use TikTokShop\Api\Generated\Event\OrderStatusChangeEvent; use TikTokShop\Api\Generated\Event\SellerDeauthorizationEvent; use TikTokShop\Api\UnknownWebhookEvent; use TikTokShop\Api\WebhookSignatureException; use TikTokShop\Api\Webhooks; // The body exactly as received (never re-encode it), and the Authorization header. $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; try { $event = Webhooks::verifyAndParse('YOUR_APP_KEY', 'YOUR_APP_SECRET', $rawBody, $signature); } catch (WebhookSignatureException) { http_response_code(401); exit; } catch (InvalidArgumentException) { http_response_code(400); // not a webhook this SDK can read exit; } if ($event instanceof OrderStatusChangeEvent) { $data = $event->getPayload()->getData(); echo $data->getOrderId(), ' is now ', $data->getOrderStatus(), PHP_EOL; } elseif ($event instanceof SellerDeauthorizationEvent) { echo 'shop ', $event->getPayload()->getShopId(), ' revoked access', PHP_EOL; } elseif ($event instanceof UnknownWebhookEvent) { error_log('unhandled webhook type ' . $event->getType()); } http_response_code(200);
Webhooks::parse()returns one event class per webhook type (they live inTikTokShop\Api\Generated\Event, rendered from the spec), each with a typedgetPayload(), or anUnknownWebhookEvent. Narrow withinstanceof. All of them extendTikTokShop\Api\WebhookEvent, which hasgetType()andgetRaw()(the decoded body, unknown fields included).- Verify against the raw body as received: re-encoding the JSON changes the signature.
- Parsing is strict. A body that is not a JSON object, a
typethat is missing or not an integer, or a known type with a required property missing or null throwsInvalidArgumentException: a known type missing a required field is a parse error. An integertypethis SDK does not know becomesUnknownWebhookEvent. Unknown fields are ignored. - 64-bit IDs keep their exact digits (a
shop_idsent as a JSON number arrives as the string"7494049642642441621"). Webhooks::EVENT_TYPESmaps a type to its name, for example1toORDER_STATUS_CHANGE.
API surface
The client's properties are the API groups: affiliateCreator, affiliatePartner, affiliateSeller, analytics, authorization, customerEngagement, customerService, event, finance, fulfilledByTikTok, fulfillment, logistics, orders, products, promotion, returnAndRefund, seller, supplyChain, tools. Each is the concrete generated API class, so your IDE completes the operations and models.
HTTP client
By default the SDK builds a Guzzle client that never follows redirects, with a 30 second timeout (timeout:, 0 for none) and Guzzle's default proxy behaviour. baseUrl: (on the client and the OAuth helper) must be an origin such as https://host:8443, with no path prefix: signing and shop-cipher matching use the request path. Requests carry the User-Agent tiktok-shop-api-php/<version>. Pass your own with httpClient:; it is used as is, except that the OAuth token calls never follow redirects even on your client, because their URL holds your app secret.
Notes
- Generated from the spec in the TikTok-Shop-Api-SDKs repository, which also has SDKs for TypeScript, Python, Go, Java, Kotlin, Rust and C#.
- Three operations are GETs with a body (
getConversationList,mapTokopediaToTikTokShopProductIDs,getShippingTemplateAvailability). The SDK sends the body and signs it. - The secrets passed to the OAuth helper are also hidden from stack traces (
#[\SensitiveParameter]).
License
MIT