---
title: "Review status of every TikTok Smart+ ad of an advertiser"
method: GET
path: "/v1/ads/tiktok-smart-plus-reviews"
tags: ["Ad Campaigns"]
---

# Review status of every TikTok Smart+ ad of an advertiser

`GET /v1/ads/tiktok-smart-plus-reviews`

## Request

- Base URL: `https://zernio.com/api`
- URL: `https://zernio.com/api/v1/ads/tiktok-smart-plus-reviews`
- Auth: HTTP bearer

Returns the review verdict of every Upgraded Smart+ ad of one TikTok advertiser, with each creative's verdict, in one paginated read. Use it to refresh reviews periodically: one call (plus its pages) per advertiser per refresh, instead of `GET /v1/ads/{adId}/tiktok-smart-plus-review` per ad.

Zernio lists the advertiser's Smart+ ads with `/smart_plus/ad/get/` (deleted ones only with `includeDeleted=true`) and reads `/smart_plus/ad/review_info/` for Smart+ ads holding up to 50 creatives in total per call (TikTok refuses more; a Smart+ ad with more than 50 creatives is read alone), across ad groups, one call after another. A full refresh of N Smart+ ads with C creatives in total costs about `ceil(N / 100)` listing calls and `ceil(C / 50)` review calls on TikTok. When TikTok still refuses a call for its creative count, Zernio lowers the creative cap for that advertiser and reads the same ads again in smaller calls, never resending the refused size.

Stale while refreshing: every review is stored for 24 hours per connection, advertiser and Smart+ ad (shared with the single-ad read), and `readAt` is always the time TikTok answered, never rewritten when served from the store. `maxAgeSeconds` (default 3600) is the freshness window: a review read within it is fresh and costs no TikTok call; an older one is still returned in `items` but is read again, and so is a Smart+ ad never read. Each request first reads every Smart+ ad of the page never read (live, then deleted with `includeDeleted=true`) and only then refreshes stale ones, oldest `readAt` first, so a quota pause cannot leave never-read ads behind stale ones, progress carries over between requests and an ad already read never goes back to pending. A stored review is replaced only by a successful read. `counts` splits the Smart+ ads on this page into `fresh`, `stale`, `pending` and `failed`, and `oldestReadAt` is the oldest `readAt` of `items`. `incomplete` is true only while some Smart+ ad of the page has never been read (`pending > 0`). Staleness is reported apart: `allFresh` is true when every item and refusal of the page is within `maxAgeSeconds` and nothing is pending. A page with stale items and nothing pending is complete (`incomplete: false`, no `Retry-After`): move on, and a later read refreshes it oldest first. On 2026-10-25 TikTok's quota allowed about 130 to 230 Smart+ ads per advertiser per hour (about 11.5 ads and 4 creatives per successful call), so a larger advertiser cannot be fully refreshed within one hour and its pages stay complete but not `allFresh`.

The listing is stored with the time it was read (`listing.readAt`) and listed again when it is older than `maxAgeSeconds`, so it can be up to `maxAgeSeconds` old: a Smart+ ad created on TikTok appears after at most that long (one hour by default). If that relisting fails, Zernio serves the stored listing with `listing.source: fallback` and the relist error in `listing.error`, instead of failing the request; `listing.source: fresh` means the listing was read within `maxAgeSeconds`. Only when no listing was ever stored does the listing error fail the request.

TikTok's review endpoint has a strict quota of its own (TikTok documents neither the quota nor its scope). When it is over quota (or Zernio is holding review reads for this advertiser and connection after an overage: about 60 seconds, then 120, 240, 480 and up to 960 while TikTok keeps refusing, and after each successful call one step less, 480 down to 60 seconds, until a success at 60 seconds ends it, each with up to 10% of random jitter), Zernio stops calling TikTok and answers 200 with the reviews it has: stale reviews in `items`, the Smart+ ads never read in `pendingSmartPlusAdIds` and, when any are pending, `incomplete: true` with `retryAfterSeconds` (also in the `Retry-After` header). Every Smart+ ad of the page is in `items`, `failedSmartPlusAds` or `pendingSmartPlusAdIds`. Wait `retryAfterSeconds` and read the same page again: only the stale and pending ads reach TikTok.

