# 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" />

The central scoring table for ad creatives. Every generated container and every eligible UGC post has a row here with an `organicScore` (0–10), a `scoringPool` (`generated` / `ugc` / `manual`), and enough context for an agent to decide what to promote, refresh, or exclude.

This is where a partner agent reads the signal Layers' own optimizer reads. Use `organicScore` as the "how healthy is this creative?" gauge; use `eligibility` for the yes/no of whether the current threshold would let it run as an ad; use `override` to see if a human or earlier agent decision has already pinned it.

<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_01HX9Y7K8M2P4RSTUV56789AB/ads-content?eligible=true&sort=score_desc&limit=25" \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..."
    ```
  </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": "lp_live_HHMRNJ2AHYJ6WZJ4 (api_key_id)",
        "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 reserved for a future enrichment that will list every layer\_meta\_ads / layer\_tiktok\_ads junction holding this `adsContentId`. It is always `[]` in Phase 1 — run [`GET /v1/projects/:projectId/ads/ads?adsContentId=…`](/docs/api/reference/ads/list-ads) to discover where a creative is already running.

## How Layers itself uses these signals [#how-layers-itself-uses-these-signals]

Layers' ad refresh loop reads the same rows: select new eligible creatives, deactivate underperforming ads, respect overrides. Nothing about `organicScore` pauses an already-running ad — pause decisions are made from live ad performance (CPA, conversion rate, fatigue), not from decay. Your agent should do the same. See [Publish to learn](/docs/api/guides/publish-to-learn) for the full feedback loop, including when to promote more of a winner versus refresh a fatigued creative.

## 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. Scoring runs on a six-hour cron. 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](/docs/api/guides/publish-to-learn) — the feedback loop
* [`GET /v1/ads-metrics`](/docs/api/reference/ads/ads-metrics) — paid performance by scope
