TikTok
Prepare TikTok creator choices and publish verified media URLs or local videos with Social SDK, covering consent, privacy, drafts, and processing checks.
To post to TikTok from a Node.js or Bun server, use TikTok’s Content Posting API: query the creator’s info, show the creator a preview where they choose a privacy level and consent, then initialize a Direct Post or an inbox upload and check the publish status. The Social SDK TikTok adapter makes those calls through social.posts.publish and social.posts.getDelivery, rejects a post without the creator’s consent or, for Direct Post, with a privacy level the creator info does not offer, and reports processing or accepted until TikTok returns a public post ID.
npm install @opencoredev/social-sdkpnpm add @opencoredev/social-sdkyarn add @opencoredev/social-sdkbun add @opencoredev/social-sdknub add @opencoredev/social-sdkaube add @opencoredev/social-sdkUntil TikTok audits your app, Direct Post can publish only to private accounts with SELF_ONLY visibility. The setup sample below configures the adapter, and the examples after it fetch creator info, publish a video and reconcile the publish ID.
For how TikTok’s app audit and posting rules compare with the other platform APIs and unified social media APIs, read the social media APIs guide.
The direct TikTok adapter accepts an existing access token and creator openId. The server package also provides tiktokOAuth for TikTok Login Kit web authorization and creator discovery. OAuth consent does not replace the creator preview and publish consent requirements.
Setup
Connect the creator
Use ConnectionManager with tiktokOAuth to bind the callback to the authenticated tenant,
show the discovered creator, and persist credentials through an encrypted credentialSink. Its
default scopes are user.info.basic and video.publish, sent comma-delimited. Request
video.upload for inbox drafts, video.list for Display API reads and post metrics,
user.info.profile for bio, username and verification, and user.info.stats for account
statistics.
Verify media origins
For PULL_FROM_URL, verify each media domain or URL prefix with TikTok and list its HTTPS
origin in verifiedMediaOrigins.
Configure the adapter
Construct the backend with the token, openId, and verified origins.
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import type { TikTokPublishOptions } from "@opencoredev/social-sdk";
import { tiktok } from "@opencoredev/social-sdk/tiktok";
const accessToken = process.env.TIKTOK_ACCESS_TOKEN;
const openId = process.env.TIKTOK_OPEN_ID;
if (!accessToken || !openId) throw new Error("Configure TikTok credentials on the server.");
const social = createSocial({
backend: tiktok({
auth: { accessToken, openId },
verifiedMediaOrigins: ["https://media.example.com"],
}),
});
const account = connectedAccountRef({
backend: "default",
platform: "tiktok",
accountId: openId,
});
TikTok currently documents access tokens as valid for 2 hours and refresh tokens for 180 days, but the lifetimes can change, so rely on each response’s expires_in and refresh_expires_in (seconds) instead of fixed numbers. expiresAt is computed from expires_in, and refreshExpiresAt from refresh_expires_in, on both the authorization-code exchange and refreshOAuthToken("tiktok", ...). Both are RFC 3339 instants, and StoredCredential accepts refreshExpiresAt, so the token set your credentialSink saves can be stored as is. Persist the new access token, refresh token and refreshExpiresAt after every refresh: each refresh returns a different refresh_token, and reusing a replaced one fails. Schedule a reconnect prompt before refreshExpiresAt: once the refresh token lapses, refreshing fails and the creator has to authorize again. See Token management.
const next = await refreshOAuthToken("tiktok", { clientId, clientSecret }, stored);
await save({ ...next }); // accessToken, refreshToken, expiresAt, refreshExpiresAt
if (next.refreshExpiresAt && Date.parse(next.refreshExpiresAt) - Date.now() < 7 * 86_400_000)
await askCreatorToReconnect();
Desktop and CLI apps
Login Kit for Desktop requires PKCE, and TikTok expects the code_challenge as the hex-encoded SHA-256 of the verifier rather than the base64url form other providers use. Pass tiktokLoginKit: "desktop" to tiktokOAuth: the authorization URL then carries the hex code_challenge with code_challenge_method=S256, and the token exchange sends the attempt’s code_verifier. The redirect URI must be a loopback URL, http://localhost:<port>/... or http://127.0.0.1:<port>/... (HTTPS also works), with an explicit port and no query or fragment; other redirects are rejected before any request. In the developer portal you may register the port as * so your app can listen on any free port. Add the exact loopback callback to ConnectionManager’s redirect allowlist. The default, "web", sends no PKCE parameters.
const provider = tiktokOAuth({
clientId,
clientSecret,
redirectUri: "http://127.0.0.1:3455/callback/",
credentialSink,
tiktokLoginKit: "desktop",
});
QR code login for TVs and kiosks
QR code authorization lets a creator authorize from the TikTok app on their phone when your surface cannot open a browser redirect. It needs the client secret, so run it on your server.
createTikTokQrCode(options, { scopes, state })callsPOST /v2/oauth/get_qrcode/withclient_key, comma-separatedscopeand optionalstate. It returns TikTok’s QRtokenandscanQrCodeUrl, thescan_qrcode_urlwith itsclient_ticketquery parameter set to a random URL-safe ticket (passclientTicketto choose your own, under 512 characters URL-encoded). RenderscanQrCodeUrlas the QR code and keep the returned object for the whole flow.checkTikTokQrCode(options, qr)callsPOST /v2/oauth/check_qrcode/withclient_key,client_secretand the QR token, and returns thestatus:new,scanned,confirmed,expired, orutilised. Poll it on your own interval while the status isneworscanned; onexpired, create a new QR code. As TikTok requires, any response that carries aclient_ticketmust echo yours, and aconfirmedresponse must carry it and yourstate; otherwise the call throwsunauthorizedand the response is ignored. Onconfirmedit returns the authorizationcode(fromcode, or from theredirect_uriTikTok returns) and thatredirectUriwithout itscodeandstate.exchangeTikTokQrCode(options, { code, redirectUri })exchanges the code atPOST /v2/oauth/token/and returns the token set, includingrefreshExpiresAt, with the creator’sopenId. TikTok requiresredirect_urito match your registered redirect URI; if the check response has none, setoptions.redirectUri.
import {
checkTikTokQrCode,
createTikTokQrCode,
exchangeTikTokQrCode,
} from "@opencoredev/social-sdk/server";
const options = { clientId, clientSecret, redirectUri };
const qr = await createTikTokQrCode(options, { scopes: ["user.info.basic"], state });
showQrCode(qr.scanQrCodeUrl);
const check = await checkTikTokQrCode(options, qr); // call again until confirmed or expired
if (check.status === "confirmed") {
const { token, openId } = await exchangeTikTokQrCode(options, check);
await saveCredential(openId, token);
}
The QR flow does not go through ConnectionManager, so bind the result to the authenticated tenant yourself before saving it.
Connect a second creator in the same browser
Login Kit skips the authorization page when the browser already has a valid TikTok session, which reconnects the signed-in account instead of letting the user pick another one. Pass disableAutoAuth: true to tiktokOAuth to send disable_auto_auth=1, which makes TikTok always show the authorization page. Without it, TikTok’s default (0) applies. The option is TikTok-only; other OAuth providers reject it. See Login Kit for Web.
const provider = tiktokOAuth({
clientId,
clientSecret,
redirectUri,
credentialSink,
disableAutoAuth: true,
});
Revoke a creator’s token
revokeTikTokOAuthToken(options, token) calls POST /v2/oauth/revoke/ with your client key (clientId), clientSecret, and the creator’s access token as a form body. TikTok answers success with an empty body and the function returns "revoked", so it fits the grants.revoke hook of createOAuthConnectHandlers. An error body such as invalid_request throws upstream_failure with the code in upstreamCode. See Revoke a user access token.
import { revokeTikTokOAuthToken } from "@opencoredev/social-sdk/server";
await revokeTikTokOAuthToken({ clientId, clientSecret }, { accessToken: stored.accessToken });
TikTok domain verification covers the verified domain; URL-prefix verification covers only that prefix. verifiedMediaOrigins checks origins only, so configuring an origin does not prove that every path is inside a verified prefix. Your application must enforce that narrower boundary. Pull URLs must not redirect and must remain accessible throughout TikTok’s download, which can take up to one hour. See the Media Transfer Guide.
Examples
Fetch creator info before publishing
Query creator information before displaying the preview. Show the returned nickname, the content and caption, and every available privacy choice. There must be no default privacy selection: require the creator to choose a value in privacyLevels.
const native = social.native("default", { acknowledgeUnsafe: true });
if (!native) throw new Error("TikTok native module is unavailable.");
const creatorInfo = await native.creatorInfo(account, {
backendInstance: "default",
correlationId: "creator-preview",
retryBudget: { maxAttempts: 1, maxElapsedMs: 10_000 },
authorization: { tenantId: "tenant", principalId: "user" },
});
The SDK returns accountId, backend, fetchedAt, username, nickname, avatarUrl, privacyLevels, commentDisabled, duetDisabled, stitchDisabled, and maxVideoDurationSeconds. avatarUrl comes from creator_avatar_url so the preview can show which account will post; TikTok signs that URL for two hours, so query creator info again instead of storing it. avatarUrl and maxVideoDurationSeconds are null when TikTok omits them. Keep this snapshot bound to the same account and backend.
The creator-info query itself can refuse a creator who cannot post right now. TikTok returns spam_risk_too_many_posts (daily post cap reached), spam_risk_user_banned_from_posting (the creator is banned from posting), or reached_active_user_cap (your app’s daily quota of active publishing creators is used up) from creator_info/query with HTTP 200. native.creatorInfo and Direct Post publishing throw a SocialError with that code in upstreamCode and a never retry disposition. TikTok’s guidelines require you to stop the flow and tell the creator to try again later instead of showing the preview. See Query Creator Info.
When a restriction is disabled by TikTok, disable the corresponding interaction control and submit true for disableComments, disableDuet, or disableStitch. Duet and stitch apply only to videos. Check video duration against maxVideoDurationSeconds, show the commercial-content disclosure described below, and ask for the AI-content choice. Present TikTok’s Music Usage Confirmation and obtain the creator’s explicit consent before transfer. The adapter refreshes creator information before Direct Post initialization; if privacy or interaction restrictions changed, refresh the preview and ask again.
Commercial content disclosure
TikTok’s content sharing guidelines require a disclosure toggle, off by default, that asks whether the content promotes the creator, a brand, a product or a service. Turning it on shows two checkboxes that map to the SDK options:
| Checkbox | SDK option (wire field) | Label TikTok applies | Declaration shown before posting |
|---|---|---|---|
| Your brand | ownBrand (brand_organic_toggle) |
“Promotional content” | “By posting, you agree to TikTok’s Music Usage Confirmation.” |
| Branded content | brandedContent (brand_content_toggle) |
“Paid partnership” | “By posting, you agree to TikTok’s Branded Content Policy and Music Usage Confirmation.” |
| Both | both true |
“Paid partnership” | The Branded Content Policy and Music Usage Confirmation declaration. |
While the toggle is on and neither box is checked, keep the publish button disabled and tell the creator they must say whether the content promotes themselves, a third party, or both. Branded content can be public or friends-only, never private: disable the “Only me” choice while Branded content is checked. The adapter also rejects brandedContent: true with SELF_ONLY for Direct Post, both during preparation and before initialization.
The following functions take validated choices from your application’s submitted creator preview. They do not select privacy or enable interactions on the creator’s behalf. Call them only after the preview, disclosure prompts, Music Usage Confirmation, and publish consent are complete.
type CreatorChoices = Required<
Pick<
TikTokPublishOptions,
| "privacy"
| "disableComments"
| "disableDuet"
| "disableStitch"
| "brandedContent"
| "ownBrand"
| "aiGenerated"
>
>;
Publish a video
Preparation requires explicit booleans for aiGenerated and draft, along with every interaction and disclosure choice. draft: false selects Direct Post; draft: true selects inbox delivery.
videoCoverTimestampMs picks the cover frame and is sent as post_info.video_cover_timestamp_ms. It takes whole milliseconds from the start of the video, must fall within the known durationSeconds, and applies to video Direct Post only, because inbox video uploads send no post_info. Without it TikTok uses the first frame, and TikTok also falls back to the first frame for a value it considers invalid.
async function publishVideo(choices: CreatorChoices) {
return social.posts.publish({
targets: [
{
account,
options: {
privacy: choices.privacy,
disableComments: choices.disableComments,
disableDuet: choices.disableDuet,
disableStitch: choices.disableStitch,
brandedContent: choices.brandedContent,
ownBrand: choices.ownBrand,
aiGenerated: choices.aiGenerated,
consentGiven: true,
creatorInfo,
draft: false,
videoCoverTimestampMs: 2_000,
},
},
],
content: {
text: "A creator-approved caption",
media: [
{
kind: "video",
source: { kind: "https-url", url: "https://media.example.com/video.mp4" },
durationSeconds: 8,
},
],
},
});
}
Local videos use FILE_UPLOAD instead of PULL_FROM_URL. Supply a Blob source
or a replayable stream with an exact positive byteSize, an explicit
video/mp4, video/quicktime, or video/webm MIME type, and known duration.
The adapter transfers at most 4 GiB in bounded chunks. uploadChunkBytes accepts
5–64 MiB and defaults to 10 MiB; small files upload as a single chunk. Provider
upload requests carry no API access token, follow no redirects, and are never
retried automatically. A successful transfer returns processing, not
published. Keep the delivery reference and reconcile uncertain transfers
before trying another upload. Verified media origins are needed for URL pulls,
not for local video uploads.
Publish a photo carousel
The adapter supports one video or 1–35 photos. photoCoverIndex selects the cover image by its zero-based index. Photo titles allow 90 UTF-16 code units and descriptions (content.text) allow 4,000; video captions allow 2,200.
autoAddMusic: true asks TikTok to add recommended music to a photo Direct Post (post_info.auto_add_music). It defaults to false. The adapter rejects it for videos and for inbox drafts, because TikTok accepts auto_add_music only on photo Direct Post. Photo posts do not use disableDuet or disableStitch.
When using the Zernio backend, TikTokPublishOptions.title (90 code units), description (4,000 code units), mediaType: "photo", and autoAddMusic configure photo posts. description and mediaType are Zernio-only; the direct adapter uses content.text as the photo description and rejects both.
async function publishPhotos(choices: CreatorChoices) {
return social.posts.publish({
targets: [
{
account,
options: {
privacy: choices.privacy,
disableComments: choices.disableComments,
disableDuet: choices.disableDuet,
disableStitch: choices.disableStitch,
brandedContent: choices.brandedContent,
ownBrand: choices.ownBrand,
aiGenerated: choices.aiGenerated,
consentGiven: true,
creatorInfo,
draft: false,
photoCoverIndex: 1,
},
},
],
content: {
text: "Photo set",
media: [
{ kind: "image", source: { kind: "https-url", url: "https://media.example.com/1.jpg" } },
{ kind: "image", source: { kind: "https-url", url: "https://media.example.com/2.jpg" } },
],
},
});
}
Send a video to the creator’s inbox
Request video.upload for this flow. After showing the preview and obtaining transfer consent, submit explicit choices with draft: true. The adapter skips Direct Post creator-info and visibility checks for inbox delivery. The video inbox endpoint transfers the media without post_info, so the video caption and other posting choices are not sent. The creator must open TikTok’s inbox notification, edit the video, and finish posting in TikTok.
async function sendVideoDraft(choices: CreatorChoices) {
return social.posts.publish({
targets: [
{
account,
options: {
privacy: choices.privacy,
disableComments: choices.disableComments,
disableDuet: choices.disableDuet,
disableStitch: choices.disableStitch,
brandedContent: choices.brandedContent,
ownBrand: choices.ownBrand,
aiGenerated: choices.aiGenerated,
consentGiven: true,
draft: true,
},
},
],
content: {
media: [
{
kind: "video",
source: { kind: "https-url", url: "https://media.example.com/video.mp4" },
durationSeconds: 8,
},
],
},
});
}
Photo inbox delivery uses draft: true with the photo shape above, including photoCoverIndex, and needs video.upload. The adapter sends post_mode: "MEDIA_UPLOAD" with only the photo title and description in post_info. TikTok documents privacy_level, disable_comment, auto_add_music, and the brand toggles as Direct Post fields, so the adapter leaves them out of inbox drafts; the creator chooses them in TikTok when completing the post. Photo inbox drafts need TikTok app version 31.8 or later on the creator’s device. Older apps make initialization fail with app_version_check_failed, which the SDK reports as upstream_failure with that upstreamCode; ask the creator to update TikTok and try again. See Photo Post.
Reconcile the publish ID
Initialization returns accepted with the publish_id in outcome.delivery.deliveryId. Reconcile through posts.getDelivery. Download/upload states remain processing; SEND_TO_USER_INBOX remains accepted and requires creator completion. PUBLISH_COMPLETE without exactly one public post ID also remains accepted, both for private or non-public completion and for several posts (below). Only a single public identity produces published. A confirmed failure is failed.
One inbox publish_id can produce several public posts, because TikTok returns publicaly_available_post_id as a list. When it lists more than one ID, getDelivery returns accepted with every ID in postIds (as decimal strings) rather than choosing one.
A FAILED status carries TikTok’s fail_reason, which becomes the outcome’s code. The SDK sets the retry disposition from it:
fail_reason |
Meaning | Retry disposition |
|---|---|---|
internal |
TikTok server error | reconcile-first |
video_pull_failed, photo_pull_failed |
TikTok could not download the URL in time | reconcile-first; retry only if the URL is still valid |
file_format_check_failed, duration_check_failed, frame_rate_check_failed, picture_size_check_failed |
The media breaks the media specifications | never; fix the media |
publish_cancelled |
The developer cancelled the post | never |
auth_removed |
The creator revoked access during processing | never; reconnect first |
spam_risk_too_many_posts, spam_risk_user_banned_from_posting, spam_risk_text, spam_risk |
TikTok’s spam checks blocked the post | never |
A missing fail_reason becomes upstream_failure. See Get Post Status.
Cancel a slow URL download
native.cancelPublish({ account, publishId, context }) calls POST /v2/post/publish/cancel/ with the publish_id of a PULL_FROM_URL Direct Post or inbox upload that TikTok is still downloading. It needs video.publish or video.upload. Cancellation is best effort: TikTok cannot stop a download that is nearly finished or already in file processing, so a resolved call only means TikTok accepted the request. Keep polling posts.getDelivery until it reports failed with publish_cancelled, or a final success. A task that already reached a final state throws upstream_failure with upstreamCode publish_not_cancellable; other codes include invalid_publish_id and token_not_authorized_for_specified_publish_id. See the Media Transfer Guide.
const native = social.native("default", { acknowledgeUnsafe: true });
await native.cancelPublish({ account, publishId, context });
async function reconcile(result: Awaited<ReturnType<typeof publishVideo>>) {
const outcome = result.outcomes[0];
if (outcome?.delivery) {
const delivery = await social.posts.getDelivery(outcome.delivery);
console.log(delivery.state); // accepted | processing | published | failed | unknown
return delivery;
}
}
Receive webhooks
Pass the app client secret as webhookSecret. adapter.webhooks.verify checks TikTok-Signature and rejects timestamps more than 300 seconds old; set webhookToleranceSeconds to change the window, because TikTok does not publish one.
TikTok sends five Content Posting events, named post.publish.*. Each carries publish_id and publish_type in content, and the SDK uses content.publish_id as backendRecordId. publish_type is INBOX_SHARE for inbox uploads.
originalType |
When TikTok sends it | Extra content fields |
SDK type |
|---|---|---|---|
post.publish.failed |
Publishing failed | reason |
publication.updated |
post.publish.complete |
The creator posted the content; one publish_id can yield several posts |
none | publication.updated |
post.publish.inbox_delivered |
The inbox notification for an upload reached the creator, so the draft is waiting in TikTok | none | publication.updated |
post.publish.publicly_available |
The post became publicly viewable | post_id |
publication.updated |
post.publish.no_longer_publicaly_available |
The post stopped being publicly viewable | post_id |
post.removed |
Read content.post_id on the last two events to find the public post, and content.reason on a failure. TikTok spells the last event no_longer_publicaly_available, and the SDK matches that spelling. post.removed here means the post is no longer public: the creator may have deleted it or made it private, or moderation may have removed it. Legacy video.* events use share_id and are not assigned that backend record ID. See Get Post Status.
authorization.removed decodes to account.updated. Read content.reason and mark the creator’s connection revoked, then ask them to reconnect if they want to keep posting:
reason |
Meaning |
|---|---|
0 |
Unknown |
1 |
The user disconnected your app in TikTok |
2 |
The user’s account was deleted |
3 |
The user’s age changed |
4 |
The user’s account was banned |
5 |
The developer revoked authorization, for example through revokeTikTokOAuthToken |
portability.download.ready (see Data Portability) decodes to unknown with its originalType and content.request_id. See Process webhooks and TikTok’s Events guide.
Read the creator’s profile
social.graph.getProfile(account, {}) reads the authorized creator through GET /v2/user/info/. TikTok’s Display API reads only the user who granted the token, so the adapter refuses a handle or another profileId before any request.
| Scope | Fields | Where they land |
|---|---|---|
user.info.basic |
open_id, union_id, avatar_url, avatar_url_100, avatar_large_url, display_name |
displayName, avatarUrl (from avatar_url), and every field in native |
user.info.profile |
username, bio_description, profile_deep_link, is_verified |
handle, bio, and native |
user.info.stats |
follower_count, following_count, likes_count, video_count |
social.analytics.getAccountMetrics |
union_id identifies the same person across every app owned by your developer account, while open_id is specific to one app. Use union_id to link a creator who connected two of your apps. The user.info.profile fields need that scope, so the adapter asks for them only when you construct it with userInfoProfile: true; without the grant TikTok rejects the whole call for a missing scope.
const profile = await social.graph.getProfile(account, {});
console.log(profile.displayName, profile.avatarUrl, profile.native?.["union_id"]);
For a custom field list, native.userInfo({ account, fields, context }) requests exactly the fields you name, plus open_id, and checks that TikTok returned the connected creator. The exported tiktokUserInfoFields groups the field names by scope.
Native operations and Display API reads
The typed native module adds inbox/draft upload, creator-info preflight, own-video listing with Display fields, filtered video query, and an explicit publish-status query. posts.list accepts a page size from 1 to 20. The documented Display API surface lists the authorized creator’s public videos; it does not promise photo or private-post enumeration. create_time is in Unix seconds, while returned pagination cursors are timestamps in milliseconds. Pass cursors back unchanged as strings.
Use native.queryVideos to refresh one to 20 owned video IDs and their current metadata, including
embed_html, embed_link, and is_aigc. The adapter sends only the documented video_ids filter
and rejects empty or oversized batches before making a request. Treat embed_html as untrusted
provider content and sanitize it before rendering. The posts.publish.preflight capability records
that Direct Post uses the Content Posting API creator-info query immediately before initialization;
it is not a Research API declaration, and inbox drafts intentionally skip that preflight.
cover_image_url expires after six hours. Refresh it through posts.get instead of treating it as a permanent asset URL. Post metrics from social.analytics.getPostMetrics read like, comment, share, and view counts through the video-query endpoint and require video.list; account metrics require user.info.stats. A missing requested video is an error; absent individual metric fields stay absent.
For the creator’s own videos, embed_html and embed_link from the Display API provide embed markup. TikTok’s oEmbed endpoint is listed under Not supported.
Embed Player
The Embed Player plays any public post in an iframe at https://www.tiktok.com/player/v1/{post_id} and needs no token. tiktokEmbedPlayerUrl(postId, options) builds that URL, sending each option you set as 1 or 0 and leaving the others at TikTok’s default:
| Option | Query parameter | Default | Effect |
|---|---|---|---|
controls |
controls |
on | Off hides the progress bar and every control button |
progressBar |
progress_bar |
on | Progress bar (video) |
playButton |
play_button |
on | Play button (video) |
volumeControl |
volume_control |
on | Volume control |
fullscreenButton |
fullscreen_button |
on | Full-screen button |
timestamp |
timestamp |
on | Playback time and duration (video) |
loop |
loop |
off | Loop playback (video) |
autoplay |
autoplay |
off | Start playing on load (video) |
musicInfo |
music_info |
off | Show the sound’s details |
description |
description |
off | Show the post’s description |
rel |
rel |
on | On shows related videos; off shows the author’s own videos |
nativeContextMenu |
native_context_menu |
on | The browser’s right-click menu |
closedCaption |
closed_caption |
on | Closed captions (video) |
forceMuted |
force_muted |
off | Volume 0, and the viewer cannot change it |
To control the player, post tiktokPlayerMessage(command) to the iframe. It builds TikTok’s { type, value, "x-tiktok-player": true } shape for play, pause, mute, unMute, seekTo (seconds) and navigateTo (image index, photo posts only). parseTikTokPlayerEvent(event.data) decodes the player’s messages, onPlayerReady, onStateChange (-1 init, 0 ended, 1 playing, 2 paused, 3 buffering), onCurrentTime, onMute, onVolumeChange, onImageChange and onPlayerError, and returns undefined for anything else. Check that event.origin is https://www.tiktok.com first.
import {
parseTikTokPlayerEvent,
tiktokEmbedPlayerUrl,
tiktokPlayerMessage,
} from "@opencoredev/social-sdk/tiktok";
iframe.src = tiktokEmbedPlayerUrl(videoId, { musicInfo: true, description: true, rel: false });
window.addEventListener("message", (event) => {
if (event.origin !== "https://www.tiktok.com") return;
const playerEvent = parseTikTokPlayerEvent(event.data);
if (playerEvent?.type === "onPlayerReady")
iframe.contentWindow?.postMessage(
tiktokPlayerMessage({ type: "play" }),
"https://www.tiktok.com",
);
});
Creator-profile embeds are listed under Not supported.
Polling is caller initiated and bounded; the SDK does not run background polling. Comments and direct messages remain platform limits for standard Content Posting and Display API apps. The only API route to a creator’s direct messages is a Data Portability export, which is a one-off archive, not an inbox.
Data Portability
TikTok’s Data Portability API lets a creator export their own TikTok data, including direct messages, to your app. It needs TikTok’s approval for the portability.* scopes on top of Login Kit. Each scope comes in a .single variant (one request) and an .ongoing variant (repeated requests):
| Category | Scope |
|---|---|
profile, video |
portability.postsandprofile.single or .ongoing |
activity |
portability.activity.single or .ongoing |
direct_message |
portability.directmessages.single or .ongoing |
all_data, or any mix |
portability.all.single or .ongoing |
The native module wraps the four endpoints:
native.addDataRequest({ account, dataFormat, categories, context })callsPOST /v2/user/data/add/withdata_format("text"or"json") andcategory_selection_list, and returns{ requestId }.native.checkDataRequest({ account, requestId, context })callsPOST /v2/user/data/check/and returnsstate(pending,downloadingwhen the archive is ready,expired,cancelled, orunknownfor a new status, with the raw value inproviderStatus),appliedAt,collectionStartedAt,dataFormat, andcategories.native.cancelDataRequest({ account, requestId, context })callsPOST /v2/user/data/cancel/. With an.ongoingscope you can then make another request.native.downloadDataRequest({ account, requestId, context })callsPOST /v2/user/data/download/and returns the zip archive as aReadableStream<Uint8Array>that you must consume or cancel. The retry budget covers only the wait for the response headers. Archives expire four days after they become ready.
TikTok’s request_id is a 64-bit integer. The SDK passes it as a decimal string and refuses IDs beyond Number.MAX_SAFE_INTEGER, which a JSON number cannot carry exactly. If TikTok accepts addDataRequest but returns no usable request_id, the call throws ambiguous_outcome with retryDisposition: { kind: "reconcile-first" }, because TikTok may already have created the export; check the creator’s requests before you retry. Poll checkDataRequest, or wait for the portability.download.ready webhook, before downloading.
const { requestId } = await native.addDataRequest({
account,
dataFormat: "json",
categories: ["profile", "video"],
context: {
backendInstance: "default",
correlationId: "export",
retryBudget: { maxAttempts: 1, maxElapsedMs: 10_000 },
},
});
Exported data includes private content such as direct messages. Store it with the same care as credentials and delete it when the creator’s request is fulfilled.
Raw requests
native.request(input, context) sends one request to a TikTok API endpoint the SDK does not wrap, with the creator’s bearer token. It checks input.account against the configured openId and context.backendInstance. A rejection with TikTok’s error.code envelope maps like other TikTok calls, such as scope_not_authorized to missing_permission. A 200 response is returned as is, so check its error.code yourself.
The only host is api (https://open.tiktokapis.com). Paths include the version, such as /v2/user/info/.
// `native` is social.native("default", { acknowledgeUnsafe: true }); `account` is the
// connected account and `context` an AdapterOperationContext for its backend instance.
const response = await native.request(
{
account,
method: "GET",
path: "/v2/user/info/",
query: { fields: "open_id,display_name,follower_count" },
},
context,
);
if (response.responseType === "json") console.log(response.status, response.body);
path is relative to the host’s base URL and starts with /; put parameters in query. Before any request, the adapter rejects absolute URLs, // paths, .. segments, encoded separators, ? or # in the path, and unknown host names. It also rejects credential and framing headers such as Authorization, Cookie and Content-Type. It does not follow redirects. A body has a kind of json, text, bytes, form or urlencoded, and its kind sets Content-Type; text and bytes also take a contentType. Set responseType to "text" or "bytes" for a body that is not JSON. Response bodies are capped at 2 MiB, and maxResponseBytes raises the cap to at most 16 MiB. The result has status, responseType, body and lowercase headers. Name extra headers in responseHeaders; set-cookie is never returned. A non-2xx response rejects with a SocialError mapped like the adapter’s other calls.
A raw request makes one attempt. It adds no idempotency key and records no outcome, so you reconcile the writes you send. A write rejects with ambiguous_outcome when it fails without a definitive answer: a timeout, a cancellation after dispatch, a 5xx, or a 2xx body that cannot be decoded. Like other native calls it skips the client’s tenant authorization, so check the tenant’s grant for the account first. Prefer a typed method when one exists.
Limits and requirements
Media specifications
| Media | Requirement | TikTok limit | Checked by the SDK before sending |
|---|---|---|---|
| Video | Format | MP4 (recommended), WebM, MOV | Declared MIME type: video/mp4, video/quicktime or video/webm, else tiktok.mime. Blob and stream videos must declare one; a URL video without a type is not checked. The file’s bytes are not inspected. |
| Video | Codec | H.264 (recommended), H.265, VP8, VP9 | No, TikTok only (file_format_check_failed) |
| Video | Frame rate | 23 to 60 FPS | No, TikTok only (frame_rate_check_failed) |
| Video | Resolution | 360 to 4,096 px in height and width | No, TikTok only (picture_size_check_failed) |
| Video | Duration | Up to the creator’s maxVideoDurationSeconds; the API accepts at most 10 minutes |
Yes: a missing or non-positive durationSeconds fails as tiktok.duration, and a Direct Post longer than the creator’s maxVideoDurationSeconds from creator info fails before upload. Drafts and the 10-minute API cap are left to TikTok (duration_check_failed). |
| Video | File size | 4 GB | Blob and stream videos: 1 byte to 4 GiB, from the Blob’s size or the stream’s declared byteSize, else tiktok.size. URL videos: no, TikTok only. |
| Photo | Format | JPEG, WebP | Declared MIME type: image/jpeg or image/webp, else tiktok.mime. Photos must be HTTPS URLs on a verified origin; a Blob or stream photo fails as tiktok.source. |
| Photo | Resolution | At most 1080p | No, TikTok only (picture_size_check_failed) |
| Photo | File size | 20 MB per photo | Only when byteSize is set: above 20 MiB fails as tiktok.photo_size. |
| Post | Items | One video, or 1 to 35 photos | Yes: anything else, including a mix of photos and video, fails as tiktok.media. |
Source: TikTok’s Media Transfer Guide. The SDK cannot read codecs, frame rate or pixel dimensions from a URL, so TikTok reports those problems after the request through fail_reason. Transcode to H.264 MP4 at 23 to 60 FPS and keep photos at 1080p or smaller to avoid them.
Unaudited Direct Post clients can post only to private creator accounts with SELF_ONLY visibility and are limited to five users in a 24-hour period. Public posting requires TikTok’s audit approval. The SDK does not claim approval or silently change privacy. Follow TikTok’s content sharing guidelines when building the creator preview.
Direct Post daily caps vary by creator and are typically about 15 posts per creator per day, shared across API clients. Inbox uploads also have a pending-share limit: TikTok documents at most five pending shares in a 24-hour period. Initialization endpoints allow 6 requests per minute per user access token, creator-info queries 20, and status queries 30.
Provider codes include rate_limit_exceeded, spam_risk_too_many_posts, spam_risk_too_many_pending_share, reached_active_user_cap, and unaudited_client_can_only_post_to_private_accounts. The SDK preserves structured provider codes as upstreamCode; HTTP 429 stays rate_limited with its status and validated structured provider code, when present. Rate-limit errors expose a delay disposition bounded to 60 seconds, using Retry-After when available and 60 seconds otherwise. This disposition does not replay a write or start automatic polling.
The adapter rejects missing consent, changed creator choices, unverified origins, unsupported schedules/replies/links, and unknown video duration.
Not supported
- Research API video query: searching public TikTok videos by keyword, hashtag, region, username or date exists only in the Research API, which TikTok limits to approved non-profit academic researchers for non-commercial use. The SDK has no public video search.
- Research API user data: public user info, followers, following, liked, pinned and reposted videos, playlists, and TikTok Shop data are also Research API-only and partner-gated.
- Research Batch Compliance APIs: the
validation_taskendpoints (POST /v2/research/validation_task/create/,validation_task_status/get/,validation_task/download/) check whether stored Research API data is still public. They need a Research API client token withresearch.data.basic, so they are partner-gated with the rest of the Research API. See Batch Compliance APIs. - Client access tokens: TikTok issues a client access token (
grant_type=client_credentials) for app-level calls, and only the Research and Commercial Content APIs use it. The SDK requests user tokens only. - Post edits: the Content Posting API sets posting options only during initialization, so
posts.updateisunsupported-by-platform. - oEmbed: TikTok’s oEmbed endpoint for embedding any public video is marked for deprecation in favor of the Embed Player, so the SDK does not wrap it.
- Creator-profile embeds: they use a separate Embed Creator Profiles snippet; the SDK has no builder for it.
Keep tokens server-side and treat media URLs, creator data, and publish IDs as sensitive. Repository evidence is offline contract and mocked transport coverage; no TikTok app or creator account has been live-verified.