Skip to content

TikTok Ads Data Source #339

@mustafaneguib

Description

@mustafaneguib

[PHASE 3] TikTok Ads Data Source

Phase

Phase 3 — Data Source Expansion | Depends on: Issues 01, 04. Can be built in parallel with Issues 09 and 10.

Description

TikTok Ads has become a primary performance marketing channel for B2C brands targeting audiences aged 18-40. Many marketing teams running campaigns across Google, Meta, and LinkedIn also run substantial budgets on TikTok, particularly for video-led awareness and retargeting campaigns. Without TikTok data, the Marketing Hub's channel comparison table (Issue 06) has a visible gap, and CPL/ROAS benchmarking across channels is incomplete.

This issue adds TikTok Ads as a fourth digital advertising data source, following the same architectural pattern established for Google Ads, Meta Ads, and LinkedIn Ads. The connector uses the TikTok Marketing API via OAuth 2.0 with advertiser account selection.

What currently exists (do NOT rebuild)

Layer File Status
Meta Ads OAuth pattern backend/src/services/MetaAdsService.ts ✅ Pattern to follow for TikTok OAuth
dra_meta_ads schema Existing tables ✅ Schema structure to mirror
DataSourceProcessor sync dispatch backend/src/processors/DataSourceProcessor.ts ✅ Extend
Marketing Hub channel normalisation backend/src/processors/MarketingReportProcessor.ts (Issue 06) ✅ Add TikTok channel
Classification system Issue 04 ✅ Auto-classified as marketing_campaign_data

First Principles Rationale

Completeness of the channel comparison view is a key value driver for the Marketing Hub. A CMO managing £500K/month in ad spend across four platforms who cannot see TikTok in the same table as Google and Meta must do external reconciliation — which is exactly the problem this platform solves. TikTok's Marketing API is well-documented and follows standard OAuth + reports pattern. The implementation effort is proportional to the existing ad platform connectors making this a logical inclusion in Phase 3.


Acceptance Criteria

  • tiktok_ads is a selectable data source type in the "Connect Data Source" flow with TikTok logo and description
  • Connection uses TikTok Marketing API OAuth 2.0 — user is redirected to TikTok Business Centre to authorise
  • After OAuth, user selects their TikTok Advertiser Account from a dropdown (fetched via /open_api/v1.3/oauth2/advertiser/get/)
  • Manual sync triggers a pull from the TikTok Reporting API (/open_api/v1.3/report/integrated/get/)
  • Synced data stored in dra_tiktok_ads schema
  • Automatic sync follows existing SchedulerService pattern
  • TikTok Ads channel appears in the Marketing Hub channel comparison table showing: spend, impressions, clicks, CPM, CTR, conversions, CPL, ROAS
  • Classification: auto-classified as marketing_campaign_data — no classification modal shown
  • npm run validate:ssr passes

Technical Implementation Plan

1. EDataSourceType Extension

// backend/src/types/EDataSourceType.ts
export enum EDataSourceType {
    // ...existing values...
    TIKTOK_ADS = 'tiktok_ads'
}

Migration: Add 'tiktok_ads' to dra_data_sources_data_type_enum PostgreSQL enum.

2. TikTok Ads Database Schema (dra_tiktok_ads)

Migration [timestamp]-CreateTikTokAdsSchema.ts:

CREATE SCHEMA IF NOT EXISTS dra_tiktok_ads;

CREATE TABLE dra_tiktok_ads.advertiser_info (
    id              SERIAL PRIMARY KEY,
    data_source_id  INTEGER NOT NULL REFERENCES dra_data_sources(id) ON DELETE CASCADE,
    advertiser_id   VARCHAR(50) NOT NULL,
    advertiser_name VARCHAR(512),
    currency        VARCHAR(10),
    timezone        VARCHAR(50),
    synced_at       TIMESTAMP
);

