# GET /v1/projects/:projectId/ads-content (/docs/api/reference/metrics/ads-content)



<Endpoint method="GET" path="/v1/projects/{projectId}/ads-content" auth="Bearer" scope="metrics:read" phase="1" />

Returns the creative inventory for a project: generated content, UGC posts, and manually added items. Each item includes its `scoringPool` (provenance: `generated` / `ugc` / `manual`), paid results in `adPerformance`, explicit `eligibility`, and override state.

Use `eligibility` for the current promote/do-not-promote decision and `override` to see whether a user has pinned the item in or out. For observed performance use [`GET /v1/projects/:projectId/top-performers`](/docs/api/reference/metrics/top-performers), which ranks on raw views, engagement, conversions and ROAS.

<Callout type="warn" title="The creative score is deprecated">
  `organicScore`, `organicPerformance`, `scoredAt`, `scoringVersion`, the `minScore` filter and the `score_desc` / `score_asc` sorts are deprecated as of 2026-09-02. Layers no longer computes a pre-launch quality score for a creative and nothing selects, ranks or recommends on it. The fields stay on the wire so existing integrations keep parsing: `organicScore` is always `0`, `organicPerformance` is always `null`, `minScore` is accepted and ignored, and the score sorts resolve to `created_at_desc`. They will be removed in a later version; migrate to `eligibility`, `override` and `top-performers`.
</Callout>

<Parameters
  title="Path"
  rows="[
  { name: 'projectId', type: 'string (UUID)', required: true, description: 'Project to list within.' },
]"
/>

