> For the complete documentation index, see [llms.txt](https://docs.aiclicks.io/llms.txt).
> Use it to discover all available pages before exploring further.

# ChatGPT Ads Cards

Individual ChatGPT ad cards (creatives) seen for a tracked domain's prompts, paginated newest-first.

Returns the individual ad cards — the creatives ChatGPT rendered — seen for your domain's tracked prompts during the lookback window, one row per card, paginated newest-first. Each card carries its advertiser, the copy (`title` / `body`), the destination URL, the prompt it was shown on, and its topic. Use it to answer _"show me the actual ads running against my prompts on ChatGPT"_ in one call.

Cards are sorted by their response time, most recent first. This is the raw creative feed; for the per-advertiser roll-up use ``.

`domain_id` is a required query parameter. Use `` to discover which domains the calling key can access.

<Request tabs="cURL,Python,Node">
  ```bash
  curl --request GET \
    --url 'https://api.aiclicks.io/api/v1/ads/cards?domain_id=8f1d3c0a-2f9b-4c11-9b80-7a82e1f0c3f3&days=30&page=1&per_page=50' \
    --header 'Authorization: Bearer ak_live_xxx'
  ```

  ```python
  import httpx, os

  resp = httpx.get(
      "https://api.aiclicks.io/api/v1/ads/cards",
      params={
          "domain_id": "8f1d3c0a-2f9b-4c11-9b80-7a82e1f0c3f3",
          "days": 30,
          "page": 1,
          "per_page": 50,
      },
      headers={"Authorization": f"Bearer {os.environ['AICLICKS_API_KEY']}"},
  )
  resp.raise_for_status()
  body = resp.json()["data"]
  print(f"{body['totalCount']} cards total")
  for card in body["cards"]:
      you = " (you)" if card["isYou"] else ""
      print(f"{card['brand']}{you}: {card['title']}  -> {card['url']}")
  ```

  ```javascript
  const url = new URL("https://api.aiclicks.io/api/v1/ads/cards");
  url.searchParams.set("domain_id", "8f1d3c0a-2f9b-4c11-9b80-7a82e1f0c3f3");
  url.searchParams.set("days", "30");
  url.searchParams.set("page", "1");
  url.searchParams.set("per_page", "50");

  const resp = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.AICLICKS_API_KEY}` },
  });
  const { data } = await resp.json();
  for (const card of data.cards) {
    console.log(`${card.brand}: ${card.title} (${card.queryShown})`);
  }
  ```
</Request>

<Response tabs="200,400,401,403,404,429">
  ```json
  {
    "data": {
      "cards": [
        {
          "id": "resp_a1b2:0:0",
          "brandId": "globex",
          "brand": "Globex",
          "domain": "globex.io",
          "color": "#DB2777",
          "favicon": "https://globex.io/favicon.ico",
          "isYou": false,
          "title": "Analytics that scales with you",
          "body": "Start free. No credit card. Ship dashboards in minutes.",
          "queryShown": "best analytics tool for startups",
          "url": "https://globex.io/startups?utm_source=chatgpt",
          "image": "https://globex.io/ads/hero.png",
          "topic": "Analytics"
        },
        {
          "id": "resp_a1b2:1:0",
          "brandId": "acme",
          "brand": "Acme",
          "domain": "acme.com",
          "color": "#2563EB",
          "favicon": "https://acme.com/favicon.ico",
          "isYou": true,
          "title": "Acme Analytics — built for founders",
          "body": "The metrics that matter, from day one.",
          "queryShown": "best analytics tool for startups",
          "url": "https://acme.com/?ref=chatgpt",
          "image": null,
          "topic": "Analytics"
        }
      ],
      "totalCount": 311,
      "pagination": {
        "current_page": 1,
        "per_page": 50,
        "total_count": 311,
        "total_pages": 7,
        "has_next": true,
        "has_prev": false,
        "last_page": false
      }
    },
    "domain_id": "8f1d3c0a-2f9b-4c11-9b80-7a82e1f0c3f3",
    "days": 30,
    "generated_at": "2026-06-17T10:00:11.218Z"
  }
  ```

  ```json
  {
    "detail": "domain_id query parameter is required. List the domains this key can access with GET /api/v1/domains, then pass ?domain_id=<uuid>."
  }
  ```

  ```json
  {
    "detail": "Invalid or revoked API key"
  }
  ```

  ```json
  {
    "detail": "API access is not enabled for this team. Contact support@aiclicks.io."
  }
  ```

  ```json
  {
    "detail": "Domain not found"
  }
  ```

  ```json
  {
    "detail": "Too many requests. Please try again later."
  }
  ```
</Response>

## Authorizations

<ParamField header="Authorization" param-type="string" required="true">
  Your API key formatted as `Bearer ak_live_<your-key>`. Create one in the dashboard under **Settings → Developers**.
</ParamField>

<ParamField header="X-Request-Id" param-type="string">
  Optional UUID for log correlation. If omitted, we generate one and echo it back in the response.
</ParamField>

## Query parameters

<ParamField query="domain_id" param-type="string" required="true">
  UUID of the domain. Find domains via ``. Omitting this returns `400`.
</ParamField>

<ParamField query="days" param-type="integer">
  Look-back window, 1–365. Defaults to `30`.
</ParamField>

<ParamField query="page" param-type="integer">
  1-based page number. Defaults to `1`. Must be `≥ 1`.
</ParamField>

<ParamField query="per_page" param-type="integer">
  Cards per page, 1–500. Defaults to `50`.
</ParamField>

## Response

<ResponseField name="data" field-type="object" required="true">
  The cards payload plus a total count and pagination metadata.
</ResponseField>

<ResponseField name="data.cards" field-type="array" required="true">
  One row per ad card, sorted by response time descending (newest first). This is the current page slice, not the full set.

  <Expandable title="Card properties" default-open="false">
    <ResponseField name="id" field-type="string" required="true">
      Stable card identifier, formatted `"<response_id>:<ad_index>:<card_index>"`.
    </ResponseField>

    <ResponseField name="brandId" field-type="string" required="true">
      Advertiser brand id, matching the leaderboard `id` and `brand_ids` filter values.
    </ResponseField>

    <ResponseField name="brand" field-type="string" required="true">
      Advertiser display name. Falls back to host, then brand id.
    </ResponseField>

    <ResponseField name="domain" field-type="string" required="true">
      Advertiser host (e.g. `globex.io`). Falls back to the brand id when no host was parsed.
    </ResponseField>

    <ResponseField name="color" field-type="string" required="true">
      Deterministic hex color for this brand, matching the leaderboard.
    </ResponseField>

    <ResponseField name="favicon" field-type="string">
      Advertiser favicon URL, or `null` when the ad payload had none.
    </ResponseField>

    <ResponseField name="isYou" field-type="boolean" required="true">
      `true` when the advertiser is your own brand (host matches the tracked domain).
    </ResponseField>

    <ResponseField name="title" field-type="string" required="true">
      Ad headline. Empty string when the card had none.
    </ResponseField>

    <ResponseField name="body" field-type="string" required="true">
      Ad body copy. Empty string when the card had none.
    </ResponseField>

    <ResponseField name="queryShown" field-type="string" required="true">
      Text of the tracked prompt this card was shown on.
    </ResponseField>

    <ResponseField name="url" field-type="string" required="true">
      Ad destination URL. Empty string when the card had none.
    </ResponseField>

    <ResponseField name="image" field-type="string">
      Ad image URL, or `null` when the card had none.
    </ResponseField>

    <ResponseField name="topic" field-type="string">
      Name of the prompt's first assigned topic, or `null` when the prompt has no topic.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="data.totalCount" field-type="integer" required="true">
  Total cards matching the window across all pages. Equal to `pagination.total_count`.
</ResponseField>

<ResponseField name="data.pagination" field-type="object" required="true">
  Pagination metadata for the `cards` slice.

  <Expandable title="Pagination properties" default-open="false">
    <ResponseField name="current_page" field-type="integer" required="true">
      Echo of the requested `page`.
    </ResponseField>

    <ResponseField name="per_page" field-type="integer" required="true">
      Echo of the requested `per_page`.
    </ResponseField>

    <ResponseField name="total_count" field-type="integer" required="true">
      Total cards across all pages.
    </ResponseField>

    <ResponseField name="total_pages" field-type="integer" required="true">
      Total number of pages at this `per_page`.
    </ResponseField>

    <ResponseField name="has_next" field-type="boolean" required="true">
      `true` when a page after this one exists.
    </ResponseField>

    <ResponseField name="has_prev" field-type="boolean" required="true">
      `true` when a page before this one exists.
    </ResponseField>

    <ResponseField name="last_page" field-type="boolean" required="true">
      `true` when this is the final page.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="domain_id" field-type="string" required="true">
  Echo of the requested domain.
</ResponseField>

<ResponseField name="days" field-type="integer" required="true">
  Echo of the requested window.
</ResponseField>

<ResponseField name="generated_at" field-type="string" required="true">
  ISO-8601 timestamp of when the server produced (or cached) this response.
</ResponseField>

## Response headers

| Header                  | Description                                                      |
| ----------------------- | ---------------------------------------------------------------- |
| `X-Cache`               | `HIT` or `MISS`. Indicates whether the response came from cache. |
| `X-Request-Id`          | Unique request id. Echoes incoming if you set one.               |
| `X-RateLimit-Limit`     | Max requests per minute for this key.                            |
| `X-RateLimit-Remaining` | Requests remaining in current minute.                            |
| `X-RateLimit-Reset`     | Unix epoch seconds when the window resets.                       |

## Caching

<Callout kind="info">
  Cached for **30 minutes** server-side per `(domain_id, days, page, per_page)`. Newly completed analyses appear after the TTL expires.
</Callout>

## Errors

<ResponseField name="400 Bad Request" field-type="error">
  `domain_id` query parameter missing. Body points you at `/api/v1/domains`.
</ResponseField>

<ResponseField name="401 Unauthorized" field-type="error">
  Missing, malformed, or revoked API key.
</ResponseField>

<ResponseField name="403 Forbidden" field-type="error">
  Team's `developer_access` flag is off, the domain belongs to a team your API key is not scoped to, or your `allowed_domains` allowlist excludes it.
</ResponseField>

<ResponseField name="404 Not Found" field-type="error">
  `domain_id` is malformed, does not exist, or your user is not a member of its team.
</ResponseField>

<ResponseField name="429 Too Many Requests" field-type="error">
  Rate limit exceeded. Inspect the `Retry-After` header for how long to wait.
</ResponseField>

## Empty result

If no ad cards ran in the window, the response is a successful `200` with an empty list, `totalCount` of `0`, and zeroed pagination:

```json
{
  "data": {
    "cards": [],
    "totalCount": 0,
    "pagination": {
      "current_page": 1,
      "per_page": 50,
      "total_count": 0,
      "total_pages": 0,
      "has_next": false,
      "has_prev": false,
      "last_page": true
    }
  },
  "domain_id": "8f1d3c0a-2f9b-4c11-9b80-7a82e1f0c3f3",
  "days": 30,
  "generated_at": "2026-06-17T10:00:11.218Z"
}
```
