# Search paid ads

Search stored Meta and TikTok ad-library observations.

## Endpoint: GET /v1/paid-ads

- Method: `GET`
- Path: `/v1/paid-ads`
- Authentication: Bearer API credential or OAuth access token
- Required scope: `context:read`
- MCP tool: `get_ads`
- MCP action: `search`

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | opaque string | no | Cursor returned by the previous page. |
| `limit` | query | integer, 1–40 | no | Page size; defaults to 18. |
| `search` | query | string, max 120 | no | Search stored lexical evidence across creative copy, transcripts, creative analysis, advertiser names and linked apps. A recognized category term may include related-category matches when includeRelated=true. |
| `searchScope` | query | all | creative | apps | advertisers | no | Limit the search fields; defaults to all. |
| `includeRelated` | query | boolean | true | false | no | Include related-category matches. Disabled by default. |
| `excludeAdvertiser` | query | string, 1–120 | no | Exclude one exact advertiser name from the returned page. |
| `groupCreatives` | query | boolean | true | false | no | Group ads that share an exact media asset and return their SHA-256 group keys. Requires a ranked sort: Best match with a nonblank search or a ranked browse order. |
| `creativeGroupKey` | query | 64 lowercase hexadecimal characters | no | Filter to one exact creative asset group; pass groupCreatives=false to expand its ads. Requires a ranked sort; balanced browsing is not supported. |
| `platform` | query | meta | tiktok | no | Advertising platform filter. |
| `status` | query | comma-separated list of active | inactive | unknown | no | Observed ad status; matches any listed value, e.g. status=active,inactive. A single value works as before. |
| `advertiser` | query | string, max 120 | no | Exact, case-sensitive advertiser name. |
| `advertiserExternalId` | query | string, 1–255 | no | One advertiser by its exact platform page id (a Meta page id). Prefer it over advertiser. |
| `archiveId` | query | string, 1–255 | no | Exact platform archive id filter. |
| `productExternalId` | query | string, max 255 | no | Include ads linked to this exact app store id. |
| `excludeProductExternalId` | query | string, max 255 | no | Exclude ads linked to this exact app store id. |
| `storeIds` | query | comma-separated numeric App Store IDs, max 25 | no | Only ads verified to link to these apps: the same ads the app’s paid intelligence counts. |
| `category` | query | Health & Wellness | Personal Development | Relationships & Lifestyle | Arts, Hobbies & Entertainment | Education & Knowledge | Fashion & Beauty | Spirituality & Beliefs | Business & Finance | Travel | Food & Cooking | Home & Design | Fitness | Technology | Gaming & Esports | Productivity & Organization | no | Canonical English category of the verified linked iOS app; known source aliases are grouped automatically. |
| `format` | query | comma-separated list of image | video | carousel | dynamic | unknown | no | Creative format filter; matches any listed value, e.g. format=video,carousel. dynamic is Meta dynamic creative (DCO), often a static image. |
| `startDate` | query | YYYY-MM-DD | no | Earliest disclosed ad start date, inclusive. |
| `endDate` | query | YYYY-MM-DD | no | Latest disclosed ad start date, inclusive. |
| `minRunDays` | query | 7 | 30 | 90 | 180 | 365 | no | Minimum run length. Live ads count to now; stopped ads to their disclosed end. |
| `discoveredAfter` | query | ISO 8601 date-time | no | Only ads PeekPanda first recorded strictly after this instant. |
| `cta` | query | present | missing | no | Whether the creative has a call to action. |
| `copyLength` | query | short | medium | long | no | Primary-text length bucket. |
| `reach` | query | reported | 10000 | 100000 | 1000000 | no | Require reported reach, optionally above a threshold. |
| `appLink` | query | linked | unlinked | no | Whether the ad has a verified App Store match. |
| `saved` | query | boolean | true | false | no | When true, only account-saved ads. |
| `sort` | query | relevance | balanced | recently_discovered | newest_started | longest_running | most_tested | highest_reported_reach | no | Defaults to relevance for nonblank search text, recently_discovered for a storeIds browse, and balanced otherwise. Explicit relevance without search text becomes that browse default. Relevance ranks lexical evidence with advertiser diversity. longest_running is the closest performance proxy: no spend is published. |

## Known errors

