curl --request POST \
--url https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Team-SID: <api-key>' \
--data '
{
"post": "<string>",
"linkedin_account_sid": "<string>",
"idempotency_key": "<string>",
"page_size": 50,
"cursor": "<string>"
}
'import requests
url = "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments"
payload = {
"post": "<string>",
"linkedin_account_sid": "<string>",
"idempotency_key": "<string>",
"page_size": 50,
"cursor": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Team-SID": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
Authorization: 'Bearer <token>',
'Team-SID': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
post: '<string>',
linkedin_account_sid: '<string>',
idempotency_key: '<string>',
page_size: 50,
cursor: '<string>'
})
};
fetch('https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'post' => '<string>',
'linkedin_account_sid' => '<string>',
'idempotency_key' => '<string>',
'page_size' => 50,
'cursor' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"Team-SID: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments"
payload := strings.NewReader("{\n \"post\": \"<string>\",\n \"linkedin_account_sid\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"page_size\": 50,\n \"cursor\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Team-SID", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments")
.header("Authorization", "Bearer <token>")
.header("Team-SID", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"post\": \"<string>\",\n \"linkedin_account_sid\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"page_size\": 50,\n \"cursor\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Team-SID"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"post\": \"<string>\",\n \"linkedin_account_sid\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"page_size\": 50,\n \"cursor\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"operation": "action",
"action": "<string>",
"item": "<unknown>",
"result": {
"rows": [
{
"commenter_ln_member_id": "<string>",
"commenter_ln_id": "<string>",
"commenter_nickname": "<string>",
"commenter_full_name": "<string>",
"commenter_headline": "<string>",
"commenter_picture_url": "<string>",
"comment_ln_id": "<string>",
"content": "<string>",
"posted_at": "<string>",
"reactions_count": 123,
"comment_permalink": "<string>",
"replies_count": 123,
"is_pinned": true,
"is_edited": true,
"is_own": true
}
],
"paging": {
"page_size": 123,
"next_cursor": "<string>",
"has_more": true,
"total": 123
},
"data_request": {},
"post": {
"post_ln_id": "<string>"
},
"skipped_invalid_rows": 123
},
"meta": {
"trace_id": "<string>",
"span_id": "<string>",
"timestamp": "<string>",
"duration_ms": 1,
"team_sid": "<string>",
"actor_type": "<string>"
},
"pacing": {
"linkedin_account_sid": "<string>",
"limit_type": "<string>",
"next_call_after": "<string>",
"remaining_today": 1
}
}{
"success": false,
"error": {
"code": "validation_failed",
"message": "<string>",
"recoverable": true,
"suggestion": "<string>",
"field_errors": {},
"blockers": [
{
"type": "<string>",
"severity": "hard",
"description": "<string>",
"entity_sid": "<string>",
"resolution": "<string>",
"resolution_hint": "<string>",
"count": 123
}
],
"context": {}
},
"meta": {
"trace_id": "<string>",
"span_id": "<string>",
"timestamp": "<string>",
"duration_ms": 1,
"team_sid": "<string>",
"actor_type": "<string>"
}
}{
"success": false,
"error": {
"code": "validation_failed",
"message": "<string>",
"recoverable": true,
"suggestion": "<string>",
"field_errors": {},
"blockers": [
{
"type": "<string>",
"severity": "hard",
"description": "<string>",
"entity_sid": "<string>",
"resolution": "<string>",
"resolution_hint": "<string>",
"count": 123
}
],
"context": {}
},
"meta": {
"trace_id": "<string>",
"span_id": "<string>",
"timestamp": "<string>",
"duration_ms": 1,
"team_sid": "<string>",
"actor_type": "<string>"
}
}Scrape post comments
Direct LinkedIn read (bypasses our DB): one page of the COMMENTS on ONE post, live. This is the only comments read there is - a row carries the comment AND the person who wrote it (content, posted_at, comment_permalink, reactions_count, replies_count, is_pinned, is_edited + member id, nickname, name, headline), annotated with is_own, so reading “what was written” and “who commented” is a single call and never two. Target any post by URL, activity URN, or the backend urn a company-page post / newsletter issue / group thread carries (share, ugcPost, groupPost) in post. Top-level comments only: replies are not on this wire at all, replies_count is a count. Order with sort_order; one wire page per call, page on with cursor.
Contract:
- MCP tool
scrape_linkedin_get_post_comments, registry packagemcp.linkedin/linkedin_scraping, mountlinkedin.scraping. - Operation
action, response envelopeaction. - Flags: paced: a call spends the
scrapingsmart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429rate_limitedwithcontext.retry_after, and the same call succeeds from that moment. Parallel calls on one account queue behind each other.
curl --request POST \
--url https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Team-SID: <api-key>' \
--data '
{
"post": "<string>",
"linkedin_account_sid": "<string>",
"idempotency_key": "<string>",
"page_size": 50,
"cursor": "<string>"
}
'import requests
url = "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments"
payload = {
"post": "<string>",
"linkedin_account_sid": "<string>",
"idempotency_key": "<string>",
"page_size": 50,
"cursor": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Team-SID": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
Authorization: 'Bearer <token>',
'Team-SID': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
post: '<string>',
linkedin_account_sid: '<string>',
idempotency_key: '<string>',
page_size: 50,
cursor: '<string>'
})
};
fetch('https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'post' => '<string>',
'linkedin_account_sid' => '<string>',
'idempotency_key' => '<string>',
'page_size' => 50,
'cursor' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"Team-SID: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments"
payload := strings.NewReader("{\n \"post\": \"<string>\",\n \"linkedin_account_sid\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"page_size\": 50,\n \"cursor\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Team-SID", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments")
.header("Authorization", "Bearer <token>")
.header("Team-SID", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"post\": \"<string>\",\n \"linkedin_account_sid\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"page_size\": 50,\n \"cursor\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/get-post-comments")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Team-SID"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"post\": \"<string>\",\n \"linkedin_account_sid\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"page_size\": 50,\n \"cursor\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"operation": "action",
"action": "<string>",
"item": "<unknown>",
"result": {
"rows": [
{
"commenter_ln_member_id": "<string>",
"commenter_ln_id": "<string>",
"commenter_nickname": "<string>",
"commenter_full_name": "<string>",
"commenter_headline": "<string>",
"commenter_picture_url": "<string>",
"comment_ln_id": "<string>",
"content": "<string>",
"posted_at": "<string>",
"reactions_count": 123,
"comment_permalink": "<string>",
"replies_count": 123,
"is_pinned": true,
"is_edited": true,
"is_own": true
}
],
"paging": {
"page_size": 123,
"next_cursor": "<string>",
"has_more": true,
"total": 123
},
"data_request": {},
"post": {
"post_ln_id": "<string>"
},
"skipped_invalid_rows": 123
},
"meta": {
"trace_id": "<string>",
"span_id": "<string>",
"timestamp": "<string>",
"duration_ms": 1,
"team_sid": "<string>",
"actor_type": "<string>"
},
"pacing": {
"linkedin_account_sid": "<string>",
"limit_type": "<string>",
"next_call_after": "<string>",
"remaining_today": 1
}
}{
"success": false,
"error": {
"code": "validation_failed",
"message": "<string>",
"recoverable": true,
"suggestion": "<string>",
"field_errors": {},
"blockers": [
{
"type": "<string>",
"severity": "hard",
"description": "<string>",
"entity_sid": "<string>",
"resolution": "<string>",
"resolution_hint": "<string>",
"count": 123
}
],
"context": {}
},
"meta": {
"trace_id": "<string>",
"span_id": "<string>",
"timestamp": "<string>",
"duration_ms": 1,
"team_sid": "<string>",
"actor_type": "<string>"
}
}{
"success": false,
"error": {
"code": "validation_failed",
"message": "<string>",
"recoverable": true,
"suggestion": "<string>",
"field_errors": {},
"blockers": [
{
"type": "<string>",
"severity": "hard",
"description": "<string>",
"entity_sid": "<string>",
"resolution": "<string>",
"resolution_hint": "<string>",
"count": 123
}
],
"context": {}
},
"meta": {
"trace_id": "<string>",
"span_id": "<string>",
"timestamp": "<string>",
"duration_ms": 1,
"team_sid": "<string>",
"actor_type": "<string>"
}
}Authorizations
Access token issued by gtm.service.id. Its access_identity claim carries team_sid, actor_sid and actor_type, and that team scope is authoritative.
Team scope for tokens that do not carry one. Ignored when the token already names a team.
Body
Request body of scrape_linkedin_get_post_comments.
The post, named any way you have it. LinkedIn files a post's comments under its social thread (the ugcPost or groupPost urn for those posts, the activity urn for a share post) and a wrong key fails quietly: the activity urn of a ugcPost post reads an EMPTY list newest-first and a first page with total 0 and no next cursor by relevance (live 2026-09-16), a share urn reads empty. So a urn:li:ugcPost:<id> or urn:li:groupPost:<group>-<post>, or a link carrying one (a -ugcPost-<id>- share link), is the thread and reads in one wire page; an urn:li:activity:<id> (bare, in a feed permalink or in a /posts/<slug>-activity-<id>-<hash> share link) or a urn:li:share:<id> costs one post read first (cached 7 days, so only the first call on a post pays it). result.post.post_ln_id is the urn the list was read by: page on with it to skip the read. A linkedin.com link with no id in it (a bare slug URL) adds one more page load to find the urn. An lnkd.in shortlink (the form Copy link to post mints) is expanded server-side and its slug id lifted locally, one wire page like any id-carrying URL; one that expands to a non-LinkedIn target is refused 422 post_not_resolvable, and so is a post the read returns null for (deleted, or not visible to the executing account). The post does NOT need to be tracked or owned by you.
512Executor account (ln_ac_...). OPTIONAL everywhere on this surface. Given: the call runs on that account ONLY; a saturated or held scraping bucket refuses 429 bucket_saturated (with retry_after). Omitted: the service auto-picks one of your connected accounts with remaining capacity (SN verbs pick only Sales-Navigator seats; 422 no_connected_accounts when none is ready, 429 when all are at capacity).
18^ln_ac_Ledger replay guard: a repeat call with the same (team, key) returns the stored outcome; no re-execution. Recommended on every search run. The KEY ALONE decides: the probe does not compare arguments, so reusing one key after changing the arguments hands back the FIRST result. On the twelve search verbs that matters twice over, because switching a call from filters to url (or back) under one key replays instead of running the new search. New search, new key.
128Default 50; one wire page per call.
1 <= x <= 100Order of the comment list. RELEVANCE is LinkedIn's own default and what you get when this is omitted; REVERSE_CHRONOLOGICAL is "Most recent" in the UI, CHRONOLOGICAL walks oldest-first. Polling a watched post for new comments wants REVERSE_CHRONOLOGICAL, because under relevance a new comment is not guaranteed to be on page 1. A cursor belongs to the order it was issued under - do not resume a walk with a different sort_order.
RELEVANCE, CHRONOLOGICAL, REVERSE_CHRONOLOGICAL Opaque resume token from paging.next_cursor; null = first page.
Response
action success envelope.
true action kebab-case verb; matches the route segment.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes