TikTok
This guide documents our TikTok integration end‑to‑end: the goals, the OAuth flow we implement in Cloud Functions, required scopes, how we fetch and persist profile information, and how we keep tokens fresh. It is intentionally detailed to aid audits, troubleshooting, and future feature work.
Quick links: Frontend • Backend
Goals and constraints
- Multi-account support: Users can connect multiple TikTok accounts
- Persist only what we need for UI and future posting: access/refresh tokens, minimal profile card, and basic metadata.
- Enforce required scopes server‑side so profile fields like
display_nameandavatar_urlare always retrievable. - Follow TikTok Open API constraints: exact
redirect_urimatch with no query string; correct HTTP methods and paths.
Why we do it this way
- TikTok returns user profile fields (including
display_nameandavatar_url) only when the grant includesuser.info.basicand sometimesuser.info.profile. We enforce these scopes in our authorize URL regardless of client input to avoid partial grants. - TikTok’s user info endpoint expects a GET with
fieldsin the query string. Some runtimes return 404/405 if method/path is wrong or when a trailing slash is used; we implement a resilient fetch that retries without the trailing slash. - We store a compact
profilefor UI (platformId,displayName,username,avatarUrl,accountType: 'user') and keep the raw response underextra.profilefor observability without widening our primary schema. - Long‑lived stability: we persist the
refresh_tokenand provide both an on‑demand refresh endpoint and a scheduled sweep to rotate access tokens before expiry.
References: ttAuthStart, ttAuthCallback, ttRefreshNow, ttRefreshSweep in apps/functions/integrations/tiktok/auth.js.
Backend
See also: Frontend
Permissions and scopes
Server‑enforced default scope set:
- Required:
user.info.basic,user.info.profile - Defaults for future posting/analytics:
video.list,video.upload,user.info.stats
Details:
- We accept optional client‑provided scopes, but we always merge and de‑duplicate with the required set above so essential profile fields are available.
- The scopes appear in the TikTok consent dialog when the user connects their account.
Token flow
1) Start: ttAuthStart
- Requires a Firebase ID token in
Authorization: Bearer <idToken>; we verify it and extract theuid. - Creates a
statedocument atoauthStates/{state}with{ uid, provider: 'tiktok', returnTo }. - Builds the authorize URL
https://www.tiktok.com/v2/auth/authorize/with:client_keyscope(merged set described above)response_type=coderedirect_uri— must not contain a query string per TikTok rules. We validate and refuse if a query or fragment is present.
- Local development: if
?desktop=1, we usehttp://localhost:3000/api/tiktok/callbackas the redirect to support Desktop flow during dev.
2) Callback: ttAuthCallback
- Validates
stateby readingoauthStates/{state}; rejects if missing. - Exchanges
codefor tokens viaPOST https://open.tiktokapis.com/v2/oauth/token/withclient_key,client_secret, and exactredirect_uri. - Computes
expiresInandexpiresAtfrom the token response. - Fetches profile for UI via user info endpoint:
- Preferred:
GET https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name - Fallback: same path without the trailing slash (some gateways reject the slash).
- Authorization:
Bearer <access_token>.
- Preferred:
- Maps the response to a compact display profile:
platformId:open_iddisplayName:display_nameusername: if present in the payloadavatarUrl:avatar_urlaccountType:'user'
- Persists to Firestore at
users/{uid}/integrations/tiktok(merge):accessToken,refreshToken,tokenType,expiresIn,expiresAtopenId,scopeprofile(display profile for UI)extra.profile(raw response for debugging/tracking)updatedAtserver timestamp
- Deletes the
oauthStates/{state}document. - Redirects back to
returnTowith?tiktok=connectedif provided, or returns a plain success page.
3) Refreshing access tokens
ttRefreshNow(HTTP): Verifies the caller’s Firebase ID token, then refreshes this user’s TikTok tokens by calling the internal helperrefreshTikTokForUid(uid).ttRefreshSweep(scheduled every 12h):- Scans
users/*/integrations/tiktok. - If
expiresAtis within 6 hours (or missing) and arefreshTokenexists, we call the refresh helper. - Writes new
accessToken, optionally newrefreshToken, carries forwardscope, and updatesexpiresIn/At.
- Scans
Multi-Account Support
Users can connect multiple TikTok accounts. Each account is stored separately with a unique accountId.
Storage path: users/{uid}/integrations/tiktok/{accountId}
Where accountId is the TikTok user ID (openId).
Account selection:
- If user has one account: Automatically selected, no
accountIdrequired in API requests - If user has multiple accounts:
accountIdis required in API requests (e.g.,/v1/posts) - Missing required
accountIdreturns400error with codemissing_account
Data model
Firestore path: users/{uid}/integrations/tiktok/{accountId}
Fields we persist:
accountId: Unique identifier for this TikTok account (same asopenId)- Tokens and expiry:
accessToken,refreshToken,tokenType,expiresIn,expiresAt,updatedAt - Identity:
openId,scope - UI profile card (
profile):platformId,displayName,username,avatarUrl,accountType: 'user'
- Debug payload (
extra.profile): raw structure from the user info endpoint enabled_for_repost: (optional) boolean flag for automatic reposting
We intentionally avoid storing additional user stats and private details beyond what we need for UI and posting. Stats can be queried on demand using the granted scopes.
Publishing
TikTok publishing is implemented via publishTiktok.
Requirements:
request.post.content.textis required- Provide either a video or a single image:
videoUrl: string(preferred; we attempt to rehost for stability)- or
imageUrls[]/mediaUrls[](first image used; rehosted when possible)
Flow:
- Validates content, loads integration and refreshes token if expiring
- Builds init request:
- Video:
POST /v2/post/publish/video/init/with{ source: 'PULL_FROM_URL', video_url } - Photo:
POST /v2/post/publish/content/init/with{ media_type: 'PHOTO', photo_images: [url] }
- Video:
- Polls
/v2/post/publish/status/query/to resolvepost_idandshare_url - Records per‑platform run status and final results in Firestore; writes a user‑visible post via
recordPublishedPost
Constraints:
- Title derived from text (150 chars); private (
privacy_level: SELF_ONLY) by default - Retries token refresh on 401 with stored
refreshToken
Endpoint: POST /publishTiktok Body { userId, postSubmissionId, request }
Observability
- All critical steps log with clear prefixes:
[ttAuthStart]building authorize URL and validating redirect URI[ttAuthCallback]token exchange results and user info fetch status (including status/body snippet on failure)[ttRefresh]and[ttRefreshSweep]token rotation outcomes
- On profile fetch failure we log status and a truncated body to help identify issues like bad HTTP method or unsupported path responses.
Security posture
- We require a Firebase ID token for all user‑initiated endpoints and store tokens under the user’s document.
- We only persist the minimum necessary profile fields for UI; full provider payloads go under
extra.profilefor auditing and debugging. - We follow our engineering guardrails on secret handling and server‑side token exchange.
Local development and verification
- Use
?desktop=1when starting auth to use the localhost callback: this supports local desktop/Next.js dev flows. - The callback endpoint also serves TikTok site‑verification
.txtresponses when the platform probes specific verification paths.
Gotchas and troubleshooting
- TikTok requires an exact
redirect_urimatch with no query string or fragment. We validate and reject invalid configurations early. - The user info endpoint is a GET with
fieldsin the query; using POST or an incorrect path can return404 Unsupported path (Janus)or405. - If
displayName/avatarUrlare missing in Firestore, check that the grant includesuser.info.basicanduser.info.profile(we enforce these, but older connections may need a reconnect). - Look for
[ttAuthCallback] user/info failedlogs to diagnose endpoint/method issues.
Related code
Frontend
See also: Backend
Hooks and services:
apps/web/src/features/integrations/hooks/useConnectIntegration.ts→startAuth('tiktok', ...)with desktop override for local.apps/web/src/features/integrations/components/PlatformPicker.tsxrendersintegrations.tiktok.profile.
Note: The web app fans out via the Public API orchestrator; there is no direct client publish hook to publishTiktok.
apps/functions/integrations/tiktok/auth.js:ttAuthStart,ttAuthCallback,ttRefreshNow,ttRefreshSweepapps/functions/integrations/tiktok/post.js:publishTiktokapps/functions/integrations/tiktok/poll.js:pollTiktok,pollerTiktok,scheduledTikTokPollapps/functions/integrations/tiktok/organic-post-trigger.js:onOrganicPostCreatedapps/functions/integrations/tiktok/metrics.js:ttMetricsCollector,scheduledTikTokMetricsapps/web/src/features/integrations/components/PlatformPicker.tsx: readsintegrations.tiktok.profileto render the card in the UI
External references
- TikTok Open API documentation:
https://developers.tiktok.com/doc/open-api-overview/ - User info endpoint reference:
https://developers.tiktok.com/doc/open-api-get-user-info/