A Smart+ ad whose review TikTok refuses on its own (for example one deleted on TikTok since the listing was read) is returned in `failedSmartPlusAds` with TikTok's error and `refusedAt`, and the rest of the page is still read. Zernio finds it by splitting the refused batch in halves, at most 16 extra TikTok calls per page; ads still unresolved after that keep their stored review, or stay in `pendingSmartPlusAdIds` when they have none, and are read first by the next request. A refusal of a single ad is stored for 24 hours and read again once it is older than `maxAgeSeconds`; when an ad has both a stored review and a stored refusal, the more recent one is returned. When both halves of a refused batch are refused with the same TikTok error (for example a permission error), Zernio stops calling TikTok for that page: when every Smart+ ad of the page has a stored review or refusal, it serves them and returns the refusal in `reviewError`; when some ad was never read, the whole request fails with that error.

Items are sorted by `adGroupId`, then `smartPlusAdId`, so pages are stable. Keep one page in flight per advertiser and read its pages one after another: parallel pages of one advertiser spend its review quota at the same time and come back incomplete. Production logs suggest the quota is counted per advertiser (not confirmed by TikTok), so different advertisers have been read in parallel without throttling each other.

## Query parameters

- `accountId` string, required
- `adAccountId` string, required
- `includeDeleted` boolean
- `maxAgeSeconds` integer
- `page` integer
- `pageSize` integer

## Response `200`

The reviews of one page of the advertiser's Smart+ ads. Check `incomplete`: when true, some Smart+ ads of the page were never read. `allFresh` says whether every review is within `maxAgeSeconds`.

