> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trygravity.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Dashboard API

> Query your advertiser dashboard data programmatically using your API key.

Pull the same metrics you see in the advertiser dashboard — and create, update, and archive campaigns and ad units — from scripts, cron jobs, or custom integrations.

## Base URL

```
https://platform.trygravity.ai
```

## Authentication

Every request must include your Gravity advertiser API key in the `X-API-Key` header. Grab the key from your [dashboard](https://app.trygravity.ai) under **Settings → API Key**.

```bash theme={null}
curl https://platform.trygravity.ai/advertiser-dashboard/info \
  -H "X-API-Key: YOUR_API_KEY"
```

<Note>
  This is the same API key shown on your advertiser settings page. You can regenerate it at any time.
</Note>

***

## Endpoints

### GET `/advertiser-dashboard/info`

Advertiser profile.

<ParamField header="X-API-Key" type="string" required>
  Your advertiser API key.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl https://platform.trygravity.ai/advertiser-dashboard/info \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "advertiser_id": "abc-123",
  "name": "Acme Corp",
  "email": "ads@acme.com"
}
```

***

### GET `/advertiser-dashboard/campaigns`

List all campaigns with lifetime stats (impressions, clicks, spend, conversions).

<ParamField query="status" type="string">
  Filter by campaign status (e.g. `active`, `paused`, `draft`).
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/campaigns" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaigns": [
    {
      "campaign_id": "camp-456",
      "name": "Summer Sale",
      "status": "active",
      "is_active": true,
      "daily_budget": 100.0,
      "landing_page": "https://acme.com/summer",
      "campaign_objective": "conversions",
      "created_at": "2025-05-01T00:00:00",
      "start_time": "2025-05-01T00:00:00",
      "end_time": null,
      "impressions": 350000,
      "clicks": 1800,
      "spend": 875.50,
      "conversions": 42,
      "conversion_value": 4200.00
    }
  ]
}
```

***

### GET `/advertiser-dashboard/campaigns/{campaign_id}`

Single campaign detail with full stats and targeting config.

<ParamField path="campaign_id" type="string" required>
  The campaign ID.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/campaigns/camp-456" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaign_id": "camp-456",
  "name": "Summer Sale",
  "status": "active",
  "is_active": true,
  "daily_budget": 100.0,
  "landing_page": "https://acme.com/summer",
  "campaign_objective": "conversions",
  "created_at": "2025-05-01T00:00:00",
  "start_time": "2025-05-01T00:00:00",
  "end_time": null,
  "allowed_countries": ["US", "CA"],
  "allowed_devices": ["desktop", "mobile"],
  "utm_params": { "utm_source": "gravity", "utm_medium": "ai" },
  "deployment_status": "deployed",
  "impressions": 350000,
  "clicks": 1800,
  "spend": 875.50,
  "conversions": 42,
  "conversion_value": 4200.00
}
```

***

### GET `/advertiser-dashboard/metrics`

Daily time-series performance metrics.

<ParamField query="days" type="integer" default="30">
  Number of days to look back (1–365).
</ParamField>

<ParamField query="start_date" type="string">
  Start date (`YYYY-MM-DD`). Overrides `days` when paired with `end_date`.
</ParamField>

<ParamField query="end_date" type="string">
  End date (`YYYY-MM-DD`).
</ParamField>

<ParamField query="campaign_id" type="string">
  Filter to a single campaign. Omit for aggregate metrics across all campaigns.
</ParamField>

<ParamField query="tz" type="string" default="UTC">
  IANA timezone (e.g. `America/New_York`) used to bucket days. Defaults to UTC. Set this to your ad account's reporting timezone so daily rows line up with the Gravity Dashboard and, for ChatGPT/OpenAI campaigns, with OpenAI's reporting (OpenAI ad accounts currently report in `America/New_York`).
</ParamField>

<Note>
  Days are bucketed by the timezone you pass in `tz`, not the timezone of the machine calling the API. A calendar day in UTC and the same day in `America/New_York` cover different hours, so the totals will differ — pick one timezone and use it consistently as your source of truth.
</Note>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/metrics?days=7&tz=America/New_York" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "data": [
    {
      "date": "2025-05-20",
      "impressions": 50000,
      "clicks": 260,
      "spend": 125.00,
      "conversions": 6,
      "revenue": 600.00,
      "ctr": 0.52,
      "cpc": 0.48,
      "cpm": 2.50,
      "conversion_rate": 2.31,
      "cpa": 20.83,
      "roas": 4.80
    }
  ],
  "total_impressions": 350000,
  "total_clicks": 1800,
  "total_spend": 875.50,
  "total_conversions": 42,
  "total_revenue": 4200.00
}
```

