# Instagram API Node.js Tutorial: Read, Publish, Refresh

Working Node.js fetch code for the Instagram API: read a profile and media, publish an image with a container and media_publish, and refresh the token.

Canonical page: https://www.tryadeli.com/blog/instagram-api-nodejs
Last updated: 2026-10-19

By Mika Reyes, Co-founder, Adeli. Published 2026-10-19.

## The short answer

To use the Instagram API from Node.js, call Meta's Instagram API with Instagram Login at graph.instagram.com using the built-in fetch and an Instagram User access token. Read the profile from /me and posts from /{ig-id}/media, publish by creating a container, polling its status, and calling media_publish, and refresh the token before day 60. Skip unofficial npm packages. Adeli does the same publish in one POST.

You can use the Instagram API from Node.js with the built-in `fetch` and no npm package: **send an Instagram User access token to `graph.instagram.com`, read the profile and media, and publish with a container plus `media_publish`**. Below is Node.js code for each call, written against Meta's documented endpoints and parameters, then a token refresh and the same publish through Adeli.

This tutorial assumes you already have a Meta app and a token. If you don't, start with our [Instagram API](https://www.tryadeli.com/blog/instagram-api-guide) guide. Writing Python instead? The [Python version of this tutorial](https://www.tryadeli.com/blog/instagram-api-python) has the same steps.

## Is there an Instagram API package for Node.js?

**You don't need one, and the "private API" packages break Instagram's terms.** Meta documents the Instagram API as plain HTTP calls, so `fetch` covers everything on this page.

The npm packages that call themselves an Instagram private API work differently. They log in with an account's username and password and imitate the Instagram mobile app's internal endpoints. That breaks the rules in two places:

- **Instagram's [Terms of Use](https://help.instagram.com/581066165581870)** say you can't "access or collect information in unauthorized ways," including automated collection without Instagram's express permission.
- **Meta's [Automated Data Collection Terms](https://www.facebook.com/legal/automated_data_collection_terms)** say you won't collect data by automated means without Meta's express written permission.

These packages also depend on **undocumented endpoints that can change without notice**, and they need your customers' passwords. **For anything you ship to customers, use the official API with a Meta app.**

## Should I use Instagram Login or Facebook Login?

**Use Instagram Login for most new Node.js projects.** Meta runs two versions of the Instagram API. The read and publish calls differ in the host and the token, and Facebook Login adds a Page lookup to find the account.

| | Instagram API with Instagram Login | Instagram API with Facebook Login |
|---|---|---|
| **Host** | `graph.instagram.com` | `graph.facebook.com` |
| **Token** | Instagram User access token | Facebook Page access token for publishing |
| **Facebook Page needed** | No | Yes |
| **Read permission** | `instagram_business_basic` | `instagram_basic`, `pages_read_engagement` |
| **Publish permission** | `instagram_business_content_publish` | `instagram_content_publish` |

Sources: Meta's [content publishing guide](https://developers.facebook.com/docs/instagram-platform/content-publishing) and [IG Media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-media).

