npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eryxenx/insta-chat

v2.1.1

Published

Unofficial Instagram Chat API for Node.js | Build Instagram DM bots on personal accounts — send messages, media, voice notes, reactions, edit & unsend, listen for incoming chats. No browser at runtime

Readme

💬 @eryxenx/insta-chat

Unofficial Instagram DM Chat API for Node.js

Send messages, media, voice notes & reactions · edit, unsend, listen · build Instagram bots on personal accounts · zero browser at runtime

npm license node


⚡ What is this?

@eryxenx/insta-chat is a standalone Instagram Direct Message API — the Instagram sibling of an FCA package. It talks to the same web endpoints www.instagram.com uses (Relay GraphQL + rupload), so you can build a bot on your own personal account:

  • ✅ Send text, photos, videos, voice notes
  • ✅ React, edit and unsend messages
  • ✅ Listen for incoming DMs — realtime MQTT push or polling — and dispatch them to your own commands
  • ✅ Typing indicators, mark-seen, mute/unmute, hide
  • ✅ Users — lookup by id / username, search
  • ✅ Pure HTTP at runtime — no Puppeteer, no browser, no native build

⚠️ Unofficial. Not affiliated with Instagram or Meta. Use only on accounts you own and at your own risk. This package automates a personal account, not the official Instagram Business API.


📦 Installation

npm install @eryxenx/insta-chat

Requires Node.js >= 18.


🚀 Quick start

const login = require("@eryxenx/insta-chat");

// `cookie` is your browser cookie string (needs at least `sessionid`).
login(cookie, (err, api) => {
  if (err) return console.error("Login error:", err.message);
  console.log("Logged in as", api.username || api.userID);

  // send a DM
  api.sendMessage("Hello from my bot 👋", "340282366841710300949128138275732109616");
});

Promise style works too:

const api = await login(cookie);
await api.sendMessage("hi", threadID);

Getting the cookie

Log in to https://www.instagram.com in a browser, open DevTools → Network → copy the Cookie request header (it contains sessionid, ds_user_id, csrftoken, ig_did). Paste the whole string; only sessionid is mandatory.


🧠 Session params (important)

Instagram's web API rejects requests that do not carry the browser session params (__dyn, __csr, __hs, __hsi, __s, fb_dtsg, jazoest, ...). They are produced by Instagram's own JS bundle but are session-scoped, so one real browser page load is enough — capture once, reuse until it expires.

const { harvestSessionParams, findChromium } = require("@eryxenx/insta-chat");

const params = await harvestSessionParams({ /* cookie jar */ });
// -> { __dyn, __csr, __hs, fb_dtsg, lsd, ... }

harvestSessionParams drives a system Chrome/Chromium through puppeteer-core (optional dependency, no browser download). Pass the result to login:

const api = await login(cookie, { sessionParams: params });

If you skip this, the package still tries to bootstrap the minimum tokens (fb_dtsg, lsd) from the Instagram home page automatically. When the params go stale use api.refreshSession().


📚 API

Login

| Call | Description | |------|-------------| | login(cookie, [options], [cb]) | Returns/clbacks a ready InstaClient (FCA surface). | | login(cookie, { native: true }) | Returns the bare InstaAPI instead. | | createClient(cookie, [options]) | Alias of login. |

options: { native, sessionParams, sessionParamsFile, deviceFile, pollInterval, minPollIntervalMs, realtime }.

To make listen() push instead of poll, opt into realtime MQTT:

const api = await login(cookie, { realtime: { enabled: true } });

realtime is { enabled, fallback, host, keepAliveMs }. With fallback !== false (the default) a broker that will not connect degrades to the inbox poller, so the bot still gets events. See Listening.

Two layers

| Layer | What it is | How to get it | |-------|-----------|---------------| | InstaClient | FCA / GoatBot-Pro shaped surface (sendMessage, setMessageReaction, getThreadInfo, …) | login(...) | | InstaAPI | Native Instagram client (sendReaction, sendPhotoBuffer, getInbox, …) | login(..., { native: true }) or client.api |