<Parameters
  title="Query"
  rows="[
  { name: 'scoringPool', type: 'string[]', description: 'Filter by pool. Repeat for multiple.', enum: ['generated', 'ugc', 'manual'] },
  { name: 'minScore', type: 'number', description: 'Deprecated. Accepted for compatibility and ignored; the creative score no longer filters this list. 0 to 10.' },
  { name: 'override', type: 'string', description: 'Filter by override state.', enum: ['include', 'exclude', 'none'] },
  { name: 'eligible', type: 'boolean', description: 'true keeps items Layers can promote right now; false keeps the refused items. See eligibility.reason for the refusal rules.' },
  { name: 'sort', type: 'string', description: 'Sort order. created_at_desc is newest first. score_desc and score_asc are deprecated and resolve to created_at_desc; scored_at_desc is deprecated and orders by the deprecated scoredAt until it is removed.', enum: ['created_at_desc', 'score_desc', 'score_asc', 'scored_at_desc'], default: 'created_at_desc' },
  { name: 'cursor', type: 'string', description: 'Opaque pagination cursor from nextCursor. A malformed cursor, or one issued before the sort changed, is rejected with 422; re-read from the first page.' },
  { name: 'limit', type: 'number', description: 'Page size, 1–200.', default: '50' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl "https://api.layers.com/v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/ads-content?eligible=true&sort=created_at_desc&limit=25" \
      -H "Authorization: Bearer lp_..."
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const res = await fetch(
      `https://api.layers.com/v1/projects/${projectId}/ads-content?eligible=true&sort=created_at_desc&limit=25`,
      { headers: { Authorization: `Bearer ${apiKey}` } },
    );
    const { items, nextCursor } = await res.json();

    // Promote the 3 newest that aren't already promoted.
    const candidates = items
      .filter((i) => i.activePromotions?.length === 0)
      .slice(0, 3);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import httpx

    r = httpx.get(
        f"https://api.layers.com/v1/projects/{project_id}/ads-content",
        params={"eligible": True, "sort": "created_at_desc", "limit": 25},
        headers={"Authorization": f"Bearer {api_key}"},
    )
    items = r.json()["items"]
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="OK">
  ```json
  {
    "items": [
      {
        "adsContentId": "adc_6f5d4c3b-2a1e-49d8-87a6-5b4c3d2e1f0a",
        "sourceType": "content_container",
        "sourceId": "cnt_7d18b9a1-8b2c-4f3e-a4d5-6e7f8a9b0c1d",
        "platformPostId": null,
        "scoringPool": "generated",
        "organicScore": 0,
        "organicPerformance": null,
        "adPerformance": { "spend": 63.50, "conversions": 22, "cpa": 2.89, "roas": 4.1 },
        "scoredAt": "2026-04-18T12:00:00Z",
        "scoringVersion": 3,
        "eligibility": { "isEligible": true, "reason": "eligible by default; no override set" },
        "override": null,
        "overrideNote": null,
        "overrideSetBy": null,
        "overrideSetAt": null,
        "activePromotions": []
      },
      {
        "adsContentId": "adc_2b1a0f9e-8d7c-4b6a-9584-3c2d1e0f9a8b",
        "sourceType": "platform_post",
        "sourceId": null,
        "platformPostId": "pp_a4f1c9d2-3e5b-4a76-8c19-2d0f7b6e5a34",
        "scoringPool": "ugc",
        "organicScore": 0,
        "organicPerformance": null,
        "adPerformance": null,
        "scoredAt": "2026-04-18T12:00:00Z",
        "scoringVersion": 3,
        "eligibility": { "isEligible": true, "reason": "included by override" },
        "override": "include",
        "overrideNote": null,
        "overrideSetBy": "api_key",
        "overrideSetAt": "2026-04-15T18:00:00Z",
        "activePromotions": []
      }
    ],
    "nextCursor": null
  }
  ```
</Response>

## Reading the signals [#reading-the-signals]

`eligibility.isEligible` is explicit, and it is the same decision Layers applies when it actually builds the ad - so a creative reported eligible here is one the campaign builder will accept. `eligibility.reason` names the rule that applied:

* `excluded by override` - the manual `override` is `"exclude"`.
* `safety_failed` - the creative failed safety review. This is not clearable with `override: "include"`.
* `flagged` - a moderator flagged the creative. Also not clearable with `override: "include"`; only the unlock action clears it.
* `spark_authorization_not_live` - the creative's TikTok Spark authorization can no longer launch a new campaign: it has expired, been revoked, or is already bound to a running ad.
* `spark_window_unknown` - the Spark authorization has no confirmed expiry yet, so Layers cannot tell whether it can still launch.
* `source_unavailable` - the media behind the creative is no longer available (deleted, or no longer a completed piece of content), so it cannot be built into an ad.

None of these is clearable with `override: "include"`. Otherwise it is eligible: `included by override` when pinned, or `eligible by default; no override set`. No score, threshold or decay is involved.

<Callout type="info">
  The last three reasons are new, and additive: every reason value returned before they existed still means exactly what it meant. They surface refusals this endpoint previously could not express, where a creative was reported eligible and then refused at build time.
</Callout>

`scoringPool` is provenance, not a quality signal: where the creative came from (`generated`, `ugc`, `manual`).

`adPerformance` is paid performance from campaigns that ran this creative (spend, conversions, CPA, ROAS). It is observed, not predicted.

`activePromotions` is currently returned as an empty array. To discover where a creative is already running, call [`GET /v1/projects/:projectId/ads/ads?adsContentId=…`](/docs/api/reference/ads/list-ads) with the `adsContentId` exactly as returned here — the prefixed `adc_<UUID>` form. That filter previously matched nothing for every value it accepted; it now resolves.

Nothing on this endpoint pauses an already-running ad. Pause decisions should use live ad performance such as CPA, conversion rate, and fatigue. See Publish to learn for the full feedback loop.

### Deprecated fields [#deprecated-fields]

These stay on the wire with fixed values until they are removed. Do not build on them.

| Field                                       | Value now                                  | Replacement                                                                   |
| ------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- |
| `organicScore`                              | always `0`                                 | `top-performers` for observed metrics; `eligibility` for the promote decision |
| `organicPerformance`                        | always `null`                              | `top-performers` (`organic.views`, `organic.engagementRate`)                  |
| `scoredAt`                                  | last write of the retired score, or `null` | none; not a signal                                                            |
| `scoringVersion`                            | version of the retired formula, or absent  | none; not a signal                                                            |
| `minScore` (query)                          | accepted, ignored                          | `eligible`, `override`                                                        |
| `sort=score_desc`, `sort=score_asc` (query) | resolve to `created_at_desc`               | `created_at_desc`                                                             |
| `sort=scored_at_desc` (query)               | still orders by `scoredAt` until removal   | `created_at_desc`                                                             |

## Notes [#notes]

* `platformPostId` is the opaque platform-post identity, `pp_<UUID>` (see [ID formats](/docs/api/concepts/id-formats)). Pass it back verbatim to [`GET /v1/projects/:projectId/metrics?scope=platform_post`](/docs/api/reference/metrics/unified-metrics), which accepts the prefixed and bare forms alike. Changed 2026-09-02 — this field previously carried the bare UUID.
* `adsContentId` is returned prefixed (`adc_<UUID>`). [`PATCH /v1/projects/:projectId/ads-content/:id`](/docs/api/reference/metrics/patch-ads-content) accepts it either prefixed or bare, per the [tolerance rule](/docs/api/concepts/id-formats#the-tolerance-rule) - pass back what you were given.
* `sourceId` is currently emitted as a **bare** UUID rather than the `cnt_<UUID>` the [ID formats catalog](/docs/api/concepts/id-formats#catalog) lists for a content container. Store the exact string you receive; a future release will move it to the prefixed form.
* Paginate with `cursor` until `nextCursor` is `null`. The default order is newest first; use `eligible=true` for the queue you iterate.
* A cursor is bound to the sort it was issued under. After changing `sort`, re-read from the first page.

## See also [#see-also]

* [`PATCH /v1/projects/:projectId/ads-content/:id`](/docs/api/reference/metrics/patch-ads-content) - pin a creative with include or exclude
* [`GET /v1/projects/:projectId/top-performers`](/docs/api/reference/metrics/top-performers) - cross-source ranking on observed metrics
* Publish to learn - the feedback loop
* [`GET /v1/ads-metrics`](/docs/api/reference/ads/ads-metrics) - paid performance by scope
