# 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 — one row per creative, 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 having to page through [`/v1/metrics`](/docs/api/reference/metrics/unified-metrics) and join 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' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl "https://api.layers.com/v1/projects/prj_01HX9Y7K8M2P4RSTUV56789AB/top-performers?metric=roas&window=30d&limit=10" \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..."
    ```
  </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 row of items) {
      console.log(row.rank, row.metricValue, row.sourceType, row.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_01HXC8...",
        "platformPostId": null,
        "adsContentId": "adc_01HXC9...",
        "title": "Before-and-after latte tutorial",
        "thumbnailUrl": "https://media.layers.com/prj_.../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_01HXD2...",
        "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 row per creative [#one-row-per-creative]

A single creative can run on multiple platforms and as multiple ads. This endpoint collapses that down: one row per `ads_content` row, 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