CREATE TABLE dra_tiktok_ads.campaign_performance (
    id              SERIAL PRIMARY KEY,
    data_source_id  INTEGER NOT NULL REFERENCES dra_data_sources(id) ON DELETE CASCADE,
    stat_date       DATE NOT NULL,
    campaign_id     VARCHAR(50) NOT NULL,
    campaign_name   VARCHAR(512),
    objective_type  VARCHAR(100),   -- 'REACH' | 'VIDEO_VIEWS' | 'CONVERSIONS' | 'APP_INSTALL' etc.
    status          VARCHAR(50),
    spend           NUMERIC(12,2) DEFAULT 0,
    impressions     BIGINT DEFAULT 0,
    clicks          INTEGER DEFAULT 0,
    ctr             NUMERIC(8,4) DEFAULT 0,      -- click-through rate
    cpm             NUMERIC(10,4) DEFAULT 0,      -- cost per mille impressions
    cpc             NUMERIC(10,4) DEFAULT 0,      -- cost per click
    conversions     INTEGER DEFAULT 0,
    cost_per_conversion NUMERIC(10,2) DEFAULT 0,
    conversion_rate NUMERIC(8,4) DEFAULT 0,
    video_views     BIGINT DEFAULT 0,             -- TikTok-specific
    video_play_actions BIGINT DEFAULT 0,          -- 6-second views
    reach           BIGINT DEFAULT 0,
    UNIQUE(data_source_id, stat_date, campaign_id)
);

CREATE TABLE dra_tiktok_ads.ad_group_performance (
    id              SERIAL PRIMARY KEY,
    data_source_id  INTEGER NOT NULL REFERENCES dra_data_sources(id) ON DELETE CASCADE,
    stat_date       DATE NOT NULL,
    campaign_id     VARCHAR(50) NOT NULL,
    ad_group_id     VARCHAR(50) NOT NULL,
    ad_group_name   VARCHAR(512),
    spend           NUMERIC(12,2) DEFAULT 0,
    impressions     BIGINT DEFAULT 0,
    clicks          INTEGER DEFAULT 0,
    conversions     INTEGER DEFAULT 0,
    cpl             NUMERIC(10,2) DEFAULT 0,
    UNIQUE(data_source_id, stat_date, ad_group_id)
);

3. TikTokAdsService (backend/src/services/TikTokAdsService.ts)

export class TikTokAdsService {
    private static instance: TikTokAdsService;
    public static getInstance(): TikTokAdsService { ... }

    private readonly BASE_URL = 'https://business-api.tiktok.com/open_api/v1.3';

    // List advertiser accounts for the authenticated user
    async listAdvertiserAccounts(accessToken: string): Promise<Array<{ id: string; name: string; currency: string }>>

    // Sync campaign performance data
    async syncCampaignPerformance(
        dataSourceId: number,
        advertiserId: string,
        accessToken: string,
        startDate: string,   // YYYY-MM-DD
        endDate: string
    ): Promise<void>

    // Sync ad group performance data
    async syncAdGroupPerformance(
        dataSourceId: number,
        advertiserId: string,
        accessToken: string,
        startDate: string,
        endDate: string
    ): Promise<void>

    // Refresh access token (TikTok tokens expire after 24 hours)
    async refreshAccessToken(refreshToken: string): Promise<{ access_token: string; expires_in: number }>
}

TikTok Reporting API call pattern:

const response = await fetch(`${this.BASE_URL}/report/integrated/get/`, {
    method: 'POST',
    headers: {
        'Access-Token': accessToken,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        advertiser_id: advertiserId,
        report_type: 'BASIC',
        dimensions: ['campaign_id', 'stat_time_day'],
        metrics: ['spend', 'impressions', 'clicks', 'ctr', 'cpm', 'cpc',
                  'conversions', 'cost_per_conversion', 'conversion_rate',
                  'video_play_actions', 'reach'],
        start_date: startDate,
        end_date: endDate,
        page_size: 1000
    })
});

4. OAuth Flow

TikTok Marketing API OAuth 2.0:

Authorization URL:

https://business-api.tiktok.com/portal/auth?app_id=[APP_ID]&state=[STATE]&redirect_uri=[URI]&scope=reporting,campaign_management

Token exchange: POST https://business-api.tiktok.com/open_api/v1.3/oauth2/access_token/

