# POST /v1/posts

The batch form of [`/v1/post`](/docs/post.md). Up to 500 URLs per request, platforms mixed freely, fanned out server-side at the router's own concurrency and rate limit. One slow or dead post never blocks the rest.

## Request

```bash
curl -X POST "https://tikdata.io/v1/posts" \
  -H "Authorization: Bearer thr_live_xxx" \
  -H "content-type: application/json" \
  -d '{"urls":[
        "https://www.tiktok.com/@tiktok/video/7680721699171601694",
        "https://x.com/nasa/status/2095890073031966734",
        "https://www.instagram.com/reel/Dc30nJeRKKz/",
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      ]}'
```

| field | required | notes |
|---|---|---|
| `urls` | yes | array of 1–500 post URLs; non-string or empty entries are dropped before counting |

## Response — 200

```json
{
  "requested": 4,
  "succeeded": 3,
  "failed": 1,
  "results": [
    { "url": "https://www.tiktok.com/@tiktok/video/7680721699171601694",
      "ok": true,
      "post": { "platform": "tiktok", "id": "7680721699171601694", "…": "same shape as /v1/post" } },
    { "url": "https://www.tiktok.com/@tiktok/video/9999999999999999999",
      "ok": false,
      "error": "post not found" }
  ]
}
```

`results` is in the same order as `urls`. Each entry is one of:

| shape | when |
|---|---|
| `{ url, ok: true, post }` | `post` is exactly the [`/v1/post`](/docs/post.md) response |
| `{ url, ok: false, error }` | `error` is one of the strings below |

| error | meaning |
|---|---|
| `unrecognized url` | not a TikTok/Instagram/YouTube/X URL |
| `post not found` | well-formed, but the platform returned no post |
| `upstream 4xx` / `upstream 5xx` | the provider returned that status |
| `upstream unreachable` | timeout or network failure |

## Limits and pacing

| | |
|---|---|
| max URLs | 500 — more returns `400 {"error":"too many urls","hint":"max 500 per request, got 501"}` |
| concurrency | 25 in flight (deployment setting `TIKHUB_CONCURRENCY`) |
| rate | held under the plan's 100 req/s by a sliding window |
| timeout | 300 s for the whole request; at 500 URLs expect roughly 20–40 s |

## Billing

One upstream call per URL that reached the provider, attributed to your key's project. Dead posts are still billed (the provider charges for the 200-with-nothing response). Malformed URLs that never reach the provider cost nothing.

There is no bulk discount underneath: the provider has no working multi-post endpoint, so 500 URLs is 500 billable calls. What you save is 500 round-trips, client-side pacing and per-item error handling.

## Errors

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

## See also

[`POST /v1/videos/bulk_stats`](/docs/videos-bulk-stats.md) — the same fan-out, but takes `{platform, id}` pairs and returns one flat row per video with typed error codes.
