TikTok Ads setup

Create the TikTok Marketing API app behind the TikTok Ads channel, set the API environment, get the app approved, and know what a sandbox can and cannot prove.

How to create the TikTok Marketing API app that backs the TikTok Ads channel, what to set in the API environment, and what TikTok's approval process will ask for.

TikTok approved the app (see Approval) and a real advertiser connected on 2026-10-09. That first run verified the consent round trip, the advertiser picker, the campaign / ad group / ad lists, the pixel list and the whole Change Log export against the live API. It also found that the approved scope list cannot read the integrated report, so no spend or conversion number has been read yet — see What the first real run found. Writes, report rows and pixel event counts are still as TikTok's reference describes them, not as observed. The step numbers and portal labels are TikTok's flow as documented, not a transcript.


This is not the Social Poster's TikTok app

Marketing OS talks to TikTok through two unrelated developer apps, and confusing them is the single most likely way to lose an afternoon:

Social Poster (organic)TikTok Ads (this document)
ProductLogin Kit + Content Posting APIMarketing API
Hostopen.tiktokapis.combusiness-api.tiktok.com
Grantsa creator's accountan advertiser in Business Center
EnvTIKTOK_CLIENT_KEY / TIKTOK_CLIENT_SECRETTIKTOK_ADS_APP_ID / TIKTOK_ADS_APP_SECRET
Setupdocs/social-publishing-setup.mdthis file

They share no credential, no consent screen and no database row. A deployment can have either, both or neither, and revoking one leaves the other working. On the Integrations tab they are two separate cards: TikTok Ads sits with Meta and Google Ads under the ad platforms, and TikTok sits under Social publishing.

