Instagram API / Code samples / Node.js

Instagram API in Node.js — Native fetch, No Package to Install

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

Calling the Instagram API from Node.js.

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

Node.js cursor pagination for Instagram API lists.

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

Node.js error handling for the Instagram API.

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

Things that only bite in Node.js.

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.

Top-level await means ESM

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.

Do not Promise.all a large handle list

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.

Never call this from the browser

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

Instagram API in Node.js FAQ.

Which npm package should I install?

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.

Can I fetch an Instagram feed from React directly?

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.

Does it work on Vercel Edge, Cloudflare Workers or Deno?

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.

Can I post or schedule content through this?

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.

How do I know whether a response cost me anything?

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

Instagram API examples beyond Node.js.

Every endpoint these samples call is documented at /docs/instagram.