# 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 scored creatives for a project: generated content, eligible UGC, and manually included items. Each item includes an `organicScore` (0–10), `scoringPool` (`generated` / `ugc` / `manual`), eligibility, and override state.

Use `organicScore` as the creative health signal, `eligibility` for the current promote/do-not-promote decision, and `override` to see whether a user has pinned the item in or out.

<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: 'Keep rows with organicScore >= minScore. 0 to 10.', default: '0' },
  { name: 'override', type: 'string', description: 'Filter by override state.', enum: ['include', 'exclude', 'none'] },
  { name: 'eligible', type: 'boolean', description: 'Keep only items whose current state clears the 4.0 threshold (or has override=include).' },
  { name: 'sort', type: 'string', description: 'Sort order.', enum: ['score_desc', 'score_asc', 'scored_at_desc'], default: 'score_desc' },
  { name: 'cursor', type: 'string', description: 'Opaque pagination cursor from nextCursor. Forged or non-UUID cursors are rejected and treated as no cursor (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=score_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=score_desc&limit=25`,
      { headers: { Authorization: `Bearer ${apiKey}` } },
    );
    const { items, nextCursor } = await res.json();

    // Promote the top 3 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": "score_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": "01HXC9A2B3C4D5E6F7G8H9J0K1",
        "sourceType": "content_container",
        "sourceId": "01HXC8A2B3C4D5E6F7G8H9J0K1",
        "platformPostId": null,
        "scoringPool": "generated",
        "organicScore": 8.4,
        "organicPerformance": {
          "best": { "isDefault": false, "views": 51200, "engagement_rate": 0.062 },
          "adBoost": 1.9
        },
        "adPerformance": { "spend": 63.50, "conversions": 22, "cpa": 2.89, "roas": 4.1 },
        "scoredAt": "2026-04-18T12:00:00Z",
        "scoringVersion": 3,
        "eligibility": { "isEligible": true, "reason": "organic_score 8.4 meets threshold" },
        "override": null,
        "overrideNote": null,
        "overrideSetBy": null,
        "overrideSetAt": null,
        "activePromotions": []
      },
      {
        "adsContentId": "01HXD2A2B3C4D5E6F7G8H9J0K1",
        "sourceType": "platform_post",
        "sourceId": null,
        "platformPostId": "01HXD1A2B3C4D5E6F7G8H9J0K1",
        "scoringPool": "ugc",
        "organicScore": 6.1,
        "organicPerformance": { "percentiles": { "quality": 0.71, "reach": 0.68 } },
        "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]

`organicScore` is on a 0–10 scale. Above 4.0 is eligible to run as an ad. The math differs by pool:

* **`generated`** - default 7.0 at creation, decays linearly toward 2.0 over 30 days. Good ad performance tacks up to +3.0 back on, so a winning ad can sit near 10 indefinitely.
* **`ugc`** - starts at 0 until real platform metrics arrive. Scored on quality and reach percentiles within the pool, with a 90-day freshness decay.
* **`manual`** - operator-set. Treated as authoritative until someone changes it.

`eligibility.isEligible` is the simple yes/no: does the current state clear the 4.0 bar, accounting for any `override`? `override: "include"` bypasses the threshold; `override: "exclude"` forces ineligible regardless of score. `null` means "follow the score."

`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).

Nothing about `organicScore` 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.

## Notes [#notes]

* Default score `7.0` with `organicPerformance.best.isDefault: true` means "not yet scored with real data" - fine for initial promotion, not a proof of performance.
* `scoringVersion` bumps when the formula changes. Scores across versions are not apples-to-apples.
* `scoredAt` lags real time. Do not treat the field as "now."
* `adsContentId` and `sourceId`/`platformPostId` are returned as raw UUIDs without partner prefixes. Pass the raw UUID back to PATCH and to ads-list filters.
* Paginate with `cursor` until `nextCursor` is `null`. Score-ranked lists can shift between requests - use `eligible=true` with a stable sort for the queue you iterate.

## 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
* Publish to learn - the feedback loop
* [`GET /v1/ads-metrics`](/docs/api/reference/ads/ads-metrics) - paid performance by scope