- object
  - `adAccountId` string
  - `includeDeleted` boolean
  - `maxAgeSeconds` integer: The freshness window this page was read with.
  - `page` integer
  - `pageSize` integer
  - `totalPages` integer
  - `totalSmartPlusAds` integer: Smart+ ads of the advertiser across all pages.
  - `listing` object: The Smart+ ad listing this page was cut from.
    - `readAt` string, date-time: When the listing was read from TikTok.
    - `source` 'fresh' | 'fallback': `fresh`: read within `maxAgeSeconds`. `fallback`: relisting failed and this is the older stored listing.
    - `error` object, nullable: The relist error on a fallback, otherwise null.
      - `tiktokCode` integer, nullable
      - `message` string
  - `reviewError` object, nullable: Set when TikTok refused both halves of a review batch the same way (for example a permission error) and the page was served from stored reviews instead of being refreshed. Null otherwise.
    - `tiktokCode` integer, nullable
    - `message` string
  - `counts` object: The Smart+ ads on this page split by state; the four counts add up to the Smart+ ads on this page (fewer than `pageSize` on the last page).
    - `fresh` integer: Items read within `maxAgeSeconds`.
    - `stale` integer: Items read earlier, returned with their `readAt` and due for a refresh.
    - `pending` integer: Smart+ ads never read (`pendingSmartPlusAdIds`).
    - `failed` integer: Entries of `failedSmartPlusAds`.
  - `oldestReadAt` string, date-time, nullable: The oldest `readAt` in `items`. Null when `items` is empty.
  - `items` object[]
    - `smartPlusAdId` string
    - `adGroupId` string, nullable
    - `campaignId` string, nullable
    - `review` TikTokSmartPlusAdLevelReview, nullable: The ad-level verdict of a Smart+ ad (`smart_plus_ad_review_infos`). Null when TikTok returned none.
      - `reviewStatus` 'approved' | 'partially_approved' | 'rejected' | 'unknown': TikTok `review_status` normalized: ALL_AVAILABLE -> approved, PART_AVAILABLE -> partially_approved (delivers in part of the targeted locations), UNAVAILABLE -> rejected, anything else or missing -> unknown (see `rawReviewStatus`).
      - `rawReviewStatus` string, nullable: TikTok `review_status`, verbatim.
      - `rejectReasons` string[]: Every distinct reason across `rejections`.
      - `passedLocations` string[]: Location codes that passed the review.
      - `appealStatus` string, nullable: TikTok `appeal_status`, verbatim: NOT_APPEALED, APPEALING, APPEAL_SUCCESSFUL, APPEAL_FAILED, APPEAL_DONE.
      - `appealRejectReasons` string[]
      - `rejections` TikTokReviewRejection[]
        - `reasons` string[]
        - `suggestion` string, nullable: TikTok's advice for fixing the rejection.
        - `forbiddenPlacements` string[]
        - `forbiddenAges` string[]
        - `forbiddenLocations` string[]
        - `content` object, nullable: The content TikTok rejected.
          - `type` string, nullable: TikTok `content_type`, verbatim (ModeString, ModeImage, ModeVideo, ModeMusic, LandingPage, ...).
          - `text` string, nullable
          - `imageId` string, nullable
          - `videoId` string, nullable
          - `musicId` string, nullable
    - `materials` TikTokSmartPlusMaterialReview[]: One entry per creative TikTok reviewed (`material_review_infos`).
      - `adMaterialId` string: TikTok `ad_material_id` of the creative inside this ad.
      - `materialType` 'spark_post' | 'video' | 'carousel' | 'catalog_carousel' | 'null', nullable: Null for a creative TikTok added on its own or a carousel image.
      - `tiktokItemId` string, nullable: The TikTok post id when the creative is a Spark post.
      - `videoId` string, nullable
      - `name` string, nullable
      - `operationStatus` string, nullable: TikTok `material_operation_status`: ENABLE or DISABLE.
      - `reviewStatus` 'approved' | 'partially_approved' | 'rejected' | 'unknown': TikTok `review_status` normalized: ALL_AVAILABLE -> approved, PART_AVAILABLE -> partially_approved (delivers in part of the targeted locations), UNAVAILABLE -> rejected, anything else or missing -> unknown (see `rawReviewStatus`).
      - `rawReviewStatus` string, nullable: TikTok `review_status`, verbatim.
      - `rejectReasons` string[]: Every distinct reason across `rejections`. Empty when approved.
      - `passedLocations` string[]
      - `rejections` TikTokReviewRejection[]
        - `reasons` string[]
        - `suggestion` string, nullable: TikTok's advice for fixing the rejection.
        - `forbiddenPlacements` string[]
        - `forbiddenAges` string[]
        - `forbiddenLocations` string[]
        - `content` object, nullable: The content TikTok rejected.
          - `type` string, nullable: TikTok `content_type`, verbatim (ModeString, ModeImage, ModeVideo, ModeMusic, LandingPage, ...).
          - `text` string, nullable
          - `imageId` string, nullable
          - `videoId` string, nullable
          - `musicId` string, nullable
    - `readAt` string, date-time: When the verdict was read from TikTok, kept as is when served from the store. Older than `maxAgeSeconds` means stale.
  - `failedSmartPlusAds` object[]: Smart+ ads of this page whose review TikTok refused on its own (for example an ad deleted on TikTok since the listing was read). Zernio splits a refused batch in halves (at most 16 extra calls per page), so the other ads of the batch are still read; ads left unresolved by that cap stay in `items` with their stored review or in `pendingSmartPlusAdIds`. Empty when none.
    - `smartPlusAdId` string
    - `adGroupId` string, nullable
    - `campaignId` string, nullable
    - `error` object
      - `tiktokCode` integer, nullable: TikTok's error code, verbatim.
      - `message` string: TikTok's error message, verbatim.
    - `refusedAt` string, date-time, nullable: When TikTok refused this ad. Null for a refusal stored before this field existed.
  - `incomplete` boolean: True while some Smart+ ads of this page were never read (`counts.pending > 0`). Stale items alone keep it false.
  - `allFresh` boolean: True when every item and refusal of this page was read within `maxAgeSeconds` and nothing is pending. False with `incomplete: false` means the page is complete but some reviews are older than the window; a later read refreshes them oldest first.
  - `pendingSmartPlusAdIds` string[]: Smart+ ads of this page never read. Empty when every ad has a stored review or refusal.
  - `retryAfterSeconds` integer, nullable: Seconds to wait before reading this page again, set only while `incomplete` is true and review reads are paused. Null when complete (stale items included), or when the page is incomplete without a quota pause (read it again right away).

