# GET /v1/bloggers

Returns a paginated list of the **accounts** (blogger records) in your organization, with their aggregated video statistics. Supports filtering, exact-campaign matching and sorting.

An *account* is one tracked (or previously tracked) social account. To see accounts grouped under a named *creator* entity, use [`GET /v1/creators`](../creators/list) instead — every account here may belong to zero, one or several creators.

To start tracking a new account, use [`POST /v1/bloggers/track`](./track) first and poll [`GET /v1/bloggers/jobs/{job_id}`](./jobs) until `succeeded`.

## 📋 Overview

| Property | Value |
|----------|-------|
| **Method** | `GET` |
| **Endpoint** | `https://api.influtics.com/v1/bloggers` |
| **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 |
|-----------|------|----------|-------------|
| `tracked` | boolean | No | `true` = actively tracking only. `false` = **not** actively tracking (paused *and* never-tracked). Omit for no filter. Also accepts `1` / `0`. |
| `creator_id` | string (uuid) | No | Return only accounts linked to this creator. See [`GET /v1/creators/{id}`](../creators/by-id) for how to obtain one. |
| `platform` | string \| string[] | No | Filter by platform. Comma-separated (`?platform=tiktok,instagram`) or repeated (`?platform=tiktok&platform=instagram`). Unknown values are rejected with `400 VALIDATION_ERROR` — see the error table below. |
| `campaign` | string \| string[] | No | **Exact** campaign name match. Comma-separated or repeated. See **⚠️ `campaign` is an exact match, not a substring** under Notes below. |
| `sort` | string | No | Sort key (default `tracked_at`). One of `avg_views`, `median_views`, `number_of_subscribers`, `total_views`, `video_count`, `tracked_at`. |
| `sort_order` | string | No | `asc` or `desc` (default `desc`). Anything other than `asc` sorts descending — see **Sorting, clamping and defaults are silent** under Notes below. |
| `limit` | number | No | Page size. Default `50`, clamped to `1`–`100`. |
| `offset` | number | No | Rows to skip. `≥ 0`, negative values are treated as `0`. |

`tracked` and `creator_id` **intersect**: `?tracked=true&creator_id=<uuid>` returns only accounts that are actively tracked *and* linked to that creator, never the union.

### Example Request

```bash
# Actively-tracked TikTok accounts, biggest audience first
curl -X GET "https://api.influtics.com/v1/bloggers?tracked=true&platform=tiktok&sort=number_of_subscribers&sort_order=desc&limit=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```bash
# Every account linked to one creator (campaign names must be exact and encoded)
curl -X GET "https://api.influtics.com/v1/bloggers?creator_id=1f0c9a5e-6b2d-4a17-9c3e-7d8b2f4a1c90&campaign=%D0%90%D0%B2%D0%B8%D1%82%D0%BE%20%D0%9D%D0%B5%D0%B4%D0%B2%D0%B8%D0%B6" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

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

## 📤 Response

```json
{
  "success": true,
  "data": {
    "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": "5e2d8a13-7c40-4b96-8f51-2d9a6e3b7c02",
        "channel_name": "Блогер без статистики",
        "channel_username": null,
        "channel_thumbnail": null,
        "url": null,
        "platform": "vk",
        "is_tracked": false,
        "tracking_id": "1a9d4e77-6b02-4c58-8f31-7d0e2b4a6c95",
        "tracked_at": "2026-06-02T14:12:09Z",
        "last_scraped_at": "2026-09-28T03:10:44Z",
        "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": "2026-05-30T08:44:00Z",
        "updated_at": "2026-09-28T03:10:44Z"
      }
    ],
    "pagination": {
      "total": 387,
      "limit": 50,
      "offset": 0,
      "has_more": true
    }
  },
  "meta": {
    "request_id": "9f7e8d4c-1a2b-4c3d-8e5f-6a7b8c9d0e1f"
  }
}
```

### `account` fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` \| `null` | Internal account UUID. Use it for [`GET /v1/bloggers/{id}`](./by-id). `null` only for an account linked to a creator that has no statistics row at all. |
| `channel_name` | `string` \| `null` | Creator's display name. |
| `channel_username` | `string` \| `null` | Username without `@`. `null` when unknown — this is common for accounts imported from video URLs rather than added by username. |
| `channel_thumbnail` | `string` \| `null` | Avatar URL. |
| `url` | `string` \| `null` | Canonical profile URL. |
| `platform` | `string` \| `null` | One of `tiktok`, `instagram`, `youtube`, `telegram`, `vk`, `ok`, `dzen`, `facebook`, `threads`, `pinterest`. |
| `is_tracked` | `boolean` | **Actively** tracking. A paused account reads `false` — see **Notes** below. |
| `tracking_id` | `string` \| `null` | UUID of the `tracked_accounts` row. |
| `tracked_at` | `string` \| `null` | When tracking started (ISO 8601). This is the default `sort` key. |
| `last_scraped_at` | `string` \| `null` | When platform stats were last refreshed — **not** when this request was made. |
| `campaign` | `string` \| `null` | Exact campaign name, or `null` when the account is not in a campaign. |
| `budget_per_video` | `number` \| `null` | Per-video budget configured for the account, or `null` if none. |
| `daily_videos_count` | `number` \| `null` | Videos per day expected by the tracking schedule. |
| `video_count` | `number` \| `null` | Number of videos with statistics in the org. |
| `number_of_subscribers` | `number` \| `null` | Subscriber / follower count at last refresh. |
| `total_views` | `number` \| `null` | Sum of views across the account's videos. |
| `median_views` | `number` \| `null` | Median views across the account's videos. |
| `avg_views` | `number` \| `null` | **Mean** views across the account's videos. Published as `avg_views` — the underlying column is named `average_views`. See **Notes** below. |
| `created_at` | `string` \| `null` | When the account row was created (ISO 8601). |
| `updated_at` | `string` \| `null` | When the account row was last written (ISO 8601). |

