Docs
APIAPI referenceMetrics

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

Creative inventory - generated content, UGC and manual uploads with provenance, override state and explicit eligibility for ad promotion.

View as Markdown
GET/v1/projects/{projectId}/ads-content
Phase 1stable
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.

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
    numberoptional
    Deprecated. Accepted for compatibility and ignored; the creative score no longer filters this list. 0 to 10.
  • override
    stringoptional
    Filter by override state.
    One of: include, exclude, none
  • eligible
    booleanoptional
    true keeps items Layers can promote right now; false keeps the refused items. See eligibility.reason for the refusal rules.
  • sort
    stringoptionaldefault: created_at_desc
    Sort 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_desc
  • cursor
    stringoptional
    Opaque pagination cursor from nextCursor. A malformed cursor, or one issued before the sort changed, is rejected with 422; re-read from the first page.
  • limit
    numberoptionaldefault: 50
    Page 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

200OK
{
  "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 manual override is "exclude".
  • safety_failed - the creative failed safety review. This is not clearable with override: "include".
  • flagged - a moderator flagged the creative. Also not clearable with override: "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.

FieldValue nowReplacement
organicScorealways 0top-performers for observed metrics; eligibility for the promote decision
organicPerformancealways nulltop-performers (organic.views, organic.engagementRate)
scoredAtlast write of the retired score, or nullnone; not a signal
scoringVersionversion of the retired formula, or absentnone; not a signal
minScore (query)accepted, ignoredeligible, override
sort=score_desc, sort=score_asc (query)resolve to created_at_desccreated_at_desc
sort=scored_at_desc (query)still orders by scoredAt until removalcreated_at_desc

Notes

  • platformPostId is the opaque platform-post identity, pp_<UUID> (see ID formats). Pass it back verbatim to GET /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.
  • adsContentId is returned prefixed (adc_<UUID>). PATCH /v1/projects/:projectId/ads-content/:id accepts it either prefixed or bare, per the tolerance rule - pass back what you were given.
  • sourceId is currently emitted as a bare UUID rather than the cnt_<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 cursor until nextCursor is null. The default order is newest first; use eligible=true for 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

On this page