## Other responses

- `400`: Invalid request
- `401`: Missing or invalid API key. `code` is `missing_credentials` when no Authorization header was sent and `invalid_credentials` when the key is unknown, revoked or expired.
- `403`: accountId is not a TikTok or TikTok Ads connection (code feature_not_available).
- `404`: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
- `409`: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
- `422`: The TikTok connection has no TikTok Ads token: connect TikTok Ads first (code ads_connection_required).
- `429`: The connected account's upstream platform quota is exhausted. Reddit rate-limits per connected Reddit user (1000 requests per 10-minute window), and that budget is shared by every operation using that account. Retry after the window resets rather than retrying immediately; repeated calls while exhausted do not succeed and keep the budget spent. Google Ads: writes and reports run on one developer token shared by every Google Ads account on Zernio. The token holds Standard access (no daily operations cap), so this only happens when Google throttles the token or your ad account. The envelope has `code: rate_limited`, `platform: google`, `details.quotaScope: DEVELOPER` (`ACCOUNT` when it is your own ad account's quota), `details.resetsAt` (ISO instant when Google accepts requests again) and `Retry-After` counting down to it. Retrying earlier cannot succeed. YouTube Analytics: reads (channel-insights, demographics, daily-views, video-retention) run on the "Queries per day" quota of the Google Cloud project the YouTube account is connected through, shared with other Zernio customers, so a 429 there is not your own volume. It resets at midnight Pacific Time. The envelope has `code: rate_limited`, `platform: google`, `details.quotaExhausted: true`, `details.quotaScope: DEVELOPER`, `details.resetsAt` and `Retry-After` counting down to it. Retrying earlier cannot succeed. Meta ads: every Meta throttle (codes 4, 17, 32, 613 and the business-use-case codes 80000-80014) returns 429 `rate_limited`, even when Meta itself answers HTTP 400. `Retry-After` comes from Meta's `x-business-use-case-usage` estimate when Meta sends one, otherwise it is Meta's documented 60-second minimum (30 seconds for the one-write-per-30-seconds limit on a single object). Pinterest ads: Pinterest rate-limits per user and app, per category (ads_read, ads_write, ads_analytics). `Retry-After` comes from Pinterest's `x-ratelimit-reset` when it sends one, otherwise 60 seconds. TikTok ads: TikTok caps requests per second and answers overage with code 40100 or with code 40002 `OVER_QPS_LIMIT`. Both return 429 `rate_limited` with `platform: tiktok` and `Retry-After: 10`. Space out bulk reads (for example one ad at a time per advertiser) instead of retrying at once. The Smart+ review read (`GET /v1/ads/{adId}/tiktok-smart-plus-review`) has a stricter quota of its own on TikTok: after an overage Zernio answers 429 for that advertiser and connection, without calling TikTok, for about 60 seconds, then 120, 240, 480 and up to 960 while TikTok keeps refusing, each with up to 10% of random jitter. After an overage, each successful call holds the next one for one step less (480, 240, 120, then 60 seconds) until a success at 60 seconds ends it; `Retry-After` is the time left. The advertiser-wide read (`GET /v1/ads/tiktok-smart-plus-reviews`) answers 200 with `incomplete: true` instead. Keep one request in flight per advertiser there.

## Changes

- **2026-10-25** `d454c6e72e58`: 8 info
  - added the new optional `query` request parameter `maxAgeSeconds`
  - added the optional property `allFresh` to the response with the `200` status
  - added the optional property `counts` to the response with the `200` status
  - added the optional property `failedSmartPlusAds/items/refusedAt` to the response with the `200` status
  - …4 more
- **2026-10-22** `773d411b1c42`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/ads/tiktok-smart-plus-reviews/get.md)

---

[API](https://skmtc.dev/zernio/apis/zernio-api.md) - [All operations](https://skmtc.dev/zernio/apis/zernio-api/llms.txt) - [OpenAPI document](https://skmtc.dev/zernio/apis/zernio-api/revisions/2ccee257b7c2?raw)