| Metric            | Description                                      |
| ----------------- | ------------------------------------------------ |
| `ctr`             | Click-through rate (clicks / impressions × 100). |
| `cpc`             | Cost per click (spend / clicks).                 |
| `cpm`             | Cost per mille (spend / impressions × 1000).     |
| `conversion_rate` | Conversions / clicks × 100.                      |
| `cpa`             | Cost per acquisition (spend / conversions).      |
| `roas`            | Return on ad spend (revenue / spend).            |

***

### GET `/advertiser-dashboard/ad-unit-metrics`

Daily time-series metrics broken down **per ad unit** — the same numbers shown under **Ad units** on a campaign's detail page. Use this for creative-level reporting and optimization, where campaign-level `/metrics` isn't granular enough.

Returns one series per `(campaign, ad unit)` link, each tagged with a `data_source`. Series built from real per-ad-unit stats are `"ad_unit_stats"`. A campaign with a single linked ad unit and no per-ad-unit rows yet falls back to the campaign's own daily totals (`data_source: "campaign_fallback"`) so the numbers stay consistent with the dashboard.

<Note>
  This endpoint reports impressions, clicks, spend, CTR, CPC, and CPM per ad unit. Conversions/CPA/ROAS are attributed at the campaign level — use `/metrics` or `/conversions` for those.
</Note>

<ParamField query="days" type="integer" default="30">
  Number of days to look back (1–365).
</ParamField>

<ParamField query="start_date" type="string">
  Start date (`YYYY-MM-DD`). Overrides `days` when paired with `end_date`.
</ParamField>

<ParamField query="end_date" type="string">
  End date (`YYYY-MM-DD`).
</ParamField>

<ParamField query="campaign_id" type="string">
  Filter to a single campaign.
</ParamField>

<ParamField query="ad_unit_id" type="string">
  Filter to a single ad unit. Returns `404` if the ad unit isn't found in your account.
</ParamField>

<ParamField query="tz" type="string" default="UTC">
  IANA timezone (e.g. `America/New_York`) used to bucket days. Same behavior as `/metrics` — set it to your ad account's reporting timezone.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/ad-unit-metrics?days=7&campaign_id=camp-456&tz=America/New_York" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "ad_units": [
    {
      "ad_unit_id": "ad-123",
      "campaign_id": "camp-456",
      "name": "Character & Brand Voice",
      "data_source": "ad_unit_stats",
      "total_impressions": 19784,
      "total_clicks": 131,
      "total_spend": 88.20,
      "data": [
        {
          "date": "2025-05-20",
          "impressions": 3200,
          "clicks": 22,
          "spend": 14.60,
          "ctr": 0.69,
          "cpc": 0.66,
          "cpm": 4.56
        }
      ]
    }
  ]
}
```

| Field                                                | Description                                                                                                                            |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `data_source`                                        | `ad_unit_stats` for real per-ad-unit rows, or `campaign_fallback` when a single-ad-unit campaign inherits campaign-level daily totals. |
| `total_impressions` / `total_clicks` / `total_spend` | Series totals over the requested range.                                                                                                |
| `data[].ctr`                                         | Click-through rate (clicks / impressions × 100).                                                                                       |
| `data[].cpc`                                         | Cost per click (spend / clicks).                                                                                                       |
| `data[].cpm`                                         | Cost per mille (spend / impressions × 1000).                                                                                           |

***

### GET `/advertiser-dashboard/conversions`

Recent conversions with pagination. Returns attributed and unattributed conversions.

<ParamField query="limit" type="integer" default="50">
  Page size (1–100).
</ParamField>

<ParamField query="cursor" type="string">
  Opaque pagination cursor (pass `next_cursor` from a previous response).
</ParamField>

<ParamField query="campaign_id" type="string">
  Filter to a single campaign.
</ParamField>

<ParamField query="event_type" type="string">
  Filter by conversion event type (e.g. `purchase`, `lead`).
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/conversions?limit=10" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "conversions": [
    {
      "conversion_id": "conv-789",
      "event_type": "purchase",
      "value_cents": 9999,
      "currency": "USD",
      "conversion_timestamp": "2025-05-20T14:30:00",
      "campaign_id": "camp-456",
      "order_id": "order-abc",
      "page_url": "https://acme.com/checkout/success",
      "source": "capi",
      "is_attributed": true
    }
  ],
  "total_conversions": 42,
  "has_more": true,
  "next_cursor": "conv-788"
}
```

***

### GET `/advertiser-dashboard/billing`

Billing summary — total spend, billed, unbilled, credits.

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/billing" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "total_spend_dollars": 875.50,
  "total_billed_dollars": 800.00,
  "unbilled_dollars": 75.50,
  "credits_dollars": 0.00,
  "billing_enabled": true,
  "last_spend_sync_at": "2025-05-20T15:00:00",
  "last_billed_at": "2025-05-19T00:00:00"
}
```

***

### GET `/advertiser-dashboard/events/analytics`

Pixel event analytics — domain breakdowns, time-series, and event type counts from your tracking pixel.

<ParamField query="days" type="integer" default="30">
  Number of days to look back (1–365).
</ParamField>

<ParamField query="start_date" type="string">
  Start date (`YYYY-MM-DD`). Overrides `days` when paired with `end_date`.
</ParamField>

<ParamField query="end_date" type="string">
  End date (`YYYY-MM-DD`).
</ParamField>

<ParamField query="domain" type="string">
  Filter by domain (partial match).
</ParamField>

<ParamField query="event_type" type="string">
  Filter by event type (e.g. `page_view`, `form_submit`, `click`).
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/events/analytics?days=30" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "domains": [
    { "domain": "acme.com", "events": 12500 },
    { "domain": "blog.acme.com", "events": 3200 }
  ],
  "time_series": [
    { "date": "2025-05-20T00:00:00", "events": 850 },
    { "date": "2025-05-20T01:00:00", "events": 720 }
  ],
  "event_types": [
    { "event_type": "page_view", "total": 10000, "last_received": "2025-05-20T15:00:00" },
    { "event_type": "form_submit", "total": 500, "last_received": "2025-05-20T14:30:00" }
  ],
  "has_historical_data": true
}
```

***

### GET `/advertiser-dashboard/events/conversion-stats`

Conversion time-series from `conversion_stats_hourly` — daily totals and per-event-type breakdowns.

<ParamField query="days" type="integer" default="30">
  Number of days to look back (1–365).
</ParamField>

<ParamField query="start_date" type="string">
  Start date (`YYYY-MM-DD`). Overrides `days` when paired with `end_date`.
</ParamField>

<ParamField query="end_date" type="string">
  End date (`YYYY-MM-DD`).
</ParamField>

<ParamField query="event_type" type="string">
  Filter by conversion event type (e.g. `purchase`, `lead`).
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/events/conversion-stats?days=30" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "time_series": [
    { "date": "2025-05-20T00:00:00", "conversions": 8 },
    { "date": "2025-05-21T00:00:00", "conversions": 12 }
  ],
  "total": 42,
  "by_event_type": [
    { "event_type": "purchase", "total": 30 },
    { "event_type": "lead", "total": 12 }
  ]
}
```

***

### GET `/advertiser-dashboard/integrations`

Connected Shopify stores. See [Integrations](/advertisers/integrations) for how to connect one.

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/integrations" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "shopify": [
    {
      "id": "conn-123",
      "shop_domain": "acme-store.myshopify.com",
      "is_active": true,
      "connected_at": "2025-04-01T12:00:00",
      "verification_status": "verified"
    }
  ]
}
```

***

## Managing campaigns

These endpoints let you create, edit, pause, and archive advertiser campaigns from the same API key surface.

### POST `/advertiser-dashboard/campaigns`

Create a campaign with inline ad groups (recommended) or legacy ad-unit links.

<ParamField body="name" type="string" required>
  Campaign name.
</ParamField>

<ParamField body="landing_page" type="string" required>
  Destination URL for the campaign.
</ParamField>

<ParamField body="daily_budget" type="number" required>
  Daily budget in dollars.
</ParamField>

<ParamField body="status" type="string">
  Campaign status. Use `draft`, `active`, or `paused`.
</ParamField>

<ParamField body="campaign_objective" type="string">
  Campaign objective.
</ParamField>

<ParamField body="billing_model" type="string">
  Billing model.
</ParamField>

<ParamField body="bid_strategy" type="string">
  Bid strategy.
</ParamField>

<ParamField body="is_active" type="boolean">
  Whether the campaign is active.
</ParamField>

<ParamField body="start_time" type="string">
  Optional start timestamp.
</ParamField>

<ParamField body="end_time" type="string">
  Optional end timestamp.
</ParamField>

<ParamField body="click_goal" type="integer">
  Optional click goal.
</ParamField>

<ParamField body="max_cpc" type="number">
  Optional max cost-per-click bid in dollars. When set, OpenAI-network ad groups place a fixed per-click bid at this amount (on a conversions-objective campaign this is oCPC: conversion-optimized, billed per click at your bid). When omitted, conversions campaigns use OpenAI's `maximize_conversions` auto-bidding — no bid is required — and other objectives use per-impression bidding. `0` is treated as unset.
</ParamField>

<ParamField body="impression_goal" type="integer">
  Optional impression goal.
</ParamField>

<ParamField body="max_cpm" type="number">
  Optional max CPM.
</ParamField>

<ParamField body="conversion_type" type="string">
  Optional conversion type.
</ParamField>

<ParamField body="max_cpa" type="number">
  Optional max CPA.
</ParamField>

<ParamField body="demographics" type="object">
  Optional demographics configuration.
</ParamField>

<ParamField body="utm_params" type="array">
  Optional UTM parameters as key/value pairs.
</ParamField>

<ParamField body="allowed_countries" type="array">
  Optional country targeting.
</ParamField>

<ParamField body="allowed_devices" type="array">
  Optional device targeting.
</ParamField>

<ParamField body="lead_form_config" type="object">
  Optional lead-form configuration.
</ParamField>

<ParamField body="ad_groups" type="array">
  Ad groups to create inline with the campaign. Each entry uses the same shape as [`POST /advertiser-dashboard/ad-groups`](#post-/advertiser-dashboard/ad-groups) minus `campaign_id` (it is set from the new campaign). Manual groups may include authored `ads`; generative groups generate their own ads and reject authored `ads`.
</ParamField>

<ParamField body="ad_unit_ids" type="array">
  Legacy: one or more existing ad-unit IDs to link. Still supported, but ad groups are the current model.
</ParamField>

<Note>
  Provide at least one of `ad_groups` or `ad_unit_ids` — the router returns `422` if both are missing or empty. Network selection is server-managed and always uses Gravity. `keywords` is not accepted here, and `network_type` / `network_config` return `422` if included.
</Note>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://platform.trygravity.ai/advertiser-dashboard/campaigns \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Summer Sale",
      "landing_page": "https://acme.com/summer",
      "daily_budget": 100,
      "status": "active",
      "campaign_objective": "conversions",
      "billing_model": "cpm",
      "bid_strategy": "lowest_cost",
      "allowed_countries": ["US", "CA"],
      "allowed_devices": ["desktop", "mobile"],
      "ad_groups": [
        {
          "name": "Summer Sale — Generative",
          "group_type": "generative",
          "context": "Summer footwear sale, focus on running shoes"
        },
        {
          "name": "Summer Sale — Manual",
          "group_type": "manual",
          "ads": [
            {
              "headline": "50% Off Running Shoes",
              "copy": "Summer sale on all running shoes.",
              "cta": "Shop Now",
              "landing_page": "https://acme.com/summer"
            }
          ]
        }
      ]
    }'
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaign_id": "camp-456",
  "user_id": "adv-123",
  "name": "Summer Sale",
  "landing_page": "https://acme.com/summer",
  "daily_budget": 100,
  "status": "active",
  "campaign_objective": "conversions",
  "is_active": true,
  "created_at": "2025-05-01T00:00:00",
  "start_time": "2025-05-01T00:00:00",
  "end_time": null,
  "daily_budget_remaining": 87.5,
  "allowed_countries": ["US", "CA"],
  "allowed_devices": ["desktop", "mobile"],
  "utm_params": null,
  "custom_favicon_url": null,
  "deployment_status": "pending_review",
  "review_status": "pending_review",
  "submitted_for_review_at": "2025-05-01T00:00:00",
  "reviewed_at": null,
  "review_notes": null,
  "lead_form_config": null,
  "ad_groups": [
    {
      "id": "adg-789",
      "campaign_id": "camp-456",
      "name": "Summer Sale — Generative",
      "group_type": "generative",
      "status": "active",
      "ads": [],
      "ads_total": 0
    },
    {
      "id": "adg-790",
      "campaign_id": "camp-456",
      "name": "Summer Sale — Manual",
      "group_type": "manual",
      "status": "active",
      "ads": [
        {
          "id": "ad-901",
          "ad_group_id": "adg-790",
          "campaign_id": "camp-456",
          "headline": "50% Off Running Shoes",
          "copy": "Summer sale on all running shoes.",
          "cta": "Shop Now",
          "landing_page": "https://acme.com/summer",
          "source": "manual",
          "status": "active",
          "review_status": "pending"
        }
      ],
      "ads_total": 1
    }
  ]
}
```

Ad-group response objects also include targeting and budget fields (`context`, `landing_page`, `daily_budget`, `max_cpc`, `allowed_countries`, `allowed_devices`, etc.) — see [Managing ad groups & ads](#managing-ad-groups-&-ads).

### PATCH `/advertiser-dashboard/campaigns/{campaign_id}`

Edit a campaign or pause/unpause it.

<ParamField path="campaign_id" type="string" required>
  The campaign ID.
</ParamField>

<ParamField body="name" type="string">
  Optional campaign name.
</ParamField>

<ParamField body="landing_page" type="string">
  Optional landing page URL.
</ParamField>

<ParamField body="daily_budget" type="number">
  Optional daily budget.
</ParamField>

<ParamField body="campaign_objective" type="string">
  Optional campaign objective.
</ParamField>

<ParamField body="max_cpc" type="number">
  Optional max cost-per-click bid in dollars. When set, OpenAI-network ad groups place a fixed per-click bid at this amount; editing it re-syncs live ad groups with the new bid. Send `null` (or `0`) to clear it — conversions campaigns then use OpenAI's `maximize_conversions` auto-bidding.
</ParamField>

<ParamField body="status" type="string">
  Optional status. Use `active`, `paused`, or `draft`.
</ParamField>

<ParamField body="start_time" type="string">
  Optional start timestamp.
</ParamField>

<ParamField body="end_time" type="string">
  Optional end timestamp.
</ParamField>

<ParamField body="allowed_countries" type="array">
  Optional country targeting.
</ParamField>

<ParamField body="allowed_devices" type="array">
  Optional device targeting.
</ParamField>

<ParamField body="utm_params" type="array">
  Optional UTM parameters as key/value pairs.
</ParamField>

<ParamField body="custom_favicon_url" type="string">
  Optional favicon URL.
</ParamField>

<ParamField body="lead_form_config" type="object">
  Optional lead-form configuration.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X PATCH https://platform.trygravity.ai/advertiser-dashboard/campaigns/camp-456 \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "status": "paused"
    }'
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaign_id": "camp-456",
  "user_id": "adv-123",
  "name": "Summer Sale",
  "landing_page": "https://acme.com/summer",
  "daily_budget": 100,
  "status": "paused",
  "campaign_objective": "conversions",
  "is_active": false,
  "created_at": "2025-05-01T00:00:00",
  "start_time": "2025-05-01T00:00:00",
  "end_time": null,
  "daily_budget_remaining": 87.5,
  "allowed_countries": ["US", "CA"],
  "allowed_devices": ["desktop", "mobile"],
  "utm_params": null,
  "custom_favicon_url": null,
  "deployment_status": "deployed",
  "review_status": "approved",
  "submitted_for_review_at": "2025-05-01T00:00:00",
  "reviewed_at": "2025-05-01T01:00:00",
  "review_notes": null,
  "lead_form_config": null
}
```