- 400 invalid_request for an invalid filter, identity or limit.
- 401 authentication_required for a missing or invalid credential.
- 402 subscription_required when access is not entitled.
- 403 authorization_required for a missing scope or unavailable preview access.
- 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach.
- 404 not_found when the record is outside the accessible catalogue.
- 429 rate_limited; honor Retry-After when present.
- 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC.

## Request

Send the documented filters as query parameters. To follow an app or advertiser over time, pass storeIds or advertiserExternalId with sort=recently_discovered and set discoveredAfter to the newest discoveredAt you already read. This read does not collect new ads.

```
curl --get 'https://api.peekpanda.com/v1/paid-ads' \
  --header "Authorization: Bearer $PEEKPANDA_TOKEN" \
  --header 'x-genviral-tenancy: personal' \
  --data-urlencode 'search=hydration' \
  --data-urlencode 'platform=meta' \
  --data-urlencode 'storeIds=6480417616' \
  --data-urlencode 'discoveredAfter=2026-09-28T06:00:00Z' \
  --data-urlencode 'limit=18' \
  --data-urlencode 'sort=relevance'
```

## MCP request

Call get_ads with action search. Path fields go in params, filters in query, and a write body in body.

```
{
  "name": "get_ads",
  "arguments": {
    "request": {
      "action": "search"
    }
  }
}

```

## Response

items are stored paid-ad observations. Search uses lexical evidence already stored in the corpus; it does not fetch new provider data. Relevance ranks matching evidence with advertiser diversity. Creative grouping and creativeGroupKey require a ranked sort; balanced browsing returns HTTP 400 for these options. Expand one group by passing its key with groupCreatives=false. Each item may include searchMatch.sources and an exact-asset creativeGroup (SHA-256 key, count and representative archive id). Group counts describe ads sharing identical media, not identical copy. Use creativeGroupKey to filter one exact group. searchSummary counts only the returned page, never the matching corpus. Keep filters and sort unchanged while following nextCursor so pages continue the same ordered search. A storeIds read is served from the stored ad list of those apps, about a minute behind new ads; without sort or search it lists them by recently_discovered (sort=balanced still mixes advertisers). With sort=recently_discovered or newest_started it reads that list in order and checks each other filter on the ads it scans, at most 4,096 per request, so a rare filter can return a short or empty page with hasMore true: keep following nextCursor until hasMore is false. Other sorts, balanced included, rank the listed ads of those apps. canonicalKey matches paid:(meta|tiktok):{archiveId}. performance is unavailable or a reported_reach disclosure. There is no spend field.

```
{
  "items": [
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "canonicalKey": "paid:meta:111222333",
      "source": "paid",
      "platform": "meta",
      "archiveId": "111222333",
      "advertiser": {
        "externalId": "444555666",
        "name": "Example Advertiser"
      },
      "status": "active",
      "startedAt": "2026-08-01T00:00:00.000Z",
      "endedAt": null,
      "discoveredAt": "2026-09-06T00:00:00.000Z",
      "sourceUrl": "https://www.facebook.com/ads/library?id=111222333",
      "creative": {
        "format": "video",
        "hook": "Turn saved recipes into a grocery list",
        "body": "Turn saved recipes into a grocery list.",
        "title": "Example Meal Planner",
        "description": null,
        "ctaText": "Download",
        "imageUrls": [
          "https://cdn.example.com/ad.jpg"
        ],
        "videoUrls": [
          "https://cdn.example.com/ad.mp4"
        ],
        "units": []
      },
      "productLink": {
        "kind": "official_domain",
        "name": "Example Meal Planner",
        "url": "https://example.com",
        "externalId": null,
        "confidence": "unresolved"
      },
      "destination": {
        "kind": "web",
        "url": "https://example.com",
        "host": "example.com",
        "page": "example.com",
        "appStoreId": null
      },
      "productIconUrl": null,
      "performance": {
        "kind": "unavailable"
      },
      "saved": false
    }
  ],
  "nextCursor": null,
  "hasMore": false,
  "searchSummary": {
    "returnedAds": 18,
    "returnedAdvertisers": 7,
    "returnedApps": 5,
    "grouped": true
  }
}

```

## Source

Stored public advertising-library observations with source dates. Spend is unavailable unless the public record reports it.

## Refresh

This read returns stored observations. Saving an ad changes boards only.