Token storage: follows the existing encrypted connection_details pattern. Store access_token, refresh_token, advertiser_id, expires_at.

Environment variables needed:

TIKTOK_ADS_APP_ID=...
TIKTOK_ADS_APP_SECRET=...
TIKTOK_ADS_REDIRECT_URI=...

5. Backend OAuth Route

Add backend/src/routes/oauth/tiktok.ts following the pattern of backend/src/routes/oauth/google.ts and backend/src/routes/oauth/linkedin.ts.

GET  /oauth/tiktok            — initiate OAuth redirect
GET  /oauth/tiktok/callback   — handle code exchange + save tokens

6. DataSourceProcessor Extension

Add to the sync dispatch:

case EDataSourceType.TIKTOK_ADS:
    await tikTokService.syncCampaignPerformance(dataSourceId, advertiserId, accessToken, startDate, endDate);
    await tikTokService.syncAdGroupPerformance(dataSourceId, advertiserId, accessToken, startDate, endDate);
    break;

7. MarketingReportProcessor Integration (Issue 06)

Add TikTok to getChannelMetrics():

// Query dra_tiktok_ads.campaign_performance for the date range
const tikTok: IChannelMetrics = {
    channelType: 'tiktok_ads',
    channelLabel: 'TikTok Ads',
    spend: sum(spend),
    impressions: sum(impressions),
    clicks: sum(clicks),
    ctr: total_clicks / total_impressions,
    conversions: sum(conversions),
    cpl: total_spend / total_conversions,
    roas: 0,      // TikTok does not report revenue unless conversion tracking is configured
    pipelineValue: 0,
    dataSourceId: dataSource.id
};

8. Frontend Composable (frontend/composables/useTikTokAds.ts)

Following the pattern of useGoogleAds.ts / useLinkedInAds.ts:

  • Initiate OAuth: redirect to /oauth/tiktok
  • After callback, fetch advertiser accounts and present selector
  • Save selected advertiser to data source

Files to Create

File Purpose
backend/src/services/TikTokAdsService.ts TikTok Marketing API sync service
backend/src/routes/oauth/tiktok.ts OAuth initiate + callback routes
backend/src/migrations/[timestamp]-AddTikTokAdsDataSourceType.ts Extend enum
backend/src/migrations/[timestamp]-CreateTikTokAdsSchema.ts dra_tiktok_ads tables
frontend/composables/useTikTokAds.ts Frontend OAuth + data composable

Files to Modify

File Change
backend/src/types/EDataSourceType.ts Add TIKTOK_ADS = 'tiktok_ads'
backend/src/processors/DataSourceProcessor.ts Dispatch TikTok sync
backend/src/processors/MarketingReportProcessor.ts Add TikTok channel row
backend/src/server.ts Register TikTok OAuth routes
frontend/utils/dataSourceClassifications.ts (Issue 04) Add 'tiktok_ads' to AUTO_CLASSIFIED_SOURCE_TYPES
Frontend data source connect UI Add TikTok Ads connector card
docker/backend/ env config Add TikTok env variables

Required Environment Variables

TIKTOK_ADS_APP_ID=
TIKTOK_ADS_APP_SECRET=
TIKTOK_ADS_REDIRECT_URI=

Add to .env.example and docker-compose.yml backend service environment section.


Dependencies

  • Requires: Issue 01 (Navigation), Issue 04 (Classification — auto-classified)
  • Enhances: Issue 06 (Marketing Hub adds TikTok as a fourth paid channel row)
  • Can be built in parallel with Issues 09 and 10

Testing Requirements

  • listAdvertiserAccounts() returns accounts from the TikTok Business API with correct token
  • syncCampaignPerformance() handles TikTok's pagination (page_info.page cursor)
  • Token expiry (expires_at) check triggers refresh before API call
  • UPSERT on unique constraint (data_source_id, stat_date, campaign_id) prevents duplicate rows on sync
  • Deleting the dra_data_sources record cascades to all dra_tiktok_ads tables
  • TikTok-specific metrics (video_play_actions, reach) are stored even though they have no equivalent in other platforms
  • npm run validate:ssr passes

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions