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

# Campaign media

> Track media, manage creator content, and read performance metrics via /sdk/v1.

All endpoints below require:

* `X-Application-Id`
* `X-Api-Key`

Media can be added and tracked independently of contracts. You do not need to set up a contract before tracking a creator's media.

To read or update eligibility scores for tracked posts, see
[List eligibility scores](/campaign-media/eligibility-scores) and
[Update eligibility scores](/campaign-media/update-eligibility-scores).

## GET /sdk/v1/campaigns/:campaignId/media

List all tracked media across the campaign with aggregate metrics.

### Query parameters

* `query` (optional; searches media URLs/titles)
* `platform` (optional; filter by platform, e.g., `instagram`, `tiktok`, `youtube`)

### Response

```json theme={null}
{
  "success": true,
  "data": [
    {
      "campaignCreatorMediaId": "ccm_...",
      "mediaUrl": "https://www.tiktok.com/@creator/video/7350000000000000000",
      "platform": "tiktok",
      "mediaType": "post",
      "isSlideshow": true,
      "authorUsername": "creator",
      "authorDisplayName": "Creator Name",
      "title": "Launch video",
      "captionText": "Our spring launch",
      "hashtags": ["springlaunch", "partner"],
      "audioTitle": "Original sound",
      "audioArtist": "Creator Name",
      "audioIsOriginal": true,
      "views": 5000,
      "likes": 320,
      "comments": 15,
      "shares": 8
    }
  ],
  "metrics": {
    "trackedMediaCount": 12,
    "trackedPostCount": 10,
    "trackedAccountCount": 2,
    "uniqueCreatorsCount": 3,
    "totalViews": 25000,
    "totalLikes": 1200,
    "totalComments": 80,
    "totalShares": 45,
    "totalBookmarks": 30
  },
  "asOfTimestamp": "2026-03-03T12:00:00.000Z"
}
```

<Note>
  `asOfTimestamp` indicates when tracking metrics were last refreshed. It may be `null` if no scraping has occurred yet.
</Note>

<Info>
  TikTok media rows include `isSlideshow`: `true` for photo carousel/slideshow posts, `false` for videos, and `null` when the value is not known yet or not applicable. Use this field instead of deriving TikTok content type from the `mediaUrl` path.
</Info>

When the platform actor supplies it, campaign media rows include normalized
author fields (`authorPlatformId`, `authorUsername`, `authorDisplayName`), content
fields (`title`, `captionText`, `hashtags`), audio fields (`audioPlatformId`,
`audioTitle`, `audioArtist`, `audioAlbum`, `audioIsOriginal`), and
`metadataCollectedAt`. Unavailable fields are `null`, and `hashtags` is an empty
array when none are available.

## GET /sdk/v1/campaigns/:campaignId/creators/media

List a campaign creator's tracked media, broken down into accounts and posts.

### Query parameters

* `email` (required)
* `includeGroups` (optional; `true` to include explicit group rows)
* `page` (default `1`)
* `limit` (default `5`)

### Response

```json theme={null}
{
  "success": true,
  "data": {
    "creatorEmail": "creator@example.com",
    "creatorProfileId": "cp_...",
    "campaignCreatorRecordId": "ccr_...",
    "accounts": [
      {
        "campaignCreatorMediaId": "ccm_...",
        "mediaUrl": "https://www.instagram.com/creatorhandle",
        "platform": "instagram",
        "mediaType": "account",
        "isSlideshow": null
      }
    ],
    "posts": [
      {
        "campaignCreatorMediaId": "ccm_...",
        "mediaUrl": "https://www.tiktok.com/@creator/video/7350000000000000000",
        "platform": "tiktok",
        "mediaType": "post",
        "isSlideshow": true,
        "authorUsername": "creator",
        "captionText": "Our spring launch",
        "hashtags": ["springlaunch", "partner"],
        "audioTitle": "Original sound",
        "views": 5000,
        "likes": 320,
        "comments": 15,
        "shares": 8
      }
    ]
  },
  "pagination": {
    "accounts": { "page": 1, "limit": 50, "totalPages": 1, "totalItems": 1 },
    "posts": { "page": 1, "limit": 50, "totalPages": 1, "totalItems": 1 }
  },
  "asOfTimestamp": "2026-03-03T12:00:00.000Z"
}
```

When `includeGroups=true`, the response includes an additional `groups` array and `pagination.groups` object.
Group rows also include `isSlideshow` when the winning media item has a known TikTok slideshow state.
Normalized author, content, hashtag, and audio metadata on a group row comes from
that group's winning media item.

### Common errors

* `400`: missing `email`
* `404`: creator not found in this campaign

## POST /sdk/v1/campaigns/:campaignId/creators/media

Add or update tracked media for a campaign creator. If the creator is not yet a member of the campaign, they are **automatically added** (implicit membership).

Background scraping is triggered automatically after media is saved.

### Body

```json theme={null}
{
  "email": "creator@example.com",
  "media": [
    {
      "mediaUrl": "https://www.instagram.com/creatorhandle",
      "platform": "instagram",
      "startDate": "2026-03-01T00:00:00.000Z",
      "trackForValue": 7,
      "trackForUnit": "DAYS"
    }
  ]
}
```

<Info>
  Tracking is start-based. Provide `startDate` (or legacy `trackStartAt`) and
  omit media-level end dates. For account URLs, Grade continues discovering
  eligible posts automatically, while `trackForValue` and `trackForUnit` define
  each discovered post's relative payable tracking window.
</Info>

<ParamField body="email" type="string" required>
  Creator email address.
</ParamField>

<ParamField body="media" type="array" required>
  Array of media items to track.
</ParamField>

Each item in the `media` array accepts:

<ParamField body="media[].mediaUrl" type="string" required>
  URL of the post or account to track.
</ParamField>

<ParamField body="media[].platform" type="string">
  Platform name (e.g., `instagram`, `tiktok`, `youtube`). Optional if the platform can be derived from the URL.
</ParamField>

<ParamField body="media[].startDate" type="string">
  Optional ISO date when tracking starts. If omitted, tracking starts at save time.
</ParamField>

<ParamField body="media[].trackStartAt" type="string">
  Backward-compatible alias for `startDate`.
</ParamField>

<ParamField body="media[].trackForValue" type="number">
  Relative tracking duration for discovered posts. Provide this together with
  `trackForUnit`, for example `7` with `DAYS`.
</ParamField>

<ParamField body="media[].trackForUnit" type="string">
  Relative tracking unit. Must be `DAYS` or `MONTHS` when `trackForValue` is
  provided.
</ParamField>

### Relative tracking windows

Use `trackForValue` and `trackForUnit` to choose how long each discovered post is tracked after it is uploaded.

Examples:

* 7-day window: `"trackForValue": 7`, `"trackForUnit": "DAYS"`
* 30-day window: `"trackForValue": 30`, `"trackForUnit": "DAYS"`
* 2-month window: `"trackForValue": 2`, `"trackForUnit": "MONTHS"`

For example, if a creator uploads a video on March 1 and you send `trackForValue: 7` with `trackForUnit: "DAYS"`, Grade counts the video's views for the first 7 days after upload. If the campaign pays CPM or bonuses after tracking completes, the video becomes payable after that 7-day window is complete.

<ParamField body="media[].captionFilter" type="string">
  Caption filter for account-type URLs (max 256 characters). Only applies to account media.
</ParamField>

<ParamField body="media[].campaignCreatorMediaId" type="string">
  Existing media record ID to update. Omit to create a new media entry.
</ParamField>

### Response

```json theme={null}
{
  "success": true,
  "data": {
    "campaignId": "camp_...",
    "creatorEmail": "creator@example.com",
    "creatorProfileId": "cp_...",
    "campaignCreatorRecordId": "ccr_...",
    "saved": true
  }
}
```

### Common errors

* `400`: missing `email` or `media`
* `400`: `startDate` is invalid
* `400`: `endDate` and `trackEndAt` are not supported for creator media. Use `trackForValue` and `trackForUnit` for relative tracking windows.
* `400`: `trackForValue` and `trackForUnit` must be provided together
* `400`: `trackForUnit` must be `DAYS` or `MONTHS`

