@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
Maintainers
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
⚡ 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-chatRequires 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):
- 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. web_profile_infoREST - canonical counts (followers/following/media), biography, verified/private/business flags, external url, category.- Profile GraphQL (
PolarisProfilePageContentQuery) - richest payload when Instagram answers it. - 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
NOTICEfile and the author credit (EryXenX), and clearly state your changes. Removing or replacing the credit violates the license.
