For developers
You send a target. We send rows.
One POST with a target and a limit, a run id back, and then rows — typed, deduplicated and whole. No pool to rotate, no headless browser to keep alive, no CAPTCHA invoice, and no retry loop that cannot tell an empty page from a blocked one.
The whole integration, in three steps
It is asynchronous, as everything serious in this category is: a job is spread across capacity rather than queued behind the one in front of it, so a large request does not take proportionally longer than a small one.
POST the scrape
One route for every platform and endpoint. Send a handle, a link or a hashtag and how many rows you want back. You get 202 and a run id in the same breath.
Poll it, or do not
GET the run until it reaches a terminal status, or register a webhook and we post it to you the moment it does — signed so you can check it came from us, and retried if your endpoint is down. Polling stays available either way.
Read typed JSON
Counts as numbers, timestamps as ISO 8601, the account behind a row as a nested object. Deduplicated, and filled to the limit you asked for rather than to whatever the first page happened to hold.
What you send
Generated from the same registry the API validates against, so this is the call rather than an approximation of it. Every endpoint in the reference carries the same thing in six languages.
curl -X POST https://socialholmes.com/api/public/v1/scrape/instagram/comments \
-H "Authorization: Bearer $SOCIALHOLMES_KEY" \
-H "Content-Type: application/json" \
-d '{"parameters": {"shortcode": "C8xY_abcDEF"}, "limit": 25}'
import os
import requests
run = requests.post(
"https://socialholmes.com/api/public/v1/scrape/instagram/comments",
headers={"Authorization": f"Bearer {os.environ['SOCIALHOLMES_KEY']}"},
json={"parameters": {"shortcode": "C8xY_abcDEF"}, "limit": 25},
).json()
print(run["id"], run["status"])
const response = await fetch("https://socialholmes.com/api/public/v1/scrape/instagram/comments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIALHOLMES_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ parameters: { shortcode: "C8xY_abcDEF" }, limit: 25 }),
})
const run = await response.json()
console.log(run.id, run.status)
require "net/http"
require "json"
response = Net::HTTP.post(
URI("https://socialholmes.com/api/public/v1/scrape/instagram/comments"),
{ parameters: { shortcode: "C8xY_abcDEF" }, limit: 25 }.to_json,
"Authorization" => "Bearer #{ENV.fetch('SOCIALHOLMES_KEY')}",
"Content-Type" => "application/json"
)
run = JSON.parse(response.body)
puts run["id"], run["status"]
<?php
$ch = curl_init("https://socialholmes.com/api/public/v1/scrape/instagram/comments");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SOCIALHOLMES_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["parameters" => ["shortcode" => "C8xY_abcDEF"], "limit" => 25]),
]);
$run = json_decode(curl_exec($ch), true);
echo $run["id"], " ", $run["status"];
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"parameters": map[string]string{"shortcode": "C8xY_abcDEF"},
"limit": 25,
})
req, _ := http.NewRequest("POST", "https://socialholmes.com/api/public/v1/scrape/instagram/comments", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SOCIALHOLMES_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var run map[string]any
json.NewDecoder(res.Body).Decode(&run)
fmt.Println(run["id"], run["status"])
}
{
"id": "8f14e45f-ceea-467a-9c2e-1b0f5a4c7d21",
"status": "queued",
"platform": "instagram",
"endpoint": "comments",
"target": "C8xY_abcDEF",
"url": "https://www.instagram.com/p/C8xY_abcDEF/",
"result_count": 0,
"created_at": "2026-10-16T16:24:17Z"
}
What you are not building
Each of these is a thing a team writes in the first week, maintains for as long as the product lives, and would rather not have on its roadmap at all.
- A proxy pool, and the month it takes to work out that the pool is the problem
- A headless browser farm, and the memory graph that comes with it
- A solving bill that scales with how well the target is defended
- A parser per platform, rewritten every time a class name changes
- A retry loop that cannot tell an empty page from a blocked one
Billing
Billed for rows, not requests
Ask for a thousand comments, get four hundred, pay for four hundred. A run that returns nothing costs nothing. The most a run can cost is quoted and reserved before it starts and then settled down to what actually arrived — so you can bound a job before you run it and still only pay for what came back.
Questions
Before you ask
Still unsure? The docs are public and complete — read what the API does before deciding.
How is a request authenticated?
Are there rate limits?
What happens when a run only partly succeeds?
Is there an SDK?
Can I try one without writing anything?
What does it cost at a million rows a month?
Read the reference, then run one
Every endpoint is documented with its parameters, a real response and a request you can copy. Nothing behind a form, and nothing you have to ask for first.