## DELETE /sdk/v1/campaigns/:campaignId/creators/media

Remove a tracked media item from a campaign creator.

### Body

```json theme={null}
{
  "email": "creator@example.com",
  "campaignCreatorMediaId": "ccm_..."
}
```

### Response

```json theme={null}
{
  "success": true,
  "data": {
    "campaignId": "camp_...",
    "creatorEmail": "creator@example.com",
    "creatorProfileId": "cp_...",
    "campaignCreatorRecordId": "ccr_...",
    "campaignCreatorMediaId": "ccm_...",
    "removed": true
  }
}
```

### Common errors

* `400`: missing `email` or `campaignCreatorMediaId`
* `404`: media item not found

## GET /sdk/v1/campaigns/:campaignId/media/discovered-posts

List posts discovered for an account-type media item. When you track an account URL (e.g., `https://www.instagram.com/creatorhandle`), the system periodically discovers new posts from that account and tracks them automatically.

### Query parameters

* `sourceCampaignCreatorMediaId` (required; the account media item ID)
* `page` (default `1`)
* `limit` (default `5`)
* `startDate` / `endDate` (optional, `YYYY-MM-DD`; provide both together)
* `performanceWindowDays` (optional integer, 1-366)

By default, metric fields such as `views` return the latest cumulative scrape
value. When `startDate` and `endDate` are provided, those same fields return
the metric delta for that date range, using the latest scrape inside the range
minus the latest scrape before the range. When `performanceWindowDays` is
provided, each post uses its own `uploadDate` as the window start and returns
the metric delta for the first N days after that upload.

Use either `startDate` / `endDate` or `performanceWindowDays`, not both. Windowed
responses also include `viewsTotal`, `likesTotal`, `commentsTotal`, `sharesTotal`,
and `metricsWindow` so you can distinguish the current cumulative totals from
the requested window deltas. If `performanceWindowDays` is requested but a post
does not yet have an `uploadDate`, window metric fields are `null` and
`metricsWindow.status` is `missingUploadDate`.

### Response

```json theme={null}
{
  "success": true,
  "data": [
    {
      "campaignCreatorMediaId": "ccm_...",
      "mediaUrl": "https://www.tiktok.com/@creator/video/7350000000000000001",
      "platform": "tiktok",
      "isSlideshow": false,
      "authorUsername": "creator",
      "captionText": "A newly discovered post",
      "hashtags": ["partner"],
      "audioTitle": "Original sound",
      "views": 1200,
      "likes": 85,
      "comments": 5,
      "uploadDate": "2026-03-01T14:22:00.000Z"
    }
  ],
  "pagination": { "page": 1, "limit": 50, "totalPages": 1, "totalItems": 3 }
}
```

Example first-7-days response:

```json theme={null}
{
  "success": true,
  "data": [
    {
      "campaignCreatorMediaId": "ccm_...",
      "mediaUrl": "https://www.tiktok.com/@creator/video/7350000000000000001",
      "isSlideshow": false,
      "views": 4200,
      "viewsTotal": 8900,
      "uploadDate": "2026-03-01T14:22:00.000Z",
      "metricsWindow": {
        "mode": "performanceWindowDays",
        "days": 7,
        "startAt": "2026-03-01T14:22:00.000Z",
        "endAt": "2026-03-08T14:22:00.000Z",
        "status": "ready",
        "asOfTimestamp": "2026-03-08T12:15:00.000Z"
      }
    }
  ],
  "pagination": { "page": 1, "limit": 50, "totalPages": 1, "totalItems": 1 }
}
```

### Common errors

* `400`: missing `sourceCampaignCreatorMediaId`
* `400`: `startDate` and `endDate` must be provided together
* `400`: `performanceWindowDays` must be an integer between 1 and 366
* `400`: use either `startDate` / `endDate` or `performanceWindowDays`, not both
* `404`: source media item not found or does not belong to your agency