1. Create the app

  1. Sign in to the TikTok for Business developer portal with an account that is an admin of the TikTok Business Center holding the advertiser you want to read. Business Center membership is what decides which advertisers the grant can reach — not the app.

  2. My Apps → Create an app. Give it the product name, a description and a privacy-policy and terms URL (the public site's /privacy and /terms serve).

  3. Add the Marketing API product and select the scopes:

    • Ad Account Management — read (the advertiser list, name, currency, timezone)
    • Ads Management — read (campaigns, ad groups, ads)
    • Reporting — the whole category, not one leaf. The integrated report (/report/integrated/get/) is not listed under any of its leaves, and an app holding only Creative fatigue insight is refused it with 40001 "The access token lacks the required scope" (observed 2026-10-09).
    • Read pixels — the pixel list behind measurement health
    • Pixel level reporting — the per-event counts behind the same
    • Change Log — read (what a human changed in Ads Manager; see Change Log below)
    • Creative fatigue insight — read (/creative_fatigue/get/, behind creative fatigue below). Allowlist-only: selecting the scope is not enough, TikTok has to enable the feature for the app (the app page's Allowlist Management → Apply), and until they do every call is refused with code 40118. The refusal is handled — the sync skips the fatigue pass and still completes — so the app is safe to run without it.
    • Ads Management — write, only if you want the registry's TikTok write tools to work (MARK-I79359: budget, status and campaign/ad-group creates)

    Ask for nothing beyond that. A scope you are not calling is something the reviewer will ask about — and the reverse is worse: TikTok's review can check that you call what you asked for, so a requested Change Log, Pixel level reporting or Creative fatigue insight scope with no /changelog/, /pixel/ or /creative_fatigue/ call against it is a question you do not want.

  4. Set the Advertiser authorization URL (TikTok's name for the redirect URI) to the API callback, exactly, character for character:

    http://localhost:4000/integrations/tiktok-ads/callback      # local dev
    https://<api-host>/integrations/tiktok-ads/callback          # production

    TikTok reads this from the app's own configuration rather than from the authorize request, which is why a mismatch shows up as a consent screen that never completes rather than as an error message. Both entries can coexist.

  5. Copy the App ID and Secret from the same page.

2. The API environment

sh
TIKTOK_ADS_APP_ID=…            # numeric; TikTok parses it as an int64
TIKTOK_ADS_APP_SECRET=…
TIKTOK_ADS_REDIRECT_URI=https://<api-host>/integrations/tiktok-ads/callback
TIKTOK_ADS_API_VERSION=v1.3    # optional; pins the API version

Until both the id and the secret are set, the TikTok Ads card reads "not configured" and its connect route answers 503. Nothing else in the app degrades — the hourly sync simply has nothing to enqueue.

3. Approval

Status: approved. The production app "Marketing OS" was submitted on 2026-09-22 and its approval was recorded on 2026-10-08, with all 11 requested scopes: Ad account information, Campaign / Ad group / Ad (read and create-and-update each), Change log, Creative fatigue insight, Read pixels and Pixel level reporting. Audience management was not requested, so audience sync's TikTok cells stay unavailable; asking for it later is a new scope request on the approved app, not a new app — an approved app's detail page has an edit pencil beside Scope of permission. A Pending or denied app cannot be edited in the portal — changing an answer means deleting it and creating it again — and the App ID and Secret render -- until the app is approved.

The rest of this section describes the wait, for whoever creates another app.

An app can authorize an advertiser before it is approved, and then be refused on every read. That is the failure mode to expect while you wait: the connect succeeds, the advertiser picker fills in, and the first sync records a 40001 / 40002 refusal as the account's sync error.

TikTok's review asks for roughly what Meta's does:

  • a description of what the integration does, in product terms;
  • a screen recording of the whole flow: a user connecting, choosing an advertiser, and the resulting data appearing in the product. Record it from the Integrations tab through to /ads-manager with the TikTok tab selected;
  • the privacy policy and terms URLs from step 1;
  • confirmation that you request only the scopes you use.

Rate limits are per app and per advertiser. The hourly incremental pulls one advertiser-info call, three entity lists and one report per account per hour, which is far inside the default quota; a first backfill is INITIAL_SYNC_MONTHS months in 30-day chunks, so it is a handful of extra report calls once.

4. Sandbox

TikTok's developer portal offers a sandbox advertiser — a fake ad account with fake entities you can authorize before approval. It is genuinely useful for the connect round trip and the entity sync.

What it cannot prove: reporting. A sandbox advertiser has no delivery, so report/integrated/get/ answers rows of zeros (or nothing at all), and pixel/event/stats/ has no events to report health on for the same reason. A sandbox run therefore verifies the OAuth flow, the advertiser picker, the entity mirror and the ads manager's empty state — and says nothing about whether spend, conversions, revenue or pixel health are read correctly.

For that, the honest test is the hermetic one: apps/api/test/tiktok-ads-sync.spec.ts drives the real client and the real processor against a scripted Marketing API and asserts the unit conversions, which is what would actually be wrong.

5. Verify a connection

  1. Settings → Integrations → Connect TikTok Ads. You should land on TikTok's consent screen, and come back to /settings/integrations?tiktokAds=connected with the advertiser picker open.
  2. Pick an advertiser and save. The card should show the advertiser's name, id, currency and timezone, with a pending → syncing → synced badge and "First backfill in progress" underneath.
  3. Open /ads-manager and choose the TikTok tab. Campaigns, then ad groups, then ads, with spend and conversions over the selected window.

If the card shows a sync error instead, it carries TikTok's own message. The three worth recognizing:

MessageWhat it means
"TikTok refused the saved credential — reconnect…"Code 40105: the grant was revoked in Business Center, or the identity lost access to the advertiser.
"…the Marketing API app needs an approved access level…"The app is authenticated but not approved for this call, or the advertiser is outside what it may read.
"No active TikTok Ads connection…"The connection row is expired or disconnected; reconnect.

What the first real run found

Connected on 2026-10-09 to a new advertiser with no campaigns, pixel or spend.

CallTikTok answered
consent → callback → advertiser pickerworked; an identity with no ad account connects with an empty picker
advertiser/info/, the three entity lists, pixel/list/code 0 (empty lists)
Change Log create → check → downloadcode 0, an empty export
report/integrated/get/40001 — "The access token lacks the required scope for endpoint …"
creative_fatigue/get/40118 — "The API is not in allow list. Please apply for the allow list…"

The whole Reporting category was requested the same day (the app row reads Approved · Scope of Permissions Change Pending; the existing scopes keep working while TikTok reviews it, and a scope change asks for a written reason).

So the card shows a sync error until that request is approved and the advertiser is reconnected — a token keeps the scopes it was issued with.

Once the scope change was approved and the advertiser reconnected (by 2026-10-15), the report was refused again, this time for a different reason:

CallTikTok answered
report/integrated/get/ (as first built)40002 — "Invalid metric fields: ['total_onsite_form']"
report/integrated/get/ (corrected)code 0, OK (an empty list: no campaigns yet)

The on-site form metric is onsite_form. total_onsite_form came from the docs and had never been sent live, and one invalid metric fails the whole report. form and user_registration (the website-pixel Submit form and Complete registration events) are accepted too, but the lead split does not ask for them yet. A 40002 is a parameter error, not a permission problem, so the card now shows TikTok's sentence without the "check the scope" hint. 40001 is TikTok's catch-all (a missing scope, an advertiser the identity cannot operate and a pixel it does not own all use it), so the card now quotes TikTok's own sentence after the code.

Two things worth knowing if you connect a brand-new TikTok login:

  • An identity with no ad account completes consent and lands on an empty picker ("That TikTok login authorized no advertisers"). Create the ad account in TikTok Ads Manager; it then appears in the picker without reconnecting.
  • TikTok may put a CAPTCHA and an emailed code in front of the consent's Confirm. That is TikTok's, not ours.

6. Change Log

Since MARK-89VFAE the hourly sync also pulls TikTok's Change Log, so a change someone made in Ads Manager appears on the same per-entity timeline as the changes Marketing OS made itself — open a campaign, ad group or ad in /ads-manager and it is in the drawer, badged In TikTok Ads Manager.

Two things about that API are worth knowing before you debug it:

  • It is an asynchronous export, not a list endpoint. One pull is three calls: /changelog/task/create/ starts a task, /changelog/task/check/ is polled until it reports SUCCESS, and /changelog/task/download/ returns the window as a CSV. TikTok removed page / page_size from it in v1.3, so there is no paging to tune.
  • Its window is capped at 30 days and is asked for in a fixed UTC+00:00 zone (Atlantic/Reykjavik), because the file's timestamps carry no offset. The pull is watermarked on the account (changeLogSyncedThrough) and overlaps its previous window by an hour to catch a late-landing row; a re-read is harmless because a row's identity is a digest of its own content.

What you will not see on the timeline: the changes Marketing OS made through its own tools. Those come back in the export carrying our app id, and they are already on the timeline as tool invocations — with their risk tier, their guardrail verdict and their Revert button — so the mirrored copy is dropped rather than shown twice.

The file arrives as JSON wrapped around a Python bytes repr ({"file_data": "b'…'", "file_name": "….csv"}) — not the Python dict of TikTok's documented example — and a status check sent immediately after the create is answered 40100 "Request too frequent." Both are handled.

If the scope has not been approved yet, the change-log pull is refused with a 40001 / 40002 while everything else keeps working. That refusal is logged and skipped on purpose: the account's sync badge stays green, the timeline simply has nothing TikTok-sourced on it, and nothing else degrades.

A change made in TikTok can never be undone from Marketing OS. There is no snapshot of ours behind it, so the entry is context rather than a decision with a missing button.

7. Pixel health in measurement health

Since MARK-0GDBF1 the measurement health panel judges TikTok alongside Meta and GA4. A business with a linked TikTok advertiser gets three extra checks on its home dashboard:

CheckReadsFails when
TikTok pixel installedpixel/list/the advertiser has no pixel at all
TikTok browser vs server events (Events API)pixel/event/stats/no events in the window, or none server-side
TikTok events attributed to adspixel/event/stats/events exist and TikTok attributed none of them

The third is the one Meta cannot answer. Meta exposes no event match quality at any scope through the Graph API — it exists only in Events Manager — so attributed_count is the closest this product gets to that signal on any channel.

Three things to know before reading a verdict:

  • A business with no TikTok advertiser is unaffected. All three checks report unknown with the reason, and unknown is excluded from the roll-up verdict, so such a business sees exactly what it saw before.
  • A dead TikTok pixel can fail the business on its own, exactly as a dead Meta pixel can. The roll-up is "worst judged check wins"; there is no per-channel weighting.
  • The TikTok read fails independently. A revoked TikTok grant leaves the Meta and GA4 halves of the report intact and says why the TikTok half is blank, which is the same contract the other three gathers hold.

There is no /pixel/event/health_reporting/

Worth writing down, because the name is plausible and the endpoint is not real: TikTok Marketing API v1.3 has no pixel/event/health_reporting/. It is in neither TikTok's own SDK nor the published v1.3 contract. The endpoint the Pixel level reporting scope carries is pixel/event/stats/.

Two of its limits shape the code, and both are tighter than the entity endpoints':

  • pixel/list/ caps page_size at 20 (the entity endpoints take 1000) and answers its rows under data.pixels, not data.list.
  • pixel/event/stats/ takes at most 10 pixel ids and a date_range of at most 30 days, in UTC. The 7-day measurement window and the 5-pixel read cap both sit inside those.

8. Creative fatigue

TikTok's own Creative Fatigue Detection — not a metric we derive. It answers, per ad per day, whether that creative has burnt out, plus the evidence behind the verdict.

  • Where it shows up: /creatives → the TikTok fatigue tab. Each row is one TikTok ad with TikTok's latest verdict, the fatigue index, daily new users, the DNU ratio and CPA, and a day-per-square strip for the window. Click a row for every day TikTok reported.
  • What is stored: one row per (ad, date) in tiktok_ads_creative_fatigue, written by the hourly sync. The three currency fields land as integer cents like every other TikTok amount; the index and the ratios are plain floats.
  • The window is TikTok's: /creative_fatigue/get/ only answers for the last 60 days and its end_date is exclusive, so the sync asks for [today-29, today) — the freshest reading is yesterday's. A day-over-day signal has nothing to say about a partial day.
  • It is a per-ad call. ad_id is required, so the pass asks only about ads that actually delivered inside the window, highest spend first, capped at 200 ads per sync. An ad with no impressions has no reading to give.

Why the tab can be empty

Three different reasons, and the tab says which:

What you seeWhy
"No TikTok advertiser linked…"No advertiser is linked for this business — do step 5 first.
"No fatigue readings for this window…"Either the app is not allowlisted for Creative Fatigue Detection, or no ad delivered enough for TikTok to judge it.
A row with an em dash in a columnTikTok reported the day but not that measure. An em dash is "not reported"; $0.00 is a reported zero.

The allowlist case is silent on purpose: the fatigue pass logs its refusal once per sync (TikTok creative fatigue unavailable for advertiser …) and the sync still finishes as synced, because a signal we were never granted is not a broken integration. Check the API log if you need to tell the two empties apart.

CPA is reported twice — cost_per_conversion and skan_cost_per_conversion — and TikTok returns 0.0 for whichever one does not apply (SKAN is the iOS 14 Dedicated Campaign one). The tab shows whichever is non-zero.

What this channel does and does not do

Reads are the bulk of it, but it is no longer read-only: since MARK-I79359 budgets, delivery statuses and new campaigns / ad groups can be changed — always through the tool registry, never an HTTP route, so the risk tiers, the money guardrails and the audit log apply to every one of them (ADR 0018). Those writes need the Ads Management write scope from step 1.

TikTok spend does count against a project's monthly spend cap, so a cap set for a business running all three channels is a cap on all three.

The pixel reads in section 7 are read-only. In particular this app does not request the Measurement scope that carries /event/track/, the server-side Events API send: Marketing OS reports on somebody's event setup, it does not send events on their behalf.


Something wrong or missing on this page? Tell us and we will fix it.