Activating a campaign runs the billing gate and can return `402`. Budget or network changes can force the Gravity-only fallback, and activation can resubmit the campaign for review and enqueue background sync.

### PATCH `/advertiser-dashboard/campaigns/{campaign_id}/archive`

Toggle a campaign's archived state.

<ParamField path="campaign_id" type="string" required>
  The campaign ID.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X PATCH https://platform.trygravity.ai/advertiser-dashboard/campaigns/camp-456/archive \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaign_id": "camp-456",
  "is_archived": true
}
```

Archiving also sets `is_active = false` and `status = "paused"`.

## Managing ad groups & ads

Campaigns are now structured as **campaign → ad groups → ads**. An ad group is either:

* **`generative`** (default) — Gravity generates ads for the group from your `context` / `ad_prompt`. Authored `ads` are rejected with `422`.
* **`manual`** — you author the ads yourself via the `ads` array or the ads endpoints below. Generated ads are read-only; only manual ads can be edited or archived.

These endpoints share the same implementation and ownership checks as the Gravity dashboard, scoped to your API key's account.

### GET `/advertiser-dashboard/ad-groups`

List your ad groups, each with its ads embedded (up to 100 per group; `ads_total` carries the full count).

<ParamField query="campaign_id" type="string">
  Filter to a single campaign.
</ParamField>

<ParamField query="include_archived" type="boolean" default="false">
  Include archived groups.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl "https://platform.trygravity.ai/advertiser-dashboard/ad-groups?campaign_id=camp-456" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
[
  {
    "id": "adg-789",
    "campaign_id": "camp-456",
    "name": "Summer Sale — Generative",
    "group_type": "generative",
    "status": "active",
    "context": "Summer footwear sale, focus on running shoes",
    "landing_page": null,
    "daily_budget": null,
    "auto_optimize_budget": true,
    "max_cpc": null,
    "allowed_countries": null,
    "allowed_devices": null,
    "sort_order": 0,
    "created_at": "2026-07-23 00:00:00",
    "updated_at": "2026-07-23 00:00:00",
    "ads": [],
    "ads_total": 0
  }
]
```

### POST `/advertiser-dashboard/ad-groups`

Create an ad group under one of your campaigns.

<ParamField body="campaign_id" type="string" required>
  The campaign to attach the group to. Must be owned by your account.
</ParamField>

<ParamField body="name" type="string" required>
  Ad-group name.
</ParamField>

<ParamField body="group_type" type="string" default="generative">
  `generative` or `manual`.
</ParamField>

<ParamField body="status" type="string" default="active">
  `active`, `paused`, or `archived`.
</ParamField>

<ParamField body="context" type="string">
  Targeting context used to match conversations (and to generate ads for generative groups).
</ParamField>

<ParamField body="negative_context" type="string">
  Conversations to avoid.
</ParamField>

<ParamField body="ad_prompt" type="string">
  Optional prompt guiding ad generation (generative groups).
</ParamField>

<ParamField body="landing_page" type="string">
  Default landing page for the group's ads.
</ParamField>

<ParamField body="daily_budget" type="integer">
  Optional group-level daily budget. `auto_optimize_budget` (default `true`) lets Gravity allocate budget across groups; `min_daily_budget` / `max_daily_budget` bound it.
</ParamField>

<ParamField body="max_cpc" type="number">
  Optional group-level max CPC.
</ParamField>

<ParamField body="allowed_countries" type="array">
  Optional country targeting override.
</ParamField>

<ParamField body="allowed_devices" type="array">
  Optional device targeting override.
</ParamField>

<ParamField body="ads" type="array">
  Authored ads (manual groups only). Each ad supports `name`, `headline` (≤80 chars), `copy` (≤500 chars), `cta` (≤80 chars), `description`, `landing_page`, `image_url`, and `status`.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://platform.trygravity.ai/advertiser-dashboard/ad-groups \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "campaign_id": "camp-456",
      "name": "Trail Runners — Manual",
      "group_type": "manual",
      "context": "Trail running gear",
      "ads": [
        {
          "headline": "Grip Every Trail",
          "copy": "Trail shoes built for mud, rock, and rain.",
          "cta": "Shop Trail",
          "landing_page": "https://acme.com/trail"
        }
      ]
    }'
  ```
</RequestExample>

Returns `201` with the created group and its ads (same shape as the list response). `422` if authored `ads` are sent to a generative group.

### GET `/advertiser-dashboard/ad-groups/{ad_group_id}`

Fetch a single ad group with its ads.

### PATCH `/advertiser-dashboard/ad-groups/{ad_group_id}`

Update any of the fields above (send only what changes). Converting a `manual` group to `generative` returns `422` while it still has live manual ads.

<RequestExample>
  ```bash theme={null}
  curl -X PATCH https://platform.trygravity.ai/advertiser-dashboard/ad-groups/adg-789 \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"status": "paused", "daily_budget": 50}'
  ```
</RequestExample>

### DELETE `/advertiser-dashboard/ad-groups/{ad_group_id}`

Archive an ad group (and its ads). Returns `204`.

### POST `/advertiser-dashboard/ad-groups/{ad_group_id}/ads`

Author an ad in a **manual** group. Returns `201` with the created ad; `422` on generative groups.

<RequestExample>
  ```bash theme={null}
  curl -X POST https://platform.trygravity.ai/advertiser-dashboard/ad-groups/adg-790/ads \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "headline": "50% Off Running Shoes",
      "copy": "Summer sale on all running shoes.",
      "cta": "Shop Now",
      "landing_page": "https://acme.com/summer"
    }'
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "id": "ad-901",
  "ad_group_id": "adg-790",
  "campaign_id": "camp-456",
  "name": null,
  "headline": "50% Off Running Shoes",
  "copy": "Summer sale on all running shoes.",
  "cta": "Shop Now",
  "description": null,
  "landing_page": "https://acme.com/summer",
  "image_url": null,
  "source": "manual",
  "status": "active",
  "review_status": "pending",
  "created_at": "2026-07-23 00:00:00",
  "updated_at": "2026-07-23 00:00:00"
}
```

### PATCH `/advertiser-dashboard/ads/{ad_id}`

Edit a manual ad (`headline`, `copy`, `cta`, `landing_page`, `status`, etc.). Generated ads are read-only and return `422`.

### DELETE `/advertiser-dashboard/ads/{ad_id}`

Archive a manual ad. Returns `204`.

***

## Legacy: ad units (deprecated)

<Warning>
  Ad units are the legacy creative model. They keep working for existing integrations — nothing breaks — but new integrations should use [ad groups & ads](#managing-ad-groups-&-ads) instead. Campaigns are now structured as campaign → ad group → ad.
</Warning>

### POST `/advertiser-dashboard/ad-units`

Create an ad unit owned by your advertiser account.

<ParamField body="name" type="string" required>
  Ad-unit name.
</ParamField>

<ParamField body="headline" type="string">
  Optional headline.
</ParamField>

<ParamField body="description" type="string" required>
  Required at submit time.
</ParamField>

<ParamField body="landing_page" type="string" required>
  Required at submit time.
</ParamField>

<ParamField body="static_ad_text" type="string" required>
  Required at submit time.
</ParamField>

<ParamField body="static_cta" type="string" required>
  Required at submit time.
</ParamField>

<ParamField body="favicon_url" type="string">
  Optional favicon URL.
</ParamField>

<ParamField body="image_url" type="string">
  Optional image URL.
</ParamField>

<ParamField body="brand_name" type="string">
  Optional brand name.
</ParamField>

<ParamField body="utm_params" type="array">
  Optional UTM parameters as key/value pairs.
</ParamField>

<Note>
  `ad_text_mode` is not accepted here. The router also rejects `source`, `status`, `review_status`, `review_reason`, `metadata`, and `generate_static_text` because those fields are server-managed.
</Note>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://platform.trygravity.ai/advertiser-dashboard/ad-units \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Summer headline variant",
      "headline": "Postgres for teams",
      "description": "Launch faster with managed Postgres.",
      "landing_page": "https://acme.com/summer",
      "static_ad_text": "Managed Postgres with built-in scaling.",
      "static_cta": "Start free",
      "brand_name": "Acme"
    }'
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "id": "ad-123",
  "name": "Summer headline variant",
  "headline": "Postgres for teams",
  "description": "Launch faster with managed Postgres.",
  "landing_page": "https://acme.com/summer",
  "static_ad_text": "Managed Postgres with built-in scaling.",
  "static_cta": "Start free",
  "favicon_url": "https://acme.com/favicon.ico",
  "image_url": null,
  "brand_name": "Acme",
  "source": "manual",
  "status": "draft",
  "review_status": "not_submitted",
  "review_reason": null,
  "utm_params": [],
  "created_at": "2025-05-01T00:00:00",
  "updated_at": "2025-05-01T00:00:00",
  "campaign_id": null
}
```

