Ads
Boost TikTok videos as Spark Ads, create video campaigns with pixel conversions, manage ad comments and run live TikTok reports.
A tiktokads connection runs TikTok's Business API: Spark Ads that promote organic videos, standalone video campaigns, website conversion campaigns on a TikTok pixel, customer list audiences, ad comment moderation and live reports with de-duplicated reach. Agencies get every advertiser under their Business Centers from one connection.
#Connect
Call GET /v1/connect/tiktok/ads with the profileId. TikTok Ads is a separate OAuth from TikTok posting, and TikTok grants its permissions at the app level, so there is no scope list to pick. You do not need your own TikTok developer onboarding.
| Connect with | What you get |
|---|---|
accountId of a connected TikTok posting account |
The ads connection links to it. Spark Ads and standalone ads can run as the real @username. |
No accountId (ads-only) |
Standalone ads run under a brand identity you configure. Spark Ads need the creator's Spark Code. |
Set the ads-only brand identity once with PATCH /v1/connect/tiktok-ads, or per ad with brandIdentity: { displayName, imageUrl } on the create. GET /v1/ads/tiktok-identities lists the identities an advertiser can run ads as.
GET /v1/ads/accounts?accountId=... returns every advertiser the connection reaches, including all advertisers under the Business Centers the user owns. GET /v1/ads/business-centers lists the Business Centers themselves with an advertiser count each, for an agency picker. The advertiser list is cached for an hour.
#Create a video campaign
TikTok's ad creative is video. On POST /v1/ads/create, the video URL goes in imageUrl (the field keeps its name across networks), body is the caption, and headline is ignored because TikTok ads have no headline slot.
curl -X POST https://api.pozy.ai/v1/ads/create \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tt-summer-drop-1" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c4f",
"adAccountId": "7301234567890123456",
"name": "Summer drop - US 18-34",
"goal": "traffic",
"budgetAmount": 50,
"budgetType": "daily",
"body": "The summer drop is live. Link in bio, or tap below.",
"imageUrl": "https://cdn.example.com/summer-drop-9x16.mp4",
"linkUrl": "https://shop.example.com/summer",
"callToAction": "SHOP_NOW",
"countries": ["US"],
"ageMin": 18,
"ageMax": 34
}'const res = await fetch("https://api.pozy.ai/v1/ads/create", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "tt-summer-drop-1",
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "66b2e19d8c3f5a7e9d0b1c4f",
adAccountId: "7301234567890123456",
name: "Summer drop - US 18-34",
goal: "traffic",
budgetAmount: 50,
budgetType: "daily",
body: "The summer drop is live. Link in bio, or tap below.",
imageUrl: "https://cdn.example.com/summer-drop-9x16.mp4",
linkUrl: "https://shop.example.com/summer",
callToAction: "SHOP_NOW",
countries: ["US"],
ageMin: 18,
ageMax: 34,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.ad.platformAdSetId); // the TikTok ad groupimport os, requests
resp = requests.post(
"https://api.pozy.ai/v1/ads/create",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": "tt-summer-drop-1",
},
json={
"accountId": "66b2e19d8c3f5a7e9d0b1c4f",
"adAccountId": "7301234567890123456",
"name": "Summer drop - US 18-34",
"goal": "traffic",
"budgetAmount": 50,
"budgetType": "daily",
"body": "The summer drop is live. Link in bio, or tap below.",
"imageUrl": "https://cdn.example.com/summer-drop-9x16.mp4",
"linkUrl": "https://shop.example.com/summer",
"callToAction": "SHOP_NOW",
"countries": ["US"],
"ageMin": 18,
"ageMax": 34,
},
timeout=120,
)
resp.raise_for_status()
print(resp.json()["ad"]["_id"])goal takes engagement, traffic, awareness, video_views, lead_generation, conversions or app_promotion. TikTok's minimum budget is $20 per ad group. smartPlus: true builds a Smart+ campaign; check each creative's review with GET /v1/ads/{adId}/tiktok-smart-plus-review and per-creative results with GET /v1/ads/tiktok-smart-plus-materials.
lead_generation sends the click to your own form on linkUrl. TikTok instant forms are not supported.
#Conversion campaigns
A website conversion campaign needs a TikTok pixel. Pass it in promotedObject, the same field Meta uses:
{
"goal": "conversions",
"promotedObject": { "pixelId": "7212345678901234567", "customEventType": "ON_WEB_ORDER" }
}| Field | Meaning |
|---|---|
pixelId |
The numeric pixel id, not the alphanumeric pixel code shown in Events Manager. List your pixels on Pixels and tags to find it. |
customEventType |
The event to optimize for, in TikTok's codes: ON_WEB_ORDER (complete payment), INITIATE_ORDER, ON_WEB_CART, ON_WEB_REGISTER, ON_WEB_DETAIL, FORM, ON_WEB_SEARCH, ON_WEB_ADD_TO_WISHLIST, LANDING_PAGE_VIEW. Optional for auto-bid. |
If a conversion ad group already exists in TikTok Ads Manager, pass its id as adSetId instead: the new ad inherits the pixel and event.
#Spark Ads
A Spark Ad promotes a published TikTok video and keeps the creator's handle, likes and comments. Call POST /v1/ads/boost:
{
"postId": "65f1c0a9e2b5af0012ab34cd",
"accountId": "66b2e19d8c3f5a7e9d0b1c4f",
"adAccountId": "7301234567890123456",
"name": "Spark - summer drop",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" },
"targeting": { "countries": ["US", "CA"] },
"linkUrl": "https://shop.example.com/summer",
"callToAction": "SHOP_NOW"
}targeting.countriesis required, unless you attach to an existing Smart+ ad group withadSetId.linkUrlandcallToActionadd a clickable destination. Without them the Spark Ad has no destination beyond the video itself.- To promote another creator's video, ask them for a Spark Code from the TikTok app's promote settings. Send it as
sparkAuthCodewith their video id inplatformPostId. Several creators at once go insparkPosts[]. - On an ads-only connection,
sparkAuthCodeandplatformPostIdare always required, because the connection has no TikTok identity of its own.
More boost options across networks are on Boost posts.
#Bidding
bidStrategy uses the shared names and maps to TikTok's bid types:
bidStrategy |
Needs | TikTok bid type |
|---|---|---|
LOWEST_COST_WITHOUT_CAP (default) |
No bid, maximum delivery | |
LOWEST_COST_WITH_BID_CAP |
bidAmount |
Custom bid |
COST_CAP |
bidAmount |
Custom bid (cost cap) |
LOWEST_COST_WITH_MIN_ROAS |
roasAverageFloor |
Minimum ROAS (value-optimized accounts only) |
It works on create, boost and PUT /v1/ads/ad-sets/{adSetId}. Reads normalize TikTok's bid type back to these names.
#Media
| Type | Format | Max size | Notes |
|---|---|---|---|
| Video | MP4, MOV, MPEG | 500 MB | 9:16 vertical, 720p or better, 5 to 60 seconds |
| Image | JPEG, PNG | 30 MB | 1080 x 1920 for full screen |
The video transfers in one request, so stay under 500 MB. URLs must be public.
#Manage running ads
| Job | Call |
|---|---|
| Pause or resume | PUT /v1/ads/campaigns/{campaignId}/status, /ad-sets/{adSetId}/status or /{adId}/status with platform: "tiktok" |
| Add an ad to an ad group built in TikTok Ads Manager | POST /v1/ads/create with adSetId (budget, targeting and goal are inherited) |
| Swap the creative | PUT /v1/ads/{adId} with creative: body becomes the ad text, linkUrl the landing page, videoUrl uploads a new video |
| Change targeting | PUT /v1/ads/{adId} with targeting |
| Duplicate a campaign | POST /v1/ads/campaigns/{campaignId}/duplicate with platform: "tiktok". The copy is created paused and keeps Spark Ad links. |
| Change budget or bid | PUT /v1/ads/ad-sets/{adSetId} |
Creating an ad group on its own is not available on TikTok. Use the create or boost calls, or attach to an existing ad group.
#Ad comments
GET /v1/ads/{adId}/comments reads comments on a TikTok ad, Spark Ads and dark posts included. TikTok also supports moderation from the same endpoints:
POST /v1/ads/{adId}/comments/{commentId}/replywithtextPOST /v1/ads/{adId}/comments/{commentId}/hidewithhidden: trueorfalseDELETE /v1/ads/{adId}/comments/{commentId}for your own comments
TikTok only looks for comments inside a date range of 30 days or less; without one you get the past 30 days. Give the range as since and until in YYYY-MM-DD, and reuse it when you reply to or hide a comment close to either end. Every comment says what you may do with it through canReply, canDelete and canHide. Pages can come back empty with pagination.hasMore still true, so follow the cursor until it is not.
#Targeting
Countries, regions, cities, ZIP codes and metros (create only), age, gender, languages, interests, behaviors, income tier and customer list audiences. Behaviors such as video:1913101 (watched or engaged with videos in a category in the last 15 days) or creator:24001 (followed creators in a category) restrict delivery to the TikTok placement. PUT /v1/ads/{adId} with targeting updates the ad group; sending a location replaces both location and ZIP targeting. More on Targeting.
#Reporting
GET /v1/ads/{adId}/analyticsreturns spend, impressions, clicks, CTR, CPM and video metrics.breakdownstakesgender,age,country_code,platform,ac(network type) andlanguage.GET /v1/ads/treeadds those metrics up for each campaign and ad group.GET /v1/ads/insightsasks TikTok for a report on the spot. SendadAccountId, adataLevel(AUCTION_ADVERTISER,AUCTION_CAMPAIGN,AUCTION_ADGROUPorAUCTION_AD), between 1 and 4dimensions, themetrics, and dates spanning 365 days or less.reportTypetakesBASICorAUDIENCE.
To count each person once across several campaigns, filter the set and group by country_code:
curl -G https://api.pozy.ai/v1/ads/insights \
-H "Authorization: Bearer $POZY_API_KEY" \
--data-urlencode "accountId=66b2e19d8c3f5a7e9d0b1c4f" \
--data-urlencode "adAccountId=7301234567890123456" \
--data-urlencode "dataLevel=AUCTION_CAMPAIGN" \
--data-urlencode "dimensions=country_code" \
--data-urlencode "metrics=reach,impressions,spend" \
--data-urlencode 'filtering=[{"field":"campaign_ids","operator":"IN","value":["1790000000000001","1790000000000002"]}]' \
--data-urlencode "fromDate=2026-09-01" \
--data-urlencode "toDate=2026-09-30"Rows come back as TikTok returns them, with metric values as strings. A filter finer than dataLevel (such as ad_ids at campaign level) returns 400. Pozy imports the last 90 days of metrics when an account connects. Async report runs are Meta only.
#Limits and errors
Not available on TikTok: catalog and TikTok Shop ads, instant forms, chunked video upload, standalone ad group creation, and the Meta-only tools (previews, creative library, reach and frequency, studies, async reports).
| Error | Cause | Fix |
|---|---|---|
422 ads_connection_required |
The profile has no tiktokads account, or the TikTok user has not been authorized as an identity of the advertiser |
Connect TikTok Ads, or authorize that identity in TikTok Ads Manager. |
40002: Please select a pixel |
A conversions goal with no promotedObject.pixelId |
Add the pixel's numeric id. |
40002: pixel_id ... is not a valid integer string |
You sent the pixel code, not its number | Send the numeric id of the pixel. |
422 invalid_field_value on targeting.countries |
A boost without a country | Send at least one country. |
400 missing_required_field on sparkAuthCode |
An ads-only connection boosting without a Spark Code | Send sparkAuthCode, or connect the TikTok account. |
| Budget rejected | Below $20 per ad group | Raise the budget. |
| Audience not applied | A new customer list takes up to 48 hours to process | Wait, then target it. |