# GET /v1/creators

Returns a paginated list of the **creators** in your organization. A creator is a named entity that groups one or more accounts — the same person on TikTok and on Instagram is one creator with two accounts.

Each creator is returned with its hydrated `accounts[]` and an `overall_stats` aggregate across those accounts. For the flat account list without the creator grouping, use [`GET /v1/bloggers`](../bloggers/list).

## 📋 Overview

| Property | Value |
|----------|-------|
| **Method** | `GET` |
| **Endpoint** | `https://api.influtics.com/v1/creators` |
| **Auth Required** | ✅ Yes |
| **Plan Required** | 💳 Paid — free-tier keys get `402 PAID_PLAN_REQUIRED` |
| **Credits Cost** | 0 credits |

## 🔐 Headers

```
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

## 📝 Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | No | **Single value only.** Case-insensitive substring match on the creator's `display_name`. Not an array — repeating the parameter does not widen the search. |
| `limit` | number | No | Page size. Default `50`, clamped to `1`–`100`. |
| `offset` | number | No | Creators to skip. `≥ 0`, negative values are treated as `0`. |

There is no `sort` parameter: creators are always ordered by `created_at` descending, newest first. To order by reach, fetch the accounts and sort client-side.

### Example Request

```bash
# All creators, first 25
curl -X GET "https://api.influtics.com/v1/creators?limit=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```bash
# Search by name — percent-encode the Cyrillic value
curl -X GET "https://api.influtics.com/v1/creators?name=%D0%9C%D0%B0%D1%80%D0%B8%D1%8F" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```bash
# Second page
curl -X GET "https://api.influtics.com/v1/creators?limit=25&offset=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## 📤 Response

```json
{
  "success": true,
  "data": {
    "creators": [
      {
        "id": "1f0c9a5e-6b2d-4a17-9c3e-7d8b2f4a1c90",
        "display_name": "Kate Comfortt",
        "created_at": "2026-02-14T10:02:00Z",
        "updated_at": "2026-10-19T22:03:12Z",
        "overall_stats": {
          "video_count": 57,
          "number_of_subscribers": 243700,
          "total_views": 25180000,
          "average_views": 441930,
          "median_views": 312000
        },
        "accounts": [
          {
            "id": "1b7c4e90-3a55-4d21-9f08-6c2ad5b19e34",
            "channel_name": "Kate Comfortt",
            "channel_username": "katecomfortt",
            "channel_thumbnail": "https://xhrjtpcqcdqwcdiqrhfw.supabase.co/storage/v1/object/public/avatars/katecomfortt.jpg",
            "url": "https://www.instagram.com/katecomfortt",
            "platform": "instagram",
            "is_tracked": true,
            "tracking_id": "8c3f2b71-0d64-4a92-b1f7-2e5c9a4d6b13",
            "tracked_at": "2026-02-14T09:31:00Z",
            "last_scraped_at": "2026-10-19T22:00:00Z",
            "campaign": "Авито Недвиж",
            "budget_per_video": 12000,
            "daily_videos_count": 3,
            "video_count": 42,
            "number_of_subscribers": 128400,
            "total_views": 18732000,
            "median_views": 312000,
            "avg_views": 446000,
            "created_at": "2026-01-08T11:02:41Z",
            "updated_at": "2026-10-19T22:03:12Z"
          },
          {
            "id": "3d90b2c8-77a1-4e5f-8c26-0b4e9a7d1f55",
            "channel_name": null,
            "channel_username": null,
            "channel_thumbnail": null,
            "url": null,
            "platform": null,
            "is_tracked": false,
            "tracking_id": null,
            "tracked_at": null,
            "last_scraped_at": null,
            "campaign": null,
            "budget_per_video": null,
            "daily_videos_count": 0,
            "video_count": 0,
            "number_of_subscribers": 0,
            "total_views": 0,
            "median_views": 0,
            "avg_views": 0,
            "created_at": null,
            "updated_at": null
          }
        ]
      }
    ],
    "pagination": {
      "total": 12,
      "limit": 25,
      "offset": 0,
      "has_more": false
    }
  },
  "meta": {
    "request_id": "3b7e1c04-9a2f-4d68-b1c5-0e8f2a6d9b34"
  }
}
```

### `creator` fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Internal creator UUID. Use it for [`GET /v1/creators/{id}`](./by-id) and for the `creator_id` filter on [`GET /v1/bloggers`](../bloggers/list). |
| `display_name` | `string` \| `null` | The creator's name. `null` when it has not been set. |
| `created_at` | `string` \| `null` | When the creator was created (ISO 8601). This is the sort key — newest first. |
| `updated_at` | `string` \| `null` | When the creator row was last written (ISO 8601). |
| `overall_stats` | `object` | Aggregate across `accounts[]`. See below. |
| `accounts` | `object[]` | The accounts linked to this creator, each shaped exactly like an [`account` on GET /v1/bloggers](../bloggers/list#account-fields). |

### `overall_stats` fields

| Field | Type | Description |
|-------|------|-------------|
| `video_count` | `number` | **Sum** of `video_count` across the creator's accounts. |
| `number_of_subscribers` | `number` | **Sum** of `number_of_subscribers` across the creator's accounts — not the largest one. |
| `total_views` | `number` | Sum of `total_views` across the creator's accounts. |
| `average_views` | `number` | Video-count-weighted mean of the per-account averages. **Note the name:** this aggregate uses `average_views`, while each account inside `accounts[]` publishes the same metric as `avg_views`. |
| `median_views` | `number` | Median across the creator's accounts, taken over the same reconstructed distribution as `average_views`. |

`overall_stats` reproduces the figures shown on the creators dashboard, including four deliberate behaviours worth knowing before you build totals on top of it:

1. **Subscribers are summed, not maximised.** A creator present on two platforms double-counts reach.
2. **`average_views` is a video-count-weighted mean of per-account averages**, reconstructed from `video_count` copies of each account's average. It is not the mean of the underlying per-video views.
3. **An account with `avg_views === 0` contributes nothing** to `average_views` or `median_views` — it drops out of both denominators entirely. A creator whose accounts are all at zero therefore reports `0` rather than an average over them.
4. **`median_views` is the upper median.** For an even number of values it biases toward the higher one.

### `pagination` fields

Pagination lives inside `data`, **not** in `meta`.

| Field | Type | Description |
|-------|------|-------------|
| `total` | `number` | Total matching creators. Unlike [`GET /v1/bloggers`](../bloggers/list), this counts creators and is not affected by duplicate tracking rows. |
| `limit` | `number` | Page size actually used, after clamping. |
| `offset` | `number` | Offset actually used, after clamping. |
| `has_more` | `boolean` | `true` when `offset + returned creators < total`. |

An **empty page is not an empty result set**: `total` is computed over the whole filtered set independently of `offset`, so paging past the end still reports the real total and `has_more: false`.

## ⚠️ Error Responses

All errors share the standard envelope `{ success: false, error: { code, message, request_id, ... } }`. The table below lists every documented `code` and the conditions that trigger it.

| Status | `code` | Trigger | Additional `error` fields |
|--------|--------|---------|---------------------------|
| `401` | `INVALID_API_KEY` | The `Authorization: Bearer` header is missing, empty, or malformed | — |
| `402` | `PAID_PLAN_REQUIRED` | Free-tier API key attempted the call | `upgrade_url` |
| `403` | `INVALID_OR_INACTIVE_API_KEY` | The API key is well-formed but unknown, revoked, or deactivated | — |
| `503` | `RETRY` | Subscription status could not be verified; retry shortly | — |
| `500` | `INTERNAL_ERROR` | Unexpected database or service failure | — |

A `403` means the key was recognised and can no longer be used — a revoked or deactivated credential — which is a different client-side fix from a `401`, where the `Authorization: Bearer` header never arrived or was malformed. The two are deliberately distinct: a credential that never arrived versus one that arrived and is unusable.

> **This endpoint has no `400` response.** Unlike [`GET /v1/bloggers`](../bloggers/list), it takes no enumerated filter, and the only two numeric parameters are normalized rather than rejected. `limit=abc`, `limit=5000`, `limit=0` and `offset=-5` all return `200` with the value clamped.

## 📌 Notes

### `name` must be percent-encoded, and it is a single value

`name` is matched as a case-insensitive substring of `display_name`. Two things bite callers:

**1. It is one value, not an array.** There is no multi-value form — `?name=a&name=b` does not return the union of `a` and `b`, and no comma-splitting happens either (`name=a,b` searches for the literal text `a,b`). Pass one substring.

**2. Percent-encode non-ASCII values.** Cyrillic must be sent as its UTF-8 percent-encoding or the search silently matches nothing and returns an empty list:

| `name` | Wire value |
|--------|-----------|
| `Мария` | `name=%D0%9C%D0%B0%D1%80%D0%B8%D1%8F` |
| `Авито` | `name=%D0%90%D0%B2%D0%B8%D1%82%D0%BE` |

Sending the raw value may work with some clients and silently fail with others, because whether the raw bytes survive depends on how the client builds the URL. **Encode it and the behaviour is deterministic.** Spaces become `%20` (or `+`), and `name` itself must be part of the query string, not the path.

The `%`, `_` and `\` characters are treated as literal characters in the search rather than as SQL wildcards, so a name containing them — `100%_real` — matches exactly that text.

> Case folding for non-ASCII letters depends on the database collation. ASCII searches are reliably case-insensitive; for Cyrillic, verify against your own data before relying on a mixed-case query.

### `overall_stats.average_views` vs `accounts[].avg_views`

The same metric has **two different names** in the same response, on purpose: the account object publishes `avg_views` (to match the other public account endpoints), while the creator aggregate publishes `average_views` (to match the dashboard). They are not two different numbers — pick the one your code path expects, and do not expect to find `average_views` on an account or `avg_views` on a creator.

### `accounts[]` never drops a linked account

An account linked to a creator but with no statistics row of its own is still returned, zeroed, with its own UUID in `id` — so `platform`, `channel_name`, `is_tracked`, `campaign` and every statistic read `null` or `0`. Such accounts exist in real organizations (a handful at present), so a `platform: null` entry inside `accounts[]` is a real account, not a malformed row.

`accounts[]` comes from the creator↔account link, not from the statistics view, which is why it survives that case.

### `is_tracked` inside `accounts[]` means *actively* tracking

Same rule as the account list: `is_tracked` is `true` only while tracking is running, so a paused account reads `false`, exactly like an account you never tracked. A creator can therefore hold a mix of actively tracked, paused and never-tracked accounts. See the [full explanation](../bloggers/list#is_tracked-means-actively-tracking-not-known-to-you).

### `avg_likes`, `avg_comments` and `avg_reposts` are **not** returned

Neither on the creator nor on any account inside `accounts[]`. The statistics view behind them aggregates views only, so there is no engagement average to compute — the fields are omitted rather than emitted as `0`. For computed engagement averages use [`GET /v1/bloggers/by-username/{username}`](../bloggers/by-username), where `0` means "no video data to average".

### Ordering and clamping are fixed

Creators are always sorted by `created_at` descending. Numeric parameters are normalized silently:

| Sent | Result |
|------|--------|
| `limit=abc` | Falls back to the default `50`. |
| `limit=5000` | Clamped to `100`. |
| `limit=0` or `limit=-1` | Clamped to `1`. |
| `offset=-5` | Treated as `0`. |

- The response is scoped to the calling organization. A creator belonging to another organization is not visible and returns `404 NOT_FOUND` from the by-id endpoint.
- These endpoints only read tables the organization already pays for and call no platform API, so they cost `0` credits.
- Statistics are as of the last platform refresh (`accounts[].last_scraped_at`), not the moment of this request.

---

**Related**: [Creator by ID →](./by-id) | [List Accounts →](../bloggers/list) | [Accounts for a Creator →](../bloggers/list)