The FCA methods are one file each under src/api/<group>/<method>.js; adding a method is adding a file. createFcaClient(client) gives a namespaced view (messages.send(), threads.getInfo()); new InstaContext(client, threadID) binds a thread. EventNormalizer maps raw inbox events to the FCA event shape.

Messaging (FCA surface)

| Method | Description | |--------|-------------| | sendMessage(form, threadID, [cb], [replyToMessage]) | Send text/attachment; quote-reply by passing the target message id. | | editMessage(text, messageID) | Edit an outgoing message (within 15 min). | | unsendMessage(messageID) | Unsend a message. | | setMessageReaction(emoji, messageID, [cb], [threadID]) | React to a message. | | sendTypingIndicator(threadID) | Typing indicator. | | getThreadInfo / getThreadList / getThreadHistory | Thread reads. | | getUserInfo / getUID | User lookup. | | getUserInfoV2(idOrIds) | Full profile (all details) for one id or an array of ids. | | getUserProfile(idOrHandle) | The complete merged profile object. | | getProfilePictureV2(idOrHandle, opts) | Profile picture at any size (square + HD). | | getProfileBio(idOrHandle) / getUserID(idOrHandle) / getUserFriendship(id) | Bio, id, friendship state. | | getSelfInfo() | The logged-in account's own profile. | | getAppState / getCookies | Cookie jar. |

Messaging (native InstaAPI)

| Method | Description | |--------|-------------| | sendMessage(text, threadID, replyToItemId?) | Send a text DM (optionally quote-reply). | | replyToMessage(threadID, text, replyToItemId?) | Send a reply (quote the target item). | | sendLink(threadID, link, text) | Send a link with optional text. | | sendReaction(threadID, itemID, emoji) | React to a message. | | removeReaction(threadID, itemID) | Remove a reaction. | | unsendMessage(threadID, itemID) | Unsend a message. | | editMessage(threadID, itemID, text) | Edit an outgoing message (within 15 min). | | startTyping(threadID) / stopTyping(threadID) | Typing indicator. |

Media

| Method | Description | |--------|-------------| | sendPhoto(threadID, filePath, [opts]) | Photo from disk. | | sendPhotoBuffer(threadID, buffer, [opts]) | Photo from a Buffer. | | sendPhotoFromUrl(threadID, url, [opts]) | Photo from a URL. | | sendVideo / sendVideoBuffer / sendVideoFromUrl | Video variants. | | sendVoice / sendVoiceBuffer / sendVoiceFromUrl | Voice-note variants. |

opts: { filename, text }.

Threads & users

| Method | Description | |--------|-------------| | getThreadInfo(threadID) | Thread metadata. | | getThreadHistory(threadID, limit) | Recent messages. | | markThreadSeen(threadID, itemID) | Mark as read. | | muteThread / unmuteThread / hideThread | Thread state. | | getUserInfo(userID) / getUserInfoByUsername(username) | User lookup. | | searchUsers(query, limit) | Search users. | | getUserProfile(idOrUsername, [opts]) | Complete profile merged from every source (see below). | | getProfilePicture(idOrUsername, [opts]) | Best picture URL; opts.size resizes via the CDN. |

Full profiles (getUserInfoV2 / getUserProfile)

V1 getUserInfo returns a small fca-shaped subset. V2 returns every detail a web session can read, and it is logout-safe: profile lookups go through a resolver that tries several sources and never turns a rate-limit (429) or a login redirect (302) into a dead-session error - the bug that used to log the bot out whenever a command looked up a profile.

Sources, merged in order (a field a source cannot supply stays null):

  1. Inbox/thread record - the users the client has already seen in the DM views. Free, needs no request, and the only source that still answers once the profile REST paths 429. getInbox() populates it automatically.
  2. web_profile_info REST - canonical counts (followers/following/media), biography, verified/private/business flags, external url, category.
  3. Profile GraphQL (PolarisProfilePageContentQuery) - richest payload when Instagram answers it.
  4. The account's own page - the home shell Instagram server-renders embeds the logged-in account's full profile, so self always resolves.
const info = await api.getUserInfoV2("32715034217");
// info["32715034217"] => { id, username, name, biography, followerCount,
//   followingCount, mediaCount, isVerified, isPrivate, isBusinessAccount,
//   externalUrl, category, profilePicUrl, profilePicUrlHd, fbid, ... }

