# TikTok 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.

```bash cURL
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
  }'
```
```ts Node
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 group
```
```python Python
import 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:

```json
{
  "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](https://pozy.ai/docs/ads/pixels) 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`:

```json
{
  "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.countries` is required, unless you attach to an existing Smart+ ad group with `adSetId`.
- `linkUrl` and `callToAction` add 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 `sparkAuthCode` with their video id in `platformPostId`. Several creators at once go in `sparkPosts[]`.
- On an ads-only connection, `sparkAuthCode` and `platformPostId` are always required, because the connection has no TikTok identity of its own.

More boost options across networks are on [Boost posts](https://pozy.ai/docs/ads/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}/reply` with `text`
- `POST /v1/ads/{adId}/comments/{commentId}/hide` with `hidden: true` or `false`
- `DELETE /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](https://pozy.ai/docs/ads/targeting#tiktok).

## Reporting

- `GET /v1/ads/{adId}/analytics` returns spend, impressions, clicks, CTR, CPM and video metrics. `breakdowns` takes `gender`, `age`, `country_code`, `platform`, `ac` (network type) and `language`.
- `GET /v1/ads/tree` adds those metrics up for each campaign and ad group.
- `GET /v1/ads/insights` asks TikTok for a report on the spot. Send `adAccountId`, a `dataLevel` (`AUCTION_ADVERTISER`, `AUCTION_CAMPAIGN`, `AUCTION_ADGROUP` or `AUCTION_AD`), between 1 and 4 `dimensions`, the `metrics`, and dates spanning 365 days or less. `reportType` takes `BASIC` or `AUDIENCE`.

To count each person once across several campaigns, filter the set and group by `country_code`:

```bash
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. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/{platform}/ads`](https://pozy.ai/docs/api/connectAds) | Connect ads for a platform |
| [`PATCH /v1/connect/tiktok-ads`](https://pozy.ai/docs/api/configureTikTokAdsBrandIdentity) | Set TikTok brand identity |
| [`GET /v1/ads/accounts`](https://pozy.ai/docs/api/listAdAccounts) | List ad accounts |
| [`GET /v1/ads/business-centers`](https://pozy.ai/docs/api/listAdsBusinessCenters) | List TikTok Business Centers |
| [`GET /v1/ads/tiktok-identities`](https://pozy.ai/docs/api/listAdsTikTokIdentities) | List TikTok ad identities |
| [`POST /v1/ads/create`](https://pozy.ai/docs/api/createStandaloneAd) | Create standalone ad |
| [`POST /v1/ads/boost`](https://pozy.ai/docs/api/boostPost) | Boost post as ad |
| [`GET /v1/ads/tree`](https://pozy.ai/docs/api/getAdTree) | Get campaign tree |
| [`PUT /v1/ads/{adId}`](https://pozy.ai/docs/api/updateAd) | Update ad |
| [`PUT /v1/ads/{adId}/status`](https://pozy.ai/docs/api/updateAdStatus) | Pause or resume a single ad |
| [`PUT /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/updateAdSet) | Update an ad set |
| [`PUT /v1/ads/campaigns/{campaignId}/status`](https://pozy.ai/docs/api/updateAdCampaignStatus) | Pause or resume a campaign |
| [`POST /v1/ads/campaigns/{campaignId}/duplicate`](https://pozy.ai/docs/api/duplicateAdCampaign) | Duplicate a campaign |
| [`GET /v1/ads/{adId}/comments`](https://pozy.ai/docs/api/getAdComments) | List comments on an ad |
| [`POST /v1/ads/{adId}/comments/{commentId}/reply`](https://pozy.ai/docs/api/replyToAdComment) | Reply to an ad comment |
| [`POST /v1/ads/{adId}/comments/{commentId}/hide`](https://pozy.ai/docs/api/hideAdComment) | Hide or unhide an ad comment |
| [`DELETE /v1/ads/{adId}/comments/{commentId}`](https://pozy.ai/docs/api/deleteAdComment) | Delete an ad comment |
| [`GET /v1/ads/{adId}/tiktok-smart-plus-review`](https://pozy.ai/docs/api/getTikTokSmartPlusAdReview) | Review status of each creative in a TikTok Smart+ ad |
| [`GET /v1/ads/tiktok-smart-plus-reviews`](https://pozy.ai/docs/api/listTikTokSmartPlusReviews) | Review status of every TikTok Smart+ ad of an advertiser |
| [`GET /v1/ads/tiktok-smart-plus-materials`](https://pozy.ai/docs/api/getTikTokSmartPlusMaterialReport) | Per-creative performance inside TikTok Smart+ ads |
| [`GET /v1/ads/pixels`](https://pozy.ai/docs/api/listTikTokAdPixels) | List TikTok ad pixels |
| [`GET /v1/ads/{adId}/analytics`](https://pozy.ai/docs/api/getAdAnalytics) | Get ad analytics |
| [`GET /v1/ads/insights`](https://pozy.ai/docs/api/queryAdInsights) | Flexible live insights query |