The examples below use **Instagram Login**. To switch to Facebook Login, change the base URL to `https://graph.facebook.com/v25.0` and use a Page token. Our [permissions guide](https://www.tryadeli.com/blog/instagram-api-permissions) explains which scopes each path needs.

## What do you need before you start?

- **Node.js 18 or later.** `fetch` is a global, so there's nothing to install.
- **A professional Instagram account.** Business or Creator. Personal accounts can't be read or published to.
- **An Instagram User access token.** For a test on your own account, click **Generate token** on the Instagram API setup page in the App Dashboard. Meta says those tokens are **valid for 60 days**.
- **A public JPEG URL** for the publish step. Meta downloads the image from it.

Keep the token out of your source. The examples are an ES module (`ig.mjs`) that reads it from an environment variable and shares one helper:

```javascript
const GRAPH = "https://graph.instagram.com/v25.0";
const TOKEN = process.env.IG_ACCESS_TOKEN;

async function ig(path, { method = "GET", params = {}, body } = {}) {
  const url = new URL(`${GRAPH}${path}`);
  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, value);
  }
  const resp = await fetch(url, {
    method,
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await resp.json();
  if (!resp.ok || data.error) {
    const err = data.error ?? {};
    throw new Error(`${err.code}/${err.error_subcode}: ${err.message}`);
  }
  return data;
}
```

## Step 1: Read the Instagram profile

**Call `/me` with the fields you want.** Meta's [getting-started guide](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started) uses `user_id` and `username`, and lists `account_type`, `followers_count`, `follows_count`, `media_count`, `name`, and `profile_picture_url` as other fields.

```javascript
const body = await ig("/me", {
  params: { fields: "user_id,username,account_type,followers_count,media_count" },
});
// Meta's sample response wraps the user in a "data" array
const profile = body.data ? body.data[0] : body;
const igId = profile.user_id;
console.log(profile.username, igId);
```

**`user_id` is the IG ID** that every other Instagram endpoint takes. Store it with the token.

## Step 2: List the account's media

**Call the `/<IG_ID>/media` edge.** It returns media IDs in `data` and paging cursors in `paging`. Ask for fields to get more than the ID:

```javascript
const page = await ig(`/${igId}/media`, {
  params: { fields: "id,media_type,media_url,permalink,timestamp" },
});
for (const media of page.data) {
  console.log(media.timestamp, media.media_type, media.permalink);
}
```

Some fields don't always come back, per Meta's [IG Media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-media):

- **`media_url` can be missing.** Meta omits it when the media contains copyrighted material.
- **`thumbnail_url` is video-only.** Image posts don't return it.
- **`like_count` can be missing.** It's omitted when the owner hides like counts.

To fetch the next page, pass `page.paging.cursors.after` as the `after` parameter. For likes, reach, and views, see our [Instagram insights API guide](https://www.tryadeli.com/blog/instagram-insights-api).

## Step 3: Create a media container

**Publishing starts with a container: `POST /<IG_ID>/media` with a public `image_url`.** Meta's [content publishing guide](https://developers.facebook.com/docs/instagram-platform/content-publishing) sends a JSON body with a bearer token:

```javascript
const container = await ig(`/${igId}/media`, {
  method: "POST",
  body: {
    image_url: "https://www.example.com/images/photo.jpg",
    caption: "Posted from Node.js",
  },
});
const containerId = container.id;
```

The image has to be **a JPEG on a publicly reachable server** when Meta fetches it. A presigned URL that expires in seconds, or a file behind auth, fails. Our guide to [posting to Instagram via API](https://www.tryadeli.com/blog/post-to-instagram-via-api) lists the size and aspect-ratio rules.

## Step 4: Wait for the container to finish

**Poll `status_code` until it reads `FINISHED` before you publish.** Meta recommends checking **once per minute, for no more than 5 minutes**:

```javascript
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function waitForContainer(id) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const { status_code: status } = await ig(`/${id}`, {
      params: { fields: "status_code" },
    });
    if (status === "FINISHED") return;
    if (status === "ERROR" || status === "EXPIRED") {
      throw new Error(`Container ${id} is ${status}`);
    }
    await sleep(60_000);
  }
  throw new Error("Container did not finish in 5 minutes");
}

await waitForContainer(containerId);
```

Images usually finish fast, so the first check often passes. Video takes longer, which is why the loop exists. In a web server, run this in a background job rather than inside a request handler.

## Step 5: Publish with media_publish

**POST the container ID as `creation_id`.** The response is the ID of the live post:

```javascript
const published = await ig(`/${igId}/media_publish`, {
  method: "POST",
  body: { creation_id: containerId },
});
console.log("Published:", published.id);
```

**Each account can publish 100 posts in a rolling 24 hours** through the API, per Meta's publishing guide. Our [rate limit guide](https://www.tryadeli.com/blog/instagram-api-rate-limit) covers the call limits on top of that. Carousels add a child container per image, which our [carousel API guide](https://www.tryadeli.com/blog/instagram-carousel-api) walks through.

## How do you refresh the token in Node.js?

**Call `refresh_access_token` with `grant_type=ig_refresh_token` before day 60.** Meta's [refresh reference](https://developers.facebook.com/docs/instagram-platform/reference/refresh_access_token) only accepts a long-lived token that is **at least 24 hours old and not expired**, and the refreshed token lasts 60 days from the refresh:

```javascript
async function refreshToken(longLivedToken) {
  const url = new URL("https://graph.instagram.com/refresh_access_token");
  url.searchParams.set("grant_type", "ig_refresh_token");
  url.searchParams.set("access_token", longLivedToken);
  const resp = await fetch(url);
  const data = await resp.json();
  if (!resp.ok) throw new Error(data.error?.message ?? "Refresh failed");
  return data; // access_token, token_type, expires_in (seconds)
}
```

Save the new `access_token` and compute its expiry from `expires_in`. Run the refresh on a schedule, around day 45, so a failed job has time to retry. If you start from the 1-hour token that the login flow returns, exchange it first with `grant_type=ig_exchange_token`. Our [access token guide](https://www.tryadeli.com/blog/instagram-api-access-token) has that call and the error 190 fixes.

## How do you handle Instagram API errors in Node.js?

**Read the `error` object instead of stopping at the HTTP status.** Graph API errors carry a `code`, an `error_subcode`, and a `message`, and the `ig()` helper above puts all three in the thrown error.

- **Code 190 means the token is dead.** Send the user back through login.
- **Subcode 2207052** means Meta couldn't fetch the media from your URL.
- **Subcode 2207027** means the container isn't ready to publish yet.

The full list is in Meta's [error code reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/error-codes).

## How do you publish the same image with Adeli in Node.js?

**One POST to `https://app.tryadeli.com/api/v1/posts` replaces Steps 3 to 5 and the refresh job.** [Adeli](https://www.tryadeli.com/product/instagram) holds the Meta developer app, carried Meta's review, and **refreshes every connected account's tokens**. Your code sends **one API key** and the image as base64, and Adeli stages it where Instagram can fetch it:

```javascript
import { readFile } from "node:fs/promises";

const imageB64 = (await readFile("photo.png")).toString("base64");

const response = await fetch("https://app.tryadeli.com/api/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ADELI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    platform: "instagram",
    accountId: "00000000-0000-0000-0000-000000000000",
    caption: "Posted from Node.js",
    image: { contentType: "image/png", base64: imageB64 },
  }),
});
console.log(response.status, await response.json());
```

What changes compared with the direct calls:

- **No public hosting.** Images can be JPEG, PNG, or WebP up to 8 MiB each, and Adeli normalizes them to JPEG.
- **No polling.** Success returns `201` with the post's `permalink`.
- **Readable failures.** An expired connection returns `422 connection_expired`, and an Instagram rejection returns `422 provider_rejected` with Instagram's message.
- **Carousels in the same call.** Swap `image` for `images` with 2 to 10 entries.

Every field is in the [Publish to Instagram docs](https://www.tryadeli.com/docs/api/posts/publish-instagram). The same endpoint in [Adeli's posting API](https://www.tryadeli.com/social-media-posting-api) publishes to TikTok, YouTube, Facebook, X, and Bluesky, and **your first 3 connected accounts are free** on [Adeli's pricing](https://www.tryadeli.com/pricing).

## Frequently asked questions

### Is there an official Instagram API npm package?

Meta documents the Instagram API as plain HTTP requests with curl examples, and the examples on this page need no package at all. Node.js 18 and later ship a global fetch, which covers every call: a URL, query parameters or a JSON body, and a bearer token.

### Can I use an Instagram private API package from npm?

Packages that log in with a username and password and call Instagram's internal app endpoints break Instagram's Terms of Use, which ban accessing or collecting information in unauthorized ways. They also depend on undocumented endpoints that can change without notice. For a product, use the official Instagram API with a Meta app.

### Should I use Instagram Login or Facebook Login in Node.js?

Use Instagram Login unless your users manage Instagram through a Facebook Page. Instagram Login needs no Page and calls graph.instagram.com with an Instagram User token. Facebook Login calls graph.facebook.com and publishes with a Page access token. The fetch code is the same apart from the host and the token.

### Can I upload a local image file to Instagram from Node.js?

Not directly. Meta's container endpoint takes an image_url that it downloads from a public server, so a local file has to be uploaded to public hosting first. Adeli's publish endpoint accepts the image as base64 and stages it where Instagram can fetch it.

## Related Adeli pages

- [Instagram API for Developers](https://www.tryadeli.com/product/instagram.md): Publish, schedule, and read analytics for Instagram through one API. Adeli owns the Instagram developer app, token refresh, and Meta review. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Mika Reyes is the CEO behind Adeli and a tech and AI content creator. She was previously co-founder and CEO of Parallax, which was acquired by Phantom, and a product lead at LinkedIn. She is a Forbes 30 Under 30 honoree.