const profile = await api.getUserProfile("@someuser");   // the raw merged object
const pic     = await api.getProfilePictureV2("someuser", { size: 640 });
// pic => { url, hd, thumb, sizes: { "150": ..., "320": ..., "640": ..., "1080": ... } }

Listening

const stop = api.listen((err, event) => {
  if (err) return console.error(err.message);
  console.log(event.senderID, "->", event.body);
  // event = { type, threadID, senderID, messageID, body, attachments, timestamp }
});

// later
stop();

listen uses the realtime MQTT stream when you pass { realtime: { enabled: true } } to login — Instagram pushes each new DM over the broker, so replies are near-instant. Without it, listen polls the inbox (default every 15s, override with options.pollInterval). Either way the callback and event shape are identical, and a realtime connection that fails falls back to polling unless you set realtime: { enabled: true, fallback: false }.

Realtime (MQTT)

Instagram's realtime broker (edge-mqtt.facebook.com) is an MQTToT connection authenticated by the same sessionid cookie — no app login. listen() drives it when realtime is enabled; you can also use it directly:

const api = await login(cookie, { native: true });

api.realtimeOn("message", (e) => console.log(e.message));
api.realtimeOn("typing", (e) => console.log(e.threadID, "is typing"));
api.realtimeOn("seen", (e) => console.log(e.threadID, "read"));
api.realtimeOn("presence", (e) => console.log(e.value));

await api.realtimeConnect();
await api.realtimeSubscribe();   // reads seq_id/snapshot_at_ms from the inbox query
// ...later
api.realtimeDisconnect();

RealtimeClient (the raw client) and RealtimeListener (the listen() wrapper) are exported too, for callers that want to drive the socket themselves.


🧩 Example — a tiny DM bot

const login = require("@eryxenx/insta-chat");

(async () => {
  const api = await login(process.env.IG_COOKIE);

  api.listen(async (err, event) => {
    if (err || !event || !event.body) return;
    if (event.body === "/ping") await api.sendMessage("pong 🏓", event.threadID);
    await api.sendReaction(event.threadID, event.messageID, "✅");
  });
})();

🗂️ Project structure

| Path | Description | |------|-------------| | index.js | Entry point; exports login + helpers. | | index.d.ts | TypeScript definitions. | | module/login.js | Classic login(credentials, [options], [callback]). | | src/api.js | InstaAPI facade wiring every native method group. | | src/api/ | FCA methods, one file each: messaging/, threads/, users/, action/, realtime/. | | src/app/InstaClient.js | The FCA-shaped client. | | src/app/InstaContext.js | Per-thread view over a client. | | src/app/createFcaClient.js | Namespaced view (messages.*, threads.*). | | src/app/loadApiMethods.js | Attaches every src/api/*/*.js onto a client. | | src/events/normalizeEvent.js | Raw inbox event → FCA event. | | src/utils/messageStyle.js | *bold* / _italic_ → Unicode. | | src/http.js | HTTP client: web headers, session params, GraphQL, rupload. | | src/constants.js | Endpoint + Relay doc_id table. | | src/cookies.js | Cookie parsing / normalization. | | src/session.js | Browser session-param harvesting (puppeteer-core). | | src/deviceIdentity.js | Persisted device identity. | | src/methods/ | Native: message, media, reactions, thread, user, inbox, typing. | | src/realtime/mqttot.js | Dependency-free MQTToT codec (Thrift compact + MQTT packets + TLS transport). | | src/realtime/client.js | Realtime MQTT client: connect, dispatch, direct events. | | src/realtime/bridge.js | Message-sync patch → raw inbox event. | | src/realtime/listener.js | Wires realtime into listen() with poller fallback. |

See DOCS.md for the full protocol notes (GraphQL doc_ids, send / upload / reaction flows).


📋 Changelog

See CHANGELOG.md.

📄 License

Apache-2.0 © EryXenX

Credit required: you may use and modify this library, but you must keep the original copyright headers, the NOTICE file and the author credit (EryXenX), and clearly state your changes. Removing or replacing the credit violates the license.