Skip to main content

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: FrontendBackend

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_name and avatar_url are always retrievable.
  • Follow TikTok Open API constraints: exact redirect_uri match with no query string; correct HTTP methods and paths.

Why we do it this way

  • TikTok returns user profile fields (including display_name and avatar_url) only when the grant includes user.info.basic and sometimes user.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 fields in 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 profile for UI (platformId, displayName, username, avatarUrl, accountType: 'user') and keep the raw response under extra.profile for observability without widening our primary schema.
  • Long‑lived stability: we persist the refresh_token and 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 the uid.
  • Creates a state document at oauthStates/{state} with { uid, provider: 'tiktok', returnTo }.
  • Builds the authorize URL https://www.tiktok.com/v2/auth/authorize/ with:
    • client_key
    • scope (merged set described above)
    • response_type=code
    • redirect_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 use http://localhost:3000/api/tiktok/callback as the redirect to support Desktop flow during dev.

2) Callback: ttAuthCallback

  • Validates state by reading oauthStates/{state}; rejects if missing.
  • Exchanges code for tokens via POST https://open.tiktokapis.com/v2/oauth/token/ with client_key, client_secret, and exact redirect_uri.
  • Computes expiresIn and expiresAt from 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>.
  • Maps the response to a compact display profile:
    • platformId: open_id
    • displayName: display_name
    • username: if present in the payload
    • avatarUrl: avatar_url
    • accountType: 'user'
  • Persists to Firestore at users/{uid}/integrations/tiktok (merge):
    • accessToken, refreshToken, tokenType, expiresIn, expiresAt
    • openId, scope
    • profile (display profile for UI)
    • extra.profile (raw response for debugging/tracking)
    • updatedAt server timestamp
  • Deletes the oauthStates/{state} document.
  • Redirects back to returnTo with ?tiktok=connected if 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 helper refreshTikTokForUid(uid).
  • ttRefreshSweep (scheduled every 12h):
    • Scans users/*/integrations/tiktok.
    • If expiresAt is within 6 hours (or missing) and a refreshToken exists, we call the refresh helper.
    • Writes new accessToken, optionally new refreshToken, carries forward scope, and updates expiresIn/At.

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 accountId required in API requests
  • If user has multiple accounts: accountId is required in API requests (e.g., /v1/posts)
  • Missing required accountId returns 400 error with code missing_account

Data model

Firestore path: users/{uid}/integrations/tiktok/{accountId}

Fields we persist:

  • accountId: Unique identifier for this TikTok account (same as openId)
  • 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.text is 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:

  1. Validates content, loads integration and refreshes token if expiring
  2. 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] }
  3. Polls /v2/post/publish/status/query/ to resolve post_id and share_url
  4. 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.profile for auditing and debugging.
  • We follow our engineering guardrails on secret handling and server‑side token exchange.

Local development and verification

  • Use ?desktop=1 when starting auth to use the localhost callback: this supports local desktop/Next.js dev flows.
  • The callback endpoint also serves TikTok site‑verification .txt responses when the platform probes specific verification paths.

Gotchas and troubleshooting

  • TikTok requires an exact redirect_uri match with no query string or fragment. We validate and reject invalid configurations early.
  • The user info endpoint is a GET with fields in the query; using POST or an incorrect path can return 404 Unsupported path (Janus) or 405.
  • If displayName/avatarUrl are missing in Firestore, check that the grant includes user.info.basic and user.info.profile (we enforce these, but older connections may need a reconnect).
  • Look for [ttAuthCallback] user/info failed logs to diagnose endpoint/method issues.

Frontend

See also: Backend

Hooks and services:

  • apps/web/src/features/integrations/hooks/useConnectIntegration.tsstartAuth('tiktok', ...) with desktop override for local.
  • apps/web/src/features/integrations/components/PlatformPicker.tsx renders integrations.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, ttRefreshSweep
  • apps/functions/integrations/tiktok/post.js: publishTiktok
  • apps/functions/integrations/tiktok/poll.js: pollTiktok, pollerTiktok, scheduledTikTokPoll
  • apps/functions/integrations/tiktok/organic-post-trigger.js: onOrganicPostCreated
  • apps/functions/integrations/tiktok/metrics.js: ttMetricsCollector, scheduledTikTokMetrics
  • apps/web/src/features/integrations/components/PlatformPicker.tsx: reads integrations.tiktok.profile to 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/