GET /v1/projects/:projectId/ads-content
Creative inventory - generated content, UGC and manual uploads with provenance, override state and explicit eligibility for ad promotion.
/v1/projects/{projectId}/ads-content- Auth
- Bearer
- Scope
- metrics:read
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, which ranks on raw views, engagement, conversions and ROAS.
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.
projectIdstring (UUID)requiredProject to list within.
scoringPoolstring[]optionalFilter by pool. Repeat for multiple.One of:generated,ugc,manualminScorenumberoptionalDeprecated. Accepted for compatibility and ignored; the creative score no longer filters this list. 0 to 10.overridestringoptionalFilter by override state.One of:include,exclude,noneeligiblebooleanoptionaltrue keeps items Layers can promote right now; false keeps the refused items. See eligibility.reason for the refusal rules.sortstringoptionaldefault: created_at_descSort 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.One of:created_at_desc,score_desc,score_asc,scored_at_desccursorstringoptionalOpaque pagination cursor from nextCursor. A malformed cursor, or one issued before the sort changed, is rejected with 422; re-read from the 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=created_at_desc&limit=25" \
-H "Authorization: Bearer lp_..."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);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"]Response
{
"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
}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 manualoverrideis"exclude".safety_failed- the creative failed safety review. This is not clearable withoverride: "include".flagged- a moderator flagged the creative. Also not clearable withoverride: "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.
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.
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=… 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
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
platformPostIdis the opaque platform-post identity,pp_<UUID>(see ID formats). Pass it back verbatim toGET /v1/projects/:projectId/metrics?scope=platform_post, which accepts the prefixed and bare forms alike. Changed 2026-09-02 — this field previously carried the bare UUID.adsContentIdis returned prefixed (adc_<UUID>).PATCH /v1/projects/:projectId/ads-content/:idaccepts it either prefixed or bare, per the tolerance rule - pass back what you were given.sourceIdis currently emitted as a bare UUID rather than thecnt_<UUID>the 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
cursoruntilnextCursorisnull. The default order is newest first; useeligible=truefor 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
PATCH /v1/projects/:projectId/ads-content/:id- pin a creative with include or excludeGET /v1/projects/:projectId/top-performers- cross-source ranking on observed metrics- Publish to learn - the feedback loop
GET /v1/ads-metrics- paid performance by scope
GET /v1/projects/:projectId/top-performers
Cross-source ranking of creatives by views, engagement rate, conversions, ROAS, or watch time.
PATCH /v1/projects/:projectId/ads-content/:id
Force-include or force-exclude a creative from ad rotation. The override is one input to the eligibility decision - the facts outrank it.