AbortSignal.timeout is the whole timeout story
It needs Node 17.3 or later and replaces the AbortController plus setTimeout dance. Without a signal, fetch waits indefinitely, and an indefinite fetch inside a queue worker is how a job backlog starts.
Instagram API / Code samples / Node.js
Node 18 and above ship fetch and AbortSignal.timeout, which is everything this API needs. Searching npm for an Instagram package is how people end up with an abandoned scraper that wants their password.
First call
Copy this, set your key, and you have a working integration. Everything below is the same request with more of the edge cases handled.
Quickstart
const BASE = "https://api.socialscrape.dev/v1";
const KEY = process.env.INSCRAPE_KEY;
if (!KEY) throw new Error("INSCRAPE_KEY is not set");
const url = new URL(`${BASE}/instagram/profile`);
url.searchParams.set("handle", "natgeo");
const res = await fetch(url, {
headers: { "x-api-key": KEY },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
if (res.status !== 200) {
throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
}
console.log(body.data.handle, body.data.follower_count, body.data.is_verified);
console.log({
charged: body.credits_charged,
remaining: body.credits_remaining,
requestedAt: body.requested_at,
processingTimeMs: body.processing_time_ms,
query: body.query,
});Pagination
Every list endpoint returns a cursor. Stop when it is absent, not after a fixed number of pages — the page size is not a contract.
Node.js — cursor loop
const BASE = "https://api.socialscrape.dev/v1";
const KEY = process.env.INSCRAPE_KEY;
async function getPage(handle, cursor) {
const url = new URL(`${BASE}/instagram/posts`);
url.searchParams.set("handle", handle);
url.searchParams.set("limit", "50");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { "x-api-key": KEY },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
if (res.status !== 200) throw new Error(`${res.status} ${body.error.code}`);
return body;
}
async function allPosts(handle, maxPages = 10) {
const posts = [];
let cursor = null;
let spent = 0;
for (let page = 0; page < maxPages; page += 1) {
const body = await getPage(handle, cursor);
spent += body.credits_charged;
posts.push(...body.data.posts);
cursor = body.data.next_cursor;
if (!cursor) break;
}
return { posts, spent };
}
const { posts, spent } = await allPosts("natgeo");
console.log(posts.length, "posts across", spent, "credits");Errors
Only HTTP 200 is charged, so a retry after a 500 costs you nothing except time. A retry after a 400 costs you nothing and fixes nothing.
Node.js — error handling
const BASE = "https://api.socialscrape.dev/v1";
const KEY = process.env.INSCRAPE_KEY;
class InScrapeError extends Error {
constructor(status, code, message) {
super(`${status} ${code}: ${message}`);
this.status = status;
this.code = code;
this.retryable = status >= 500;
}
}
async function call(path, params = {}) {
const url = new URL(`${BASE}${path}`);
for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));
let res;
try {
res = await fetch(url, { headers: { "x-api-key": KEY }, signal: AbortSignal.timeout(30_000) });
} catch (err) {
// TimeoutError or a transport failure: no response, therefore no charge.
throw new InScrapeError(0, err.name, "no response");
}
const body = await res.json();
if (res.status === 200) return body;
throw new InScrapeError(res.status, body.error.code, body.error.message);
}
async function callWithRetry(path, params, attempts = 3) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
return await call(path, params);
} catch (err) {
if (!err.retryable || attempt === attempts - 1) throw err;
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
}
}
try {
const body = await callWithRetry("/instagram/profile", { handle: "natgeo" });
if (body.data?.profile === null) {
console.log(body.data.profile, "credits charged:", body.credits_charged);
} else {
console.log(body.data.profile.follower_count, "credits left:", body.credits_remaining);
}
} catch (err) {
if (err.code === "INSUFFICIENT_CREDITS") console.log("out of credits");
else if (err.code === "ENDPOINT_NOT_FOUND") console.log("API route not found");
else if (err.code === "INVALID_API_KEY") console.log("check INSCRAPE_KEY");
else throw err;
}Gotchas
It needs Node 17.3 or later and replaces the AbortController plus setTimeout dance. Without a signal, fetch waits indefinitely, and an indefinite fetch inside a queue worker is how a job backlog starts.
These samples run as .mjs, or as .js with "type": "module" in package.json. In CommonJS, wrap the calls in an async main() and call it.
Fifty parallel calls will not finish faster in any way that matters and will burn credits before your error handling catches a bad key. A bounded pool of ten to twenty with a small queue is the shape that survives production.
Putting x-api-key in client-side JavaScript publishes it. Call the API from a route handler, an edge function or a server action, then send your own frontend only the fields it needs. This applies to React, Next.js and every framework that renders on the client.
FAQ
None. Node 18+ has global fetch, and the API is a single header plus a query string. Any npm Instagram package that asks for a username and password is doing something different and riskier than this.
Not with your API key in the browser. Fetch on the server, cache the result, and pass the rendered data to the component. This is also cheaper, because your own cache absorbs the repeat views.
Yes. The calls are standard fetch with no Node-specific APIs, so the same code runs on any runtime with a WHATWG fetch. AbortSignal.timeout is the only modern-ish requirement.
No. Every endpoint is a GET and the API is read-only. Publishing requires Meta's Content Publishing API with an authorised business account, which is a different product entirely.
Read credits_charged on the envelope. It reflects actual resource-based charges on both success and error responses, so summing it across a run gives you the exact spend without checking the dashboard.
Other languages
Every endpoint these samples call is documented at /docs/instagram.