GET /v1/projects/:projectId/top-performers
Cross-source ranking of creatives by views, engagement rate, conversions, ROAS, or watch time.
/v1/projects/{projectId}/top-performers- Auth
- Bearer
- Scope
- metrics:read
Returns the top-N creatives in a project, ranked by a single metric across an explicit window. Generated containers, UGC posts, and manual uploads are pooled together, with both the organic signal (views, engagement) and the paid signal (conversions, ROAS) attached. This is the shortcut for "what should I promote right now?" without paging through /v1/metrics and joining across sources yourself.
The list is already sorted by the metric you asked for. No client-side sort needed. No per-creative metric roll-ups needed. Pre-ranking is why this endpoint exists as a separate call rather than a flag on unified metrics.
projectIdstring (UUID)requiredProject to rank within.
metricstringrequiredRanking dimension.One of:views,engagement_rate,conversions,roas,watch_time_mswindowstringoptionaldefault: 30dWindow to evaluate over.One of:7d,30d,90dsourceTypestring[]optionalRestrict to one or more sources.One of:content_container,platform_post,manualplatformstring[]optionalRestrict to posts on these platforms. Ignores paid-only sources.One of:instagram,tiktok,youtube,meta_ads,tiktok_ads,apple_adslimitnumberoptionaldefault: 25Number of rows to return, 1–100.includeIneligiblebooleanoptionaldefault: falseOpt out of the eligibility gate. By default a post is dropped when Layers could not promote its creative right now, or when it has no creative record at all (nothing to promote). Set to `true` to surface every post regardless.
Eligibility filter
By default, results are filtered to eligible creatives, using the same predicate as /v1/projects/:projectId/ads-content and the same gate the platform applies before promoting a creative. A post is dropped when its creative is:
- excluded by override (
override = 'exclude'), - safety-failed (
eligibility.reason: "safety_failed"), - flagged by a moderator (
eligibility.reason: "flagged"), - unable to launch on its TikTok Spark authorization (
eligibility.reason: "spark_authorization_not_live"or"spark_window_unknown"), or - missing the media behind it (
eligibility.reason: "source_unavailable"),
or when it has no creative record in this project at all: there is nothing to promote, so it is not ranked as eligible. The deprecated creative score is not consulted; the ranking itself is on observed metrics (views, engagement rate) and paid results (conversions, ROAS), which are unaffected.
This applies to both the organic and paid paths:
- Organic (
metric=views,engagement_rate): platform posts whose creative is refused by any of the rules above are dropped before ranking. The ranking pages past them, so a refused post never costs you a slot — the list still returns your toplimitpromotable posts. - Paid (
metric=conversions,roas): mapped creatives refused by any of the rules above are dropped. Unmapped paid rows (noadsContentId) pass through — there is no creative to gate on. Paid rows come only from campaigns imported into the project.
Opting out
To see the dropped posts too, including posts with no creative record, set ?includeIneligible=true to bypass the gate. The metric values are the same observed numbers; nothing is recomputed.
Example request
curl "https://api.layers.com/v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/top-performers?metric=roas&window=30d&limit=10" \
-H "Authorization: Bearer lp_..."const res = await fetch(
`https://api.layers.com/v1/projects/${projectId}/top-performers?metric=roas&window=30d&limit=10`,
{ headers: { Authorization: `Bearer ${apiKey}` } },
);
const { items } = await res.json();
// Already sorted - just walk it.
for (const item of items) {
console.log(item.rank, item.metricValue, item.sourceType, item.sourceId);
}import httpx
r = httpx.get(
f"https://api.layers.com/v1/projects/{project_id}/top-performers",
params={"metric": "roas", "window": "30d", "limit": 10},
headers={"Authorization": f"Bearer {api_key}"},
)
items = r.json()["items"]Response
{
"metric": "roas",
"window": "30d",
"items": [
{
"rank": 1,
"sourceType": "content_container",
"sourceId": "cnt_7d18b9a1...",
"platformPostId": null,
"adsContentId": "adc_6f5d4c3b...",
"title": "Before-and-after latte tutorial",
"thumbnailUrl": "https://media.layers.com/prj_254a4ce1.../thumb_01HXC8.jpg",
"metricValue": 5.2,
"organic": {
"views": 148200,
"engagementRate": 0.061,
"platforms": ["instagram", "tiktok"]
},
"paid": {
"spend": 412.00,
"conversions": 88,
"roas": 5.2,
"cpa": 4.68
}
},
{
"rank": 2,
"sourceType": "platform_post",
"sourceId": null,
"platformPostId": "pp_a4f1c9d2-3e5b-4a76-8c19-2d0f7b6e5a34",
"adsContentId": "adc_6f5d4c3b...",
"title": "Creator pour-over UGC",
"thumbnailUrl": "https://media.meetsift.com/.../cover.jpg",
"metricValue": 4.7,
"organic": {
"views": 88100,
"engagementRate": 0.079,
"platforms": ["tiktok"]
},
"paid": {
"spend": 210.00,
"conversions": 41,
"roas": 4.7,
"cpa": 5.12
}
}
]
}{ "error": { "code": "VALIDATION", "message": "metric=roas requires paid data. Filter sourceType to content_container or manual, or pick an organic metric." } }One item per creative
A single creative can run on multiple platforms and as multiple ads. This endpoint collapses that down to one item per creative, with organic and paid metrics summed across surfaces in the window. Use /v1/metrics?scope=platform_post if you need per-platform breakouts.
The window parameter is explicit - there is no "all time." Windows are evaluated at query time against platform and ad sync data, so results can shift between calls if a sync completes mid-flight.
Metric availability by source
| Source | views | engagement_rate | watch_time_ms | conversions | roas |
|---|---|---|---|---|---|
content_container | Yes | Yes | Yes | Only if promoted | Only if promoted |
platform_post | Yes | Yes | Yes (TikTok/Reels) | Only if promoted | Only if promoted |
manual | No | No | No | Only if promoted | Only if promoted |
Ranking by a paid metric implicitly filters out creatives that were never promoted, and only counts spend from campaigns imported into the project. Ranking by an organic metric includes everything with platform metrics.
Notes
thumbnailUrlis permanent for UGC and generated content. Do not cache ad-platform thumbnail URLs from other endpoints - those expire.metricValueis the same number as the field insideorganicorpaid- surfaced at the top level so your UI can render "5.2x" without guessing which subtree it came from.- Ties break on
adsContentIddescending, which stabilizes the order between requests. - Windows
7d/30d/90dend at query time. There is no offset or custom range; use unified metrics for that.
See also
GET /v1/projects/:projectId/ads-content- creative inventory with override state and eligibilityGET /v1/metrics- raw time series by scopeGET /v1/ads-metrics- paid metrics only
GET /v1/projects/:projectId/metrics
Unified organic metrics across post, account, project, and layer scopes. One query shape, one response shape.
GET /v1/projects/:projectId/ads-content
Creative inventory - generated content, UGC and manual uploads with provenance, override state and explicit eligibility for ad promotion.