### POST `/advertiser-dashboard/campaigns/{campaign_id}/ad-units/generate`

Generate draft ad units from a landing page URL.

<ParamField path="campaign_id" type="string" required>
  The campaign ID.
</ParamField>

<ParamField body="site_url" type="string" required>
  Landing page URL to scrape.
</ParamField>

<ParamField body="count" type="integer">
  Number of draft ad units to generate, from 1 to 5.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://platform.trygravity.ai/advertiser-dashboard/campaigns/camp-456/ad-units/generate \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "site_url": "https://acme.com/summer",
      "count": 3
    }'
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaign_id": "camp-456",
  "ad_units": [
    {
      "id": "ad-123",
      "name": "Managed Postgres for teams",
      "headline": "Postgres for teams",
      "description": "Launch faster with managed Postgres.",
      "landing_page": "https://acme.com/summer",
      "static_ad_text": "Managed Postgres with built-in scaling.",
      "static_cta": "Start free",
      "favicon_url": "https://acme.com/favicon.ico",
      "image_url": null,
      "brand_name": "Acme",
      "source": "scraped",
      "status": "draft",
      "review_status": "not_submitted",
      "review_reason": null,
      "utm_params": [],
      "created_at": "2025-05-01T00:00:00",
      "updated_at": "2025-05-01T00:00:00",
      "campaign_id": null
    }
  ]
}
```

### POST `/advertiser-dashboard/campaigns/{campaign_id}/ad-units`

Add ad units to a campaign without replacing the existing set.

<ParamField path="campaign_id" type="string" required>
  The campaign ID.
</ParamField>

<ParamField body="ad_unit_ids" type="array" required>
  One or more ad-unit IDs. Duplicate IDs are deduped before write.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://platform.trygravity.ai/advertiser-dashboard/campaigns/camp-456/ad-units \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "ad_unit_ids": ["ad-123", "ad-456"]
    }'
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaign_id": "camp-456",
  "ad_unit_ids": ["ad-123", "ad-456"]
}
```

The router marks linked units for review when needed, resubmits already-approved campaigns that now contain unreviewed ad units, and schedules the campaign ad-unit sync task in the background.

### PUT `/advertiser-dashboard/campaigns/{campaign_id}/ad-units`

Replace the campaign's ad-unit set.

<ParamField path="campaign_id" type="string" required>
  The campaign ID.
</ParamField>

<ParamField body="ad_unit_ids" type="array" required>
  Replacement ad-unit IDs. Duplicate IDs are deduped before write.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X PUT https://platform.trygravity.ai/advertiser-dashboard/campaigns/camp-456/ad-units \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "ad_unit_ids": ["ad-456", "ad-789"]
    }'
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "campaign_id": "camp-456",
  "ad_unit_ids": ["ad-456", "ad-789"]
}
```

This route has the same ownership checks and review/sync behavior as the `POST` variant, but it replaces the existing linked set before returning.

### PATCH `/advertiser-dashboard/ad-units/{ad_unit_id}/archive`

Toggle an ad unit's archived state.

<ParamField path="ad_unit_id" type="string" required>
  The ad unit ID.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X PATCH https://platform.trygravity.ai/advertiser-dashboard/ad-units/ad-123/archive \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
{
  "ad_unit_id": "ad-123",
  "status": "archived"
}
```

Unarchiving restores `status = "draft"`.

***

## Managing pixels

### POST `/advertiser-dashboard/pixels`

Create a conversion pixel for a site. Verification of the pixel installation runs in the background after creation.

<ParamField body="url" type="string" required>
  The site URL the pixel will be installed on. `https://` is prepended if no scheme is given.
</ParamField>

