# TikTok Ads Library Scraper | Export & Changes (`danthedataman/tiktok-commercial-ads-change-feed`) Actor

Export TikTok Commercial Content Library ads by keyword or advertiser, with available media, reach and targeting. Compare full exports with disappearance safeguards.

- **URL**: https://apify.com/danthedataman/tiktok-commercial-ads-change-feed.md
- **Developed by:** [Eli J](https://apify.com/danthedataman) (community)
- **Categories:** Social media, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.36 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What you can export

Use this **TikTok Ad Library scraper** for commercial-ad research and recurring creative tracking. Search the Commercial Content Library by keyword or exact advertiser to export available shown dates, media, reach, impressions and targeting.

Compare a full prior export for new, changed and observed-absent ads. An ended row describes disappearance from a complete query read; it does not independently confirm that a campaign stopped.

### Try the example input

```json
{
  "advertisers": [],
  "searchTerms": [
    "SHEIN"
  ],
  "regions": [
    "NL",
    "DE"
  ],
  "maxItems": 10
}
```

For an advertiser, use its exact name from the Library's advertiser suggestions. An optional business ID must match that name; a business ID by itself cannot resolve a name. With details enabled, the advertiser in each ad's detail supplies the identity. With details off, each card must match the selected name; a supplied business ID forces details because cards cannot verify it.

```json
{
  "advertisers": [{"name": "NIKE Retail B.V.", "businessId": "6876453864464188162"}],
  "regions": ["NL"],
  "startDate": "2026-09-06",
  "endDate": "2026-10-06",
  "maxItems": 200
}
```

### Input

| Field | Type | What it does |
| --- | --- | --- |
| `advertisers` | array | Objects with required `name` and optional numeric-string `businessId`. Nonblank names match exact suggestions, case insensitive; ambiguous names and mismatched IDs go to ERRORS. `[{"name":""}]` explicitly browses the source's advertiser mode (`query_type=2`); a blank name cannot carry an ID. Empty/omitted/null arrays request no advertiser queries. |
| `searchTerms` | array | Keyword strings across advertisers. `[""]` explicitly browses the source's keyword mode (`query_type=1`). Empty/omitted/null arrays request no keyword queries. |
| `regions` | array | Search regions: **AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR**. Include `all` to select all 33 regions, case insensitive; duplicates are removed. Each is a separate query. Empty/omitted/null selects no advertiser or keyword queries. |
| `adUrls` | array | Full Library `/ads` search URLs with one supported region, or `/ads/detail/?ad_id=ID` URLs. Search URLs retain their query, second-resolution date boundaries and ad type. Each date omitted by a search URL inherits its `startDate`/`endDate` input; a URL-supplied date wins. Direct detail URLs select one ad independently of regions and date inputs. Empty/omitted/null requests no URL queries. |
| `adType` | string | `ALL`, `VIDEO`, `IMAGE` or `TEXT`; omitted/null uses `ALL`. Search URLs can override it with `ad_type`; direct detail URLs ignore it. |
| `startDate` | string | Inclusive UTC YYYY-MM-DD boundary; omitted/null is 30 days before the end date. |
| `endDate` | string | Inclusive UTC YYYY-MM-DD boundary; omitted/null is today's UTC date. |
| `maxItems` | integer | Maximum distinct ads scanned per query and region, including unchanged ads. Omitted/null uses 100; 0 requests no work. |
| `fetchDetails` | boolean | Omitted/null uses `true`. `false` exports list-card fields without per-ad detail calls. A supplied advertiser `businessId` forces details for that request; direct URLs always use their detail endpoint. |
| `baselineDatasetId` | string | Optional read-only dataset picker. Select a full export from this Actor with the same queries, regions, date inputs and effective basic/detail mode to switch to monitor mode. |

Omitted work inputs never fill themselves with sample queries. `{}` finishes successfully with guidance and no rows. Invalid supplied input is recorded in ERRORS with guidance before any request. Use fixed date boundaries to track membership over time: an omitted date follows the current 30-day window, and ads can leave that window while remaining available elsewhere in the Library.

For a direct ad, copy its Library detail URL:

```json
{"adUrls": ["https://library.tiktok.com/ads/detail/?ad_id=101"], "maxItems": 1}
```

For a Library search URL, use `https://library.tiktok.com/ads/?region=NL&adv_name=SHEIN&query_type=1&ad_type=1`. Only the active-ad/default audience filters and the default `create_time,desc` sort are supported; non-default or unknown search filters are refused into ERRORS. Advertiser URLs use `query_type=2` and an exact `adv_name`, optionally `adv_biz_ids`. A supplied business ID disambiguates exact-name suggestions and every detail must match the selected advertiser.

A search URL with no `adv_name`, such as `https://library.tiktok.com/ads/?region=NL`, browses the source's keyword mode for the selected region/date/type filters. Add `query_type=2` for blank advertiser-mode browsing. The source returns different result sets for these two modes; the Actor preserves the requested mode.

For a faster browse export:

```json
{"advertisers":[{"name":""}],"regions":["FR"],"startDate":"2025-01-01","maxItems":20,"fetchDetails":false}
```

### Output

JSON, CSV, Excel and API export are available through the run's dataset. The following illustrative row shows the shape; optional values can be null or empty.

With `fetchDetails:false`, rows carry list-card identity/name, shown dates and source timestamps, audience bucket/bounds, creative media/type, caption, audit/rejection information and category when supplied. `advertiserBusinessId` and detail-only fields (impressions, targeting, payer/profile, landing page, CTA/objective and download URL) are null or empty. `regions` is empty because cards do not supply an impression-region breakdown; the requested region remains in `sourceUrl`. Card `impression:0` is not interpreted as a measured impression count. Keyword and browse rows use the card's advertiser label; named requests verify that label exactly, case insensitive.

```json
{
  "adId": "101",
  "advertiserName": "Example Advertiser",
  "advertiserBusinessId": "9001",
  "firstShown": "2026-09-07",
  "lastShown": "2026-10-05",
  "firstShownTimestamp": 1788739200000,
  "lastShownTimestamp": 1791158400000,
  "regions": ["NL"],
  "reachRange": "1K-10K",
  "reachMin": 1000,
  "reachMax": 10000,
  "impressionRange": "10K-100K",
  "impressionMin": 10000,
  "impressionMax": 100000,
  "targetAudienceMin": null,
  "targetAudienceMax": null,
  "regionImpressions": [{"region": "NL", "impressionRange": "10K-100K", "impressionMin": 10000, "impressionMax": 100000}],
  "targeting": {"countries": ["NL"], "audience": "No"},
  "creativeType": "video",
  "videoUrls": ["https://cdn.example/ad.mp4"],
  "imageUrls": ["https://cdn.example/cover.jpg"],
  "adText": "Example ad caption",
  "landingPage": null,
  "paidForBy": "Example payer",
  "advertiserCountry": null,
  "advertiserProfile": null,
  "callToAction": null,
  "audienceBreakdown": [],
  "advertisingObjective": null,
  "auditStatus": "1",
  "rejectionInfo": null,
  "adSubject": null,
  "downloadUrl": null,
  "daysRunning": 29,
  "sourceUrl": "https://library.tiktok.com/ads/detail/?ad_id=101&region=NL&adv_name=SHEIN&query_type=1&adv_biz_ids=&start_time=1788652800000&end_time=1791331199000",
  "scrapedAt": "2026-10-06T17:00:00Z"
}
```

| Output field | Meaning |
| --- | --- |
| `adId` | Library ad identity, a string. |
| `advertiserName`, `advertiserBusinessId` | Advertiser from the matching ad detail. List-only rows use the card name and a null business ID. Neither payer nor a brand in a caption is substituted. |
| `firstShown`, `lastShown` | Source millisecond timestamps converted to UTC dates; null if absent. |
| `firstShownTimestamp`, `lastShownTimestamp` | Original source Unix timestamps in **milliseconds**, without rounding; null if absent. |
| `regions` | Regions in the ad's detail impression breakdown, which may include regions beyond the requested search region. |
| `reachRange` | Source audience bucket, retained as text; never an exact count. |
| `reachMin`, `reachMax` | Integer lower/upper counts parsed from `reachRange`, not exact delivered reach. |
| `impressionRange`, `regionImpressions` | Total and region-level impression buckets, retained as text; absent buckets are null. |
| `impressionMin`, `impressionMax` | Integer lower/upper counts parsed from the total `impressionRange`. Each `regionImpressions` entry also has its own `impressionMin` and `impressionMax`. |
| `targetAudienceMin`, `targetAudienceMax` | Integer lower/upper counts from `targeting.target_audience_size`; its original text remains in `targeting`. Target audience size is distinct from observed reach. |
| `targeting` | Source targeting summary: age/gender, targeted audience size, geography, language, devices, operating systems, interests and interaction/audience selections when shown. Source names and value shapes are retained. |
| `creativeType` | `video` or `image` from the proved Library display modes. Unproved detail modes go to ERRORS and make the query partial. |
| `videoUrls`, `imageUrls` | Source media links; images include video covers. Signed links can expire. Download media promptly. |
| `adText`, `landingPage` | Source caption and outbound URL when supplied, otherwise null. Landing pages are often absent. |
| `paidForBy`, `advertiserCountry` | Source sponsor/payer and advertiser registration location, otherwise null. Payer can differ from advertiser. |
| `advertiserProfile` | Source TikTok account object with `username`, `displayName`, `followers`, `accountType`, `profileUrl`, `avatarUrl`; null when absent. Followers retain the source's abbreviated text. |
| `callToAction`, `advertisingObjective` | Source CTA and objective when supplied, otherwise null. |
| `auditStatus`, `rejectionInfo` | Raw source audit code and rejection information, otherwise null. An audit code is not interpreted as proof that a campaign is currently running. Rejection information retains its source JSON shape. |
| `adSubject`, `downloadUrl` | Source industry/category and app download URL when supplied, otherwise null. |
| `daysRunning` | Inclusive count of UTC days from first shown to last shown; null if either date is absent. It does not measure continuous delivery or today's activity. |
| `audienceBreakdown` | Source impression buckets by `region`, `age`, `gender`, `impressionRange`, with integer `impressionMin`/`impressionMax` on every entry; an empty array when absent. |
| `sourceUrl` | Ad detail permalink retaining query/region/date/ad-type context; direct URL reads retain only ad ID. Empty date parameters represent omitted date inputs. |
| `scrapedAt` | UTC timestamp of this run's query work. |
| `changeStatus` | Monitor mode only: `new`, `changed` or `ended`. |
| `changedFields` | Changed row field names, present only for `changed` rows. |

Bucket bounds expand K/M/B by 1,000/1,000,000/1,000,000,000. For example, `11.7M-14.2M` becomes 11700000–14200000, and `159K` has both bounds 159000. Missing, empty, unrecognized, reversed or nonintegral ranges have null bounds and retain any supplied text. These are bucket boundaries, not exact counts.

### Monitor ads

1. Run an export and keep its dataset ID.
2. Run again with the same query, region, date and effective basic/detail mode inputs, choosing that dataset in `baselineDatasetId`. A basic/detail mode mismatch is refused to avoid false changes.
3. Export the change rows. Keep using a full export as the baseline; a change-feed dataset is refused because it omits unchanged ads.

`new` means absent from the selected baseline, including ads omitted by a previous cap. It is not a claim about campaign launch time. `changed` compares substantive output fields, excluding ad ID, source URL, observation timestamp and the volatile `lastShown`, `daysRunning` and `advertiserProfile.followers` values. Equivalent ordering or duplicate values in targeting/breakdown arrays, signed-media query parameters, CDN hosts and relay tokens do not create changes. Actual media paths and landing-page URL changes do.

Pure `lastShown` advances, derived `daysRunning` extensions and profile follower growth produce no change row or result charge. A delivered row still carries their current values. Other advertiser profile fields remain substantive.

Numeric bounds and source timestamps are snapshot conveniences and do not independently trigger changes; the original bucket text and first-shown date remain compared. Earlier full export baselines are accepted: missing numeric bounds are derived from their retained text, and missing source timestamps remain null rather than inferred from dates.

`ended` means a baseline ad was not found in this run's complete read of that same query context. It is an observed disappearance, not independent confirmation that a campaign stopped. Complete requires a terminal page, every distinct ad counted against the source's reported total, and no cap, source failure or charge stop. A source that drifts or omits IDs is partial and cannot generate ended rows. Previously observed IDs are retained conservatively across a fresh-session retry.

### Coverage and errors

The run's **COVERAGE** record reports each query as complete or partial, distinct valid ads scanned and the stop reason (`end`, `cap`, `source-error`, `rejected-ad`, `snapshot-drift` or `charge-limit`). Search and detail errors, unresolved advertisers and queries with no ads go to **ERRORS**, never the dataset. An unavailable or malformed ad detail is recorded with its ad ID; later ads continue, but that query cannot infer ended rows. Partial results remain available. A run with no rows fails only if every attempted query failed; a genuine empty answer succeeds with guidance. Direct detail reads never infer ended ads.

COVERAGE also records `effectiveStartTime` and `effectiveEndTime` as the UTC Unix-second boundaries used for a search. They are null for direct details or queries stopped before a search window was attempted, including unresolved advertisers.

The Actor uses anonymous Chrome-impersonated HTTP over Apify datacenter proxies. Search sessions rotate after four pages and separate detail sessions after four reads. A search page or detail HTTP/application non-answer receives up to three attempts on fresh sessions; page retries preserve its offset and cursor. Query-total inconsistency receives up to three full traversals while retaining every discovered ID conservatively. Pagination begins at page zero and deduplicates ad IDs within each query. The same ad can appear once under each selected query/region context. No residential proxy is used.

### Coverage limits

Supported surface: public **TikTok Commercial Content Library paid ads search and ad details**, including advertiser suggestions, in the 33 regions listed above. The Library offers these regions; some return no matches for a given keyword, which does not imply an empty Library. Coverage varies by advertiser, date and region. Creative Center top ads, its CTR/likes/ranking data, other commercial content, reporting surfaces and the official research API are outside this product. No TikTok account is needed.

Video and image filters returned populated search and detail answers in the bounded check. The Library offers a text filter, which returned a genuine empty answer for that check; a populated text detail has not yet been observed.

Some moderated-ad details return rejection information while withholding advertiser identity. Those ads go to ERRORS because a correctly identified row cannot be produced; their query remains partial even when the ad ID appeared in search.

The Library's robots.txt disallows `/ads` and `/api`. This Actor reads public business advertising content; use it in accordance with your applicable obligations. Library interfaces and optional field availability can change. This Actor is independent of TikTok.

### Billing and support

Pricing is per 1,000 ads. Each delivered export or change row is one billable result; unchanged ads and ERRORS entries are never dataset results. Actor Start may also apply according to the listing's pricing. A binding customer charge limit stops before another ad detail, page or query is read.

For support, open an issue with the run ID, query inputs and the relevant ERRORS or COVERAGE entry. Avoid sharing credentials or private datasets publicly.

# Changelog

This Actor's version history is a separate document: https://apify.com/danthedataman/tiktok-commercial-ads-change-feed/changelog.md

# Actor input Schema

## `advertisers` (type: `array`):

Exact advertiser suggestion names with an optional matching businessId. A business ID alone cannot resolve a name. A blank name explicitly browses advertiser mode and cannot carry an ID. With fetchDetails false, name-only requests verify card names; supplying businessId forces details and verifies each ad ID/name. Omitted/null/empty arrays request no advertiser queries.

## `searchTerms` (type: `array`):

Keyword searches across advertisers. An explicit blank string browses the source's keyword mode. Empty/omitted/null arrays request no keyword queries.

## `regions` (type: `array`):

Proven Library regions: AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR. Include all (case insensitive) to select every supported region once. Each is a separate query. Empty/omitted/null selects no advertiser or keyword queries; adUrls carry their own region.

## `startDate` (type: `string`):

Optional inclusive UTC YYYY-MM-DD search boundary. Omitted/null is 30 days before endDate (or today's UTC date). Use an explicit fixed range when comparing campaign membership over time.

## `endDate` (type: `string`):

Optional inclusive UTC YYYY-MM-DD search boundary. Omitted/null is today's UTC date.

## `maxItems` (type: `integer`):

Maximum distinct ads scanned for each advertiser/keyword and region, including unchanged ads. Omitted/null uses 100; 0 requests no work. A cap prevents ended inference unless the entire query is read.

## `fetchDetails` (type: `boolean`):

Omitted/null uses true. False exports only list-card fields, without per-ad detail calls; advertiser business ID and other detail-only fields are null/empty. Named advertisers are checked against each card. A supplied businessId forces details for that request, and direct URLs always use their detail endpoint. Baselines must match the effective basic/detail mode for every query.

## `baselineDatasetId` (type: `string`):

Optional read-only full export dataset from this Actor with the same advertiser/keyword, region, date and effective basic/detail mode inputs. Switches to new/changed/ended rows. Unchanged ads are omitted. Change-feed datasets, malformed rows and mismatched query contexts or modes are refused.

## `adUrls` (type: `array`):

Optional full https://library.tiktok.com/ads search URLs with one supported region, or /ads/detail/?ad_id=ID URLs. Search URL query, exact second-resolution dates and ad type are retained. Each omitted URL date inherits its top-level startDate/endDate; a URL-supplied date wins. Non-default audience/status/sort filters are refused. Detail URLs select one ad independently of region/date filters. Empty/omitted/null requests no URL queries.

## `adType` (type: `string`):

Keyword/advertiser filter; omitted/null uses ALL. Search URLs can supply ad_type. Direct detail URLs ignore this filter.

## Actor input object example

```json
{
  "advertisers": [],
  "searchTerms": [
    "SHEIN"
  ],
  "regions": [
    "NL",
    "DE"
  ],
  "maxItems": 10
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `errors` (type: `string`):

No description

## `coverage` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "advertisers": [],
    "searchTerms": [
        "SHEIN"
    ],
    "regions": [
        "NL",
        "DE"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("danthedataman/tiktok-commercial-ads-change-feed").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "advertisers": [],
    "searchTerms": ["SHEIN"],
    "regions": [
        "NL",
        "DE",
    ],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("danthedataman/tiktok-commercial-ads-change-feed").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "advertisers": [],
  "searchTerms": [
    "SHEIN"
  ],
  "regions": [
    "NL",
    "DE"
  ],
  "maxItems": 10
}' |
apify call danthedataman/tiktok-commercial-ads-change-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,danthedataman/tiktok-commercial-ads-change-feed"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/sxP4pZ0oJJhNMJcbZ/builds/6Jj9HscWp8CftHTgx/openapi.json
