GET /v1/projects/:projectId/ads-content
Scored creatives - generated content and UGC with organicScore, scoringPool, and eligibility flags for ad promotion.
/v1/projects/{projectId}/ads-content- Auth
- Bearer
- Scope
- metrics:read
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.
projectIdstring (UUID)requiredProject to list within.
scoringPoolstring[]optionalFilter by pool. Repeat for multiple.One of:generated,ugc,manualminScorenumberoptionaldefault: 0Keep rows with organicScore >= minScore. 0 to 10.overridestringoptionalFilter by override state.One of:include,exclude,noneeligiblebooleanoptionalKeep only items whose current state clears the 4.0 threshold (or has override=include).sortstringoptionaldefault: score_descSort order.One of:score_desc,score_asc,scored_at_desccursorstringoptionalOpaque pagination cursor from nextCursor. Forged or non-UUID cursors are rejected and treated as no cursor (first page).limitnumberoptionaldefault: 50Page size, 1–200.
Example request
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_..."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
{
"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
}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=….
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
- Default score
7.0withorganicPerformance.best.isDefault: truemeans "not yet scored with real data" - fine for initial promotion, not a proof of performance. scoringVersionbumps when the formula changes. Scores across versions are not apples-to-apples.scoredAtlags real time. Do not treat the field as "now."adsContentIdandsourceId/platformPostIdare returned as raw UUIDs without partner prefixes. Pass the raw UUID back to PATCH and to ads-list filters.- Paginate with
cursoruntilnextCursorisnull. Score-ranked lists can shift between requests - useeligible=truewith a stable sort for the queue you iterate.
See also
PATCH /v1/projects/:projectId/ads-content/:id- pin a creative with include or excludeGET /v1/projects/:projectId/top-performers- cross-source ranking- Publish to learn - the feedback loop
GET /v1/ads-metrics- paid performance by scope