# Comments in bulk (/docs/recipes/comments-in-bulk) Get the comments under up to 25 post URLs from eight platforms in one call, then poll the same list for new comments only. You will send a list of post URLs and get each post's comments back in one call, then turn the same call into a poll that returns only the comments posted since your last run. The posts can come from Instagram, TikTok, YouTube, Facebook, Reddit, X, Threads and LinkedIn, mixed in one list. **Cost per run:** each post costs what its own comment endpoint charges per page: 1 credit on TikTok, YouTube, Facebook, X and Threads, 5 on Instagram, LinkedIn and Reddit. The call below reads 20 comments from three posts and costs 7 credits. A link that is not a post, and a post with no comments, cost nothing. ## How do I get the comments from many posts at once? Send the post URLs to `POST /v1/prism/post-comments`: ```bash title="cURL" curl -X POST "https://www.socialcrawl.dev/v1/prism/post-comments" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"per_post": 20, "items": ["https://www.tiktok.com/@mrbeast/video/7654638524729216287", "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://www.reddit.com/r/socialmedia/comments/1qca2ki/social_listening_tool/"]}' ``` Each URL gets one row in `data.results`, in the order you sent them. `per_post` sets how many comments are read per post, 20 by default and up to 200. You can send up to 25 URLs per call. ## What you get back One row per URL. An `ok` row holds the post's comments in `data.items`, in the same shape that platform's own comment endpoint returns. A URL that is not a post comes back `unsupported` at 0 credits, and the other rows are not affected. ```json title="Response" { "success": true, "endpoint": "/v1/prism/post-comments", "data": { "results": [ { "index": 0, "custom_id": "campaign-7", "target": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "status": "ok", "data": { "platform": "youtube", "endpoint": "/v1/youtube/video/comments", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "items": [ { "comment": { "id": "UgxwNNVdFXL7B03gU3R4AaABAg", "post_id": "dQw4w9WgXcQ", "text": "did anyone else come from the feodoric video", "author": { "username": "@Ky4tsuk1_Bakuh0" }, "engagement": { "likes": 0, "replies": 0 }, "published_at": "2026-10-25T22:50:22.000Z" } } ], "truncated": true, "next_cursor": null, "pages": 1 }, "cost": 1 }, { "index": 1, "target": "https://example.com/not-a-post", "status": "unsupported", "error": { "type": "UNSUPPORTED_URL", "message": "Not a post URL on a platform with a comment endpoint (instagram, tiktok, youtube, facebook, reddit, twitter, threads, linkedin). Pass the link to one post, video or tweet." }, "cost": 0 } ], "summary": { "total": 2, "ok": 1, "not_found": 0, "unsupported": 1, "error": 0, "deferred": 0, "coverage": 0.5, "credits_charged": 1, "credits_refunded": 0 } }, "credits_used": 1 } ``` That response is trimmed to one comment and the main comment fields. Each row tells you: - **`status`**: `ok`, `not_found` (the post cannot be found), `unsupported` (not a post URL on one of the eight platforms), `error` or `deferred`. Only `ok` rows cost credits. - **`cost`**: the credits that row cost. `credits_used` on the response is the sum, and every page that was not read is refunded. - **`truncated`** and **`next_cursor`**: `truncated` is `true` when the post has more comments than the row holds. Send `next_cursor` as `cursor` to `data.endpoint` to read the next page. It is `null` when the post has no more comments, and also when the read was cut at `per_post`; in that case raise `per_post` to read further. - **`custom_id`**: any id you attach to an item (up to 128 characters) comes back on its row, so you can match rows to your own records. ## How do I get only the new comments? Add `since`. Each row then keeps only the comments published at or after it, and a post with no new comments costs nothing. This program prints the comments posted in the last 24 hours under three posts: ```typescript title="TypeScript" // poll-comments.ts // Prints the comments posted in the last 24 hours under a list of posts. // Run with: SOCIALCRAWL_KEY=sc_... npx tsx poll-comments.ts const KEY = process.env.SOCIALCRAWL_KEY; if (!KEY) throw new Error("Set SOCIALCRAWL_KEY"); const BASE = "https://www.socialcrawl.dev"; const posts = [ { url: "https://www.tiktok.com/@mrbeast/video/7654638524729216287", custom_id: "mrbeast-tiktok" }, { url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", custom_id: "rickroll-youtube" }, { url: "https://www.instagram.com/p/CnpPou9hWqq/", custom_id: "nasa-instagram" }, ]; const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); interface Row { index: number; custom_id?: string; status: string; cost: number; data?: { items: { comment: { text: string; author: { username: string | null } } }[]; }; } async function newComments(items: typeof posts) { const res = await fetch(`${BASE}/v1/prism/post-comments`, { method: "POST", headers: { "x-api-key": KEY!, "Content-Type": "application/json" }, body: JSON.stringify({ items, since, per_post: 50 }), }); const json = (await res.json()) as { success: boolean; data?: { results: Row[] }; credits_used: number; error?: { message: string }; }; if (!json.success) throw new Error(json.error?.message ?? `HTTP ${res.status}`); return { rows: json.data!.results, credits: json.credits_used }; } const first = await newComments(posts); // A post not started within the 45-second limit comes back "deferred" and // costs nothing. Send those posts again. const deferred = first.rows.filter((r) => r.status === "deferred").map((r) => posts[r.index]!); const second = deferred.length > 0 ? await newComments(deferred) : { rows: [], credits: 0 }; for (const row of [...first.rows.filter((r) => r.status !== "deferred"), ...second.rows]) { if (row.status !== "ok") { console.log(`${row.custom_id}: ${row.status}`); continue; } console.log(`${row.custom_id}: ${row.data!.items.length} new comments, ${row.cost} credits`); for (const { comment } of row.data!.items.slice(0, 3)) { console.log(` ${comment.author.username}: ${comment.text.slice(0, 80)}`); } } console.log(`Charged ${first.credits + second.credits} credits since ${since}`); ``` Instagram, YouTube, X and LinkedIn posts are read newest first, so the read stops at the first comment older than `since`. TikTok, Facebook, Reddit and Threads posts are read up to `per_post` comments and then filtered, so on a busy post raise `per_post` to make sure nothing new is missed. Run the program on a schedule and pass the time of your last run as `since`. ## What to change - **The list**: up to 25 post URLs per call. For more, split the list, or run up to 1,000 posts in the background with `POST /v1/prism/jobs` and that platform's comment endpoint (see [Async requests](/docs/async.md#run-many-calls-to-one-get-as-a-job)). - **`per_post`**: how deep each post is read, from 1 to 200. Deeper reads cost more pages on busy posts. - **Retries**: send an `Idempotency-Key` header, and a retry of a call that timed out returns the first answer at 0 credits instead of running again. See [Idempotency](/docs/idempotency.md). - **Streaming**: send `Accept: text/event-stream` to get each post's row as soon as it is read. See [Streaming (SSE)](/docs/streaming.md). ## What to know about Reddit A Reddit row holds the comments of one read of the thread. `GET /v1/reddit/post/comments` on a single thread also expands deeper reply trees, so use it when you need the full tree of one thread. A Reddit thread can take 20 to 30 seconds to read, so a list with many Reddit posts can come back partly `deferred`. Send those posts again, or run the list as a background job. ## Related - [Batch endpoints](/docs/batch.md#comments-under-a-list-of-posts): Every batch endpoint, row statuses, billing and retries. - [Async requests](/docs/async.md): Run up to 1,000 calls to one endpoint in the background. - [Sentiment analysis](/docs/recipes/sentiment-analysis.md): Feed social comments into a sentiment pipeline. - [Pagination](/docs/pagination.md): Read further into one post with next_cursor.