# GET /v1/projects/:projectId/top-performers (/docs/api/reference/metrics/top-performers)



<Endpoint method="GET" path="/v1/projects/{projectId}/top-performers" auth="Bearer" scope="metrics:read" phase="1" />

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`](/docs/api/reference/metrics/unified-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.

<Parameters
  title="Path"
  rows="[
  { name: 'projectId', type: 'string (UUID)', required: true, description: 'Project to rank within.' },
]"
/>

<Parameters
  title="Query"
  rows="[
  { name: 'metric', type: 'string', required: true, description: 'Ranking dimension.', enum: ['views', 'engagement_rate', 'conversions', 'roas', 'watch_time_ms'] },
  { name: 'window', type: 'string', description: 'Window to evaluate over.', enum: ['7d', '30d', '90d'], default: '30d' },
  { name: 'sourceType', type: 'string[]', description: 'Restrict to one or more sources.', enum: ['content_container', 'platform_post', 'manual'] },
  { name: 'platform', type: 'string[]', description: 'Restrict to posts on these platforms. Ignores paid-only sources.', enum: ['instagram', 'tiktok', 'youtube', 'meta_ads', 'tiktok_ads', 'apple_ads'] },
  { name: 'limit', type: 'number', description: 'Number of rows to return, 1–100.', default: '25' },
  { name: 'includeIneligible', type: 'boolean', description: 'Opt out of the eligibility gate. By default the response only includes creatives the scoring pipeline already deemed eligible. Set to `true` to surface every creative regardless of `organic_score`.', default: 'false' },
]"
/>

## Eligibility filter [#eligibility-filter]

By default, results are filtered to creatives the scoring pipeline already deemed **eligible** — `organic_score >= 4.0` OR `override = 'include'`, with `override = 'exclude'` always-ineligible. This matches the rule documented on [`/v1/projects/:projectId/ads-content`](/docs/api/reference/metrics/ads-content) and is the same gate the platform applies before promoting a creative.

This applies to both the organic and paid paths:

* **Organic** (`metric=views`, `engagement_rate`, `watch_time_ms`): UGC platform posts with `organic_score < 4.0` and no `include` override are dropped before ranking.
* **Paid** (`metric=conversions`, `roas`): mapped creatives with `organic_score < 4.0` and no `include` override are dropped. Unmapped paid rows (no `ads_content_id`) pass through — we have no scoring signal to gate on.

### Opting out [#opting-out]

For forensic exploration of low-score content, set `?includeIneligible=true` to bypass the gate and surface every creative regardless of `organic_score`. The metric value reflects exactly what the scoring pipeline saw; nothing is recomputed.

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    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_..."
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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);
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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"]
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="OK - already sorted by the requested metric, descending.">
  ```json
  {
    "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_01HXD1...",
        "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
        }
      }
    ]
  }
  ```
</Response>

<Response status="422" description="Validation failed - metric missing, unknown enum, or `metric=roas` paired with an organic-only source mix.">
  ```json
  { "error": { "code": "VALIDATION", "message": "metric=roas requires paid data. Filter sourceType to content_container or manual, or pick an organic metric." } }
  ```
</Response>

## One item per creative [#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`](/docs/api/reference/metrics/unified-metrics) 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 [#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. Ranking by an organic metric includes everything with platform metrics.

## Notes [#notes]

* `thumbnailUrl` is permanent for UGC and generated content. Do not cache ad-platform thumbnail URLs from other endpoints - those expire.
* `metricValue` is the same number as the field inside `organic` or `paid` - surfaced at the top level so your UI can render "5.2x" without guessing which subtree it came from.
* Ties break on `adsContentId` descending, which stabilizes the order between requests.
* Windows `7d`/`30d`/`90d` end at query time. There is no offset or custom range; use unified metrics for that.

## See also [#see-also]

* [`GET /v1/projects/:projectId/ads-content`](/docs/api/reference/metrics/ads-content) - scored creatives with organic\_score and eligibility
* [`GET /v1/metrics`](/docs/api/reference/metrics/unified-metrics) - raw time series by scope
* [`GET /v1/ads-metrics`](/docs/api/reference/ads/ads-metrics) - paid metrics only
