Layers
Partner APIAPI referenceMetrics

GET /v1/projects/:projectId/ads-content

Scored creatives — generated content and UGC with organicScore, scoringPool, and eligibility flags for ad promotion.

View as Markdown
GET/v1/projects/{projectId}/ads-content
Phase 1stable
Auth
Bearer
Scope
metrics:read

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.

Path
  • projectId
    string (UUID)required
    Project to list within.
Query
  • scoringPool
    string[]optional
    Filter by pool. Repeat for multiple.
    One of: generated, ugc, manual
  • minScore
    numberoptionaldefault: 0
    Keep rows with organicScore >= minScore. 0 to 10.
  • override
    stringoptional
    Filter by override state.
    One of: include, exclude, none
  • eligible
    booleanoptional
    Keep only items whose current state clears the 4.0 threshold (or has override=include).
  • sort
    stringoptionaldefault: score_desc
    Sort order.
    One of: score_desc, score_asc, scored_at_desc
  • cursor
    stringoptional
    Opaque pagination cursor from nextCursor. Forged or non-UUID cursors are rejected and treated as no cursor (first page).
  • limit
    numberoptionaldefault: 50
    Page size, 1–200.

Example request

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..."
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);
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"]

Response

200OK
{
  "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
}

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=… to discover where a creative is already running.

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 for the full feedback loop, including when to promote more of a winner versus refresh a fatigued creative.

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

On this page