### `pagination` fields

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

| Field | Type | Description |
|-------|------|-------------|
| `total` | `number` | Total matching **view rows**, not necessarily distinct accounts — see **Notes** below. |
| `limit` | `number` | Page size actually used, after clamping. Echo it back rather than assuming your requested value. |
| `offset` | `number` | Offset actually used, after clamping. |
| `has_more` | `boolean` | `true` when `offset + returned rows < 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 |
|--------|--------|---------|---------------------------|
| `400` | `VALIDATION_ERROR` | `sort` is not one of the six supported keys | `field: "sort"` |
| `400` | `VALIDATION_ERROR` | `platform` contains a value outside the supported platform list | `field: "platform"` |
| `400` | `VALIDATION_ERROR` | `tracked` is not `true`, `false`, `1` or `0` | `field: "tracked"` |
| `400` | `VALIDATION_ERROR` | `campaign` is present but contains no usable name (e.g. `campaign=,`) | `field: "campaign"` |
| `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.

An unknown id in a by-id path, or a path that matches no route at all, returns `404 NOT_FOUND`.

## 📌 Notes

### `is_tracked` means *actively* tracking, not "known to you"

This is the single most misread field on this endpoint. `is_tracked` is `true` only while tracking is running. An account you started tracking and later **paused** reads `is_tracked: false` — the same value as an account you never tracked. The response never distinguishes the two.

So:

- `?tracked=false` returns **paused and never-tracked accounts together**, not just the paused ones.
- An account can appear in an unfiltered list with `is_tracked: false` while its videos are still being fetched.
- Filtering by `is_tracked` to build a "currently tracked" roster is correct; treating it as "is in our database" will over-report.

### `avg_views`, not `average_views`

The published field name is `avg_views`. It is the account's **mean** views per video (`ROUND(AVG(views))`). `median_views` is the 50th percentile, and `total_views` the sum.

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

These three fields are deliberately absent from this endpoint. The statistics view behind it aggregates views only — it has no columns that could produce an engagement average — so there is no correct value to send. They are omitted rather than emitted as `0`.

If you need engagement averages, use [`GET /v1/bloggers/by-username/{username}`](./by-username), which computes `avg_likes`, `avg_comments` and `avg_reposts` through a different query. There, `0` means "no video data to average". Treat the absence here as "not computable", and the `0` there as "genuinely zero" — do not unify them.

`avg_views` itself *is* present on both endpoints with the same meaning, so the two responses can be compared field-for-field apart from those three.

### ⚠️ `campaign` is an exact match, not a substring

`?campaign=Авито` does **not** match `Авито Недвиж`. The value is compared for equality against the stored campaign name. If you need a prefix or substring match, fetch and filter client-side.

Two consequences that look like bugs but are not:

1. **Filtering by campaign legitimately returns a very small result set.** `campaign` is set on only about **3% of accounts** and is `null` on the other **97%**. The rate varies by organization — a campaign-heavy organization can be far higher — but globally expect to filter away nearly everything. A handful of results, or none, is the filter working, not a broken endpoint.
2. **There is no way to ask for "accounts with no campaign."** An unset campaign is stored as `null`, and no filter expresses `null`. Omitting `campaign` returns everyone; supplying it returns only the named ones.

Campaign names are arbitrary user text and may be Cyrillic. **Percent-encode them** — `Авито Недвиж` becomes
`%D0%90%D0%B2%D0%B8%D1%82%D0%BE%20%D0%9D%D0%B5%D0%B4%D0%B2%D0%B8%D0%B6`.

### `total` counts view rows, so an account can be counted twice

The statistics view groups by tracking row, so a blogger with two tracking rows produces two rows. `pagination.total` counts rows, and so can over-report the number of distinct accounts — about four such duplicate pairs exist at present. The same account can also appear twice in `accounts[]`, and can straddle a page boundary.

Deduplicate on `id` before counting or paging if your workflow depends on exact account counts. `has_more` is computed against the same row count, so it stays internally consistent.

### Sorting, clamping and defaults are silent

Three query parameters are normalized rather than rejected:

| Sent | Result |
|------|--------|
| `limit=abc` | Falls back to the default `50` — no error. |
| `limit=5000` | Clamped to `100`. |
| `limit=0` or `limit=-1` | Clamped to `1`. |
| `offset=-5` | Treated as `0`. |
| `sort_order=downwards` | Treated as `desc` — any value other than `asc` sorts descending, with no error. |

Only `sort`, `platform`, `tracked` and an empty `campaign` produce a `400`. Check the echoed `pagination.limit` and `pagination.offset` if the exact page size matters to you.

`nullsFirst: false` is applied to every sort, so accounts with no value for the sort key (for example `avg_views` on an account with no videos) always appear **last**, in both directions.

### Other characteristics

- The response is scoped to the calling organization. An account 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, which is what `last_scraped_at` reports — not the moment of this request.

---

**Related**: [Account by ID →](./by-id) | [Creator Lookup by Username →](./by-username) | [List Creators →](../creators/list) | [Track an Account →](./track)