<ParamField body="name" type="string">
  Display name for the pixel. Defaults to `Untitled Pixel`.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://platform.trygravity.ai/advertiser-dashboard/pixels \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://acme.com",
      "name": "Acme main site"
    }'
  ```
</RequestExample>

**Response** (`201`)

```json theme={null}
{
  "pixel_id": "pix-123",
  "url": "https://acme.com",
  "name": "Acme main site",
  "is_active": true,
  "is_functional": null,
  "account_id": "acct-789",
  "created_at": "2026-07-01T12:00:00"
}
```

`is_functional` starts as `null` and flips to `true`/`false` once background verification confirms whether the pixel script is installed on the URL. Returns `409` if a pixel already exists for the URL, and `422` if the URL is invalid.

### GET `/advertiser-dashboard/pixels`

List all pixels on your account.

<RequestExample>
  ```bash theme={null}
  curl https://platform.trygravity.ai/advertiser-dashboard/pixels \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</RequestExample>

**Response**

```json theme={null}
[
  {
    "pixel_id": "pix-123",
    "url": "https://acme.com",
    "name": "Acme main site",
    "is_active": true,
    "is_functional": true,
    "account_id": "acct-789",
    "created_at": "2026-07-01T12:00:00"
  }
]
```

***

## MCP server

The reporting endpoints above are also exposed as an [MCP](https://modelcontextprotocol.io) server, so AI agents and MCP-capable clients can query your account directly.

* **Endpoint:** `POST https://platform.trygravity.ai/mcp` (JSON-RPC 2.0 over HTTP)
* **Auth:** `X-API-Key` header (or `Authorization: Bearer <key>`)

An advertiser API key unlocks read-only tools mirroring this API: `advertiser_get_info`, `advertiser_list_campaigns`, `advertiser_get_campaign`, `advertiser_get_metrics`, `advertiser_get_conversions`, `advertiser_get_billing`, `advertiser_get_events_analytics`, `advertiser_get_conversion_stats`, and `advertiser_get_integrations`.

```bash theme={null}
curl https://platform.trygravity.ai/mcp \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
```

***

## Code examples

<CodeGroup>
  ```python Python theme={null}
  import requests

  API_KEY = "your-api-key"
  BASE = "https://platform.trygravity.ai/advertiser-dashboard"

  # Get last 30 days of metrics
  metrics = requests.get(
      f"{BASE}/metrics",
      headers={"X-API-Key": API_KEY},
      params={"days": 30},
  ).json()

  print(f"Impressions: {metrics['total_impressions']:,}")
  print(f"Spend: ${metrics['total_spend']:.2f}")
  print(f"Conversions: {metrics['total_conversions']}")
  print(f"ROAS: {metrics['total_revenue'] / metrics['total_spend']:.2f}x" if metrics['total_spend'] > 0 else "")
  ```

  ```javascript JavaScript theme={null}
  const API_KEY = "your-api-key";
  const BASE = "https://platform.trygravity.ai/advertiser-dashboard";

  const res = await fetch(`${BASE}/metrics?days=30`, {
    headers: { "X-API-Key": API_KEY },
  });
  const metrics = await res.json();

  console.log(`Impressions: ${metrics.total_impressions.toLocaleString()}`);
  console.log(`Spend: $${metrics.total_spend.toFixed(2)}`);
  console.log(`Conversions: ${metrics.total_conversions}`);
  ```

  ```bash curl theme={null}
  # Get advertiser info
  curl https://platform.trygravity.ai/advertiser-dashboard/info \
    -H "X-API-Key: YOUR_API_KEY"

  # Get metrics for a specific date range
  curl "https://platform.trygravity.ai/advertiser-dashboard/metrics?start_date=2025-05-01&end_date=2025-05-31" \
    -H "X-API-Key: YOUR_API_KEY"

  # List active campaigns
  curl "https://platform.trygravity.ai/advertiser-dashboard/campaigns?status=active" \
    -H "X-API-Key: YOUR_API_KEY"
  ```
</CodeGroup>

## Errors

| Status | Description                                                                                                                                                                                                                                            |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `401`  | Invalid or missing `X-API-Key`.                                                                                                                                                                                                                        |
| `400`  | Invalid parameter (e.g. out-of-range `days`).                                                                                                                                                                                                          |
| `402`  | Campaign activation is blocked by billing.                                                                                                                                                                                                             |
| `404`  | Campaign not found or doesn't belong to your account.                                                                                                                                                                                                  |
| `422`  | Validation errors, including missing/empty `ad_unit_ids`, removed fields like `keywords` and `ad_text_mode`, missing required ad-unit fields, invalid favicon URLs, invalid lead-form config, or no update fields on `PATCH /campaigns/{campaign_id}`. |

## Questions

Email [support@trygravity.ai](mailto:support@trygravity.ai) for anything API-related. We read it.
