# POST /v1/videos/bulk_stats

Up to 100 videos per call, platforms mixed freely, identified by id or URL, flattened to one row per video with every engagement stat the platform exposes. One entry out per entry in, in order, each with its own `ok`, so a single dead id never fails the batch.

## Request

```bash
curl -X POST "https://tikdata.io/v1/videos/bulk_stats" \
  -H "Authorization: Bearer thr_live_xxx" \
  -H "content-type: application/json" \
  -d '{"videos":[
        {"platform":"tiktok",    "id":  "7680721699171601694"},
        {"platform":"instagram", "id":  "Dc30nJeRKKz"},
        {"platform":"instagram", "url": "https://www.instagram.com/reel/Dc30nJeRKKz/"},
        {"platform":"x",         "id":  "2095890073031966734"},
        {"platform":"youtube",   "id":  "dQw4w9WgXcQ"}
      ]}'
```

| field | required | notes |
|---|---|---|
| `videos` | yes | array of 1–100 objects |
| `videos[].platform` | one of `platform` or `url` | `tiktok` \| `instagram` \| `youtube` \| `x`; if omitted, detected from `url` |
| `videos[].id` | one of `id` or `url` | TikTok aweme id, Instagram shortcode, YouTube video id, X tweet id |
| `videos[].url` | one of `id` or `url` | any post URL; the id is extracted from it |

Instagram accepts a bare shortcode with no URL — the provider's endpoint takes either.

## Response — 200

```json
{
  "count": 5,
  "videos": [
    {
      "id": "7680721699171601694",
      "platform": "tiktok",
      "ok": true,
      "views": 202473,
      "likes": 8267,
      "comments": 1261,
      "shares": 631,
      "saves": 922,
      "reposts": 0,
      "quotes": 0,
      "downloads": 40,
      "created_at": "2026-09-02T00:03:08Z",
      "author": "tiktok",
      "cover_url": "https://p16-common-sign.tiktokcdn-us.com/…"
    },
    {
      "id": "Dc30nJeRKKz",
      "platform": "instagram",
      "ok": true,
      "views": 111591938,
      "likes": 871528,
      "comments": 19968,
      "shares": 166102,
      "saves": 28346,
      "reposts": 23535,
      "quotes": 0,
      "downloads": 0,
      "created_at": "2026-09-04T16:32:46Z",
      "author": "instagram",
      "cover_url": "https://scontent-….cdninstagram.com/…"
    },
    { "id": "9999999999999999999", "platform": "tiktok", "ok": false, "error": "invalid_id" }
  ]
}
```

| field | type | notes |
|---|---|---|
| `count` | integer | `videos.length` — always equals the input length |
| `videos[].id` | string | the id used (extracted from `url` when only that was given) |
| `videos[].platform` | string | |
| `videos[].ok` | boolean | |
| `views` `likes` `comments` `shares` `saves` `reposts` `quotes` `downloads` | integer | see the platform table in [`/v1/post`](/docs/post.md); `0` where a platform has no such number |
| `created_at` | ISO 8601 (seconds) \| null | |
| `author` | string | bare handle |
| `cover_url` | string | |
| `error` | string | only on `ok: false` |

## Error codes

| code | meaning | costs a call? |
|---|---|---|
| `invalid_id` | malformed id — TikTok ids encode a creation timestamp, so this is decided offline | no |
| `not_found` | well-formed reference, but the platform returned no post | yes |
| `unsupported_platform` | `platform` given but not one of the four | no |
| `bad_input` | no usable `id` or `url`, or no platform determinable | no |
| `upstream_error` | provider returned a non-2xx we could not classify | no (errors are free upstream) |
| `upstream_unreachable` | timeout or network failure | no |

There is deliberately **no `video_private`**. The provider omits the post object identically whether a video was deleted, made private, or never existed. The signal exists at TikTok itself (`statusMsg: "status_self_see"` vs `"item doesn't exist"`) but is stripped before it reaches us — verified 2026-09-08 across seven endpoints. Reporting `video_private` would be inventing information your tracker might act on.

## What this saves, and what it does not

It saves 100 round-trips, client-side pacing and per-item error handling — the fan-out happens here, under the router's rate gate. It does **not** save money: the provider has no working bulk video endpoint to fan into (its four `fetch_multi_video*` routes are 404, or accept the schema and then 400 — re-verified 2026-09-07), so 100 videos is 100 billable calls either way.

## Errors

| status | body |
|---|---|
| 400 | `{"error":"body must be JSON","hint":"{\"videos\":[{\"platform\":\"tiktok\",\"id\":\"768...\"}]}"}` |
| 400 | `{"error":"videos[] is required"}` |
| 400 | `{"error":"too many videos","hint":"max 100 per request, got N"}` |
| 401 | see [authentication](/docs/authentication.md) |
