
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
nodejs-insta-private-api-mqt
Advanced tools
Complete Instagram MQTT protocol with full-featured REALTIME and REST API — all in one project.
Full featured Java and nodejs Instagram api private with Mqtt full suport and api rest
Dear users The repository on github is currently unavailable will be available soon
If you This Project you can help with any ammount donation at USDC 0x8AD64F47a715eC24DeF193FBb9aC64d4E857f0f3
This project implements a complete and production-ready MQTT protocol client for Instagram's real-time messaging infrastructure. Instagram uses MQTT natively for direct messages, notifications, and real-time presence updates. This library replicates that exact implementation, allowing developers to build high-performance bots and automation tools that communicate with Instagram's backend using the same protocol the official app uses.
By leveraging MQTT instead of Instagram's REST API, this library achieves sub-500ms message latency, bidirectional real-time communication, and native support for notifications, presence tracking, and thread management. The implementation is reverse-engineered from Instagram's mobile app protocol and tested extensively for reliability and compatibility.
This library is optimized for Direct Messages and implements the core MQTT protocols used by Instagram for:
For full MQTT coverage analysis, see MQTT_COVERAGE_ANALYSIS.md
npm install nodejs-insta-private-api-mqt
Requires Node.js 18 or higher.
Default Device: Samsung Galaxy S25 Ultra (Android 15) - used automatically if you don't set a custom device.
This feature allows you to choose which phone model Instagram sees when your bot connects. Instead of using a default device, you can emulate any Android phone like Samsung Galaxy S25 Ultra, Huawei P60 Pro, Google Pixel 9, and more.
const { IgApiClient } = require('nodejs-insta-private-api');
const ig = new IgApiClient();
// Set device BEFORE login
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
// Now login - Instagram will see a Samsung S25 Ultra
await ig.login({
username: 'your_username',
password: 'your_password'
});
console.log('Logged in with device:', ig.state.deviceString);
| Device Name | Manufacturer | Android Version |
|---|---|---|
| Samsung Galaxy S25 Ultra | Samsung | Android 15 |
| Samsung Galaxy S24 Ultra | Samsung | Android 14 |
| Samsung Galaxy S23 Ultra | Samsung | Android 14 |
| Samsung Galaxy Z Fold 5 | Samsung | Android 14 |
| Huawei P60 Pro | Huawei | Android 12 |
| Huawei Mate 60 Pro | Huawei | Android 12 |
| Google Pixel 8 Pro | Android 14 | |
| Google Pixel 9 Pro | Android 15 | |
| OnePlus 12 | OnePlus | Android 14 |
| Xiaomi 14 Ultra | Xiaomi | Android 14 |
| Xiaomi Redmi Note 13 Pro | Xiaomi | Android 14 |
| OPPO Find X7 Ultra | OPPO | Android 14 |
For complete control, use setCustomDevice() with your own configuration:
const ig = new IgApiClient();
ig.state.setCustomDevice({
manufacturer: 'samsung',
model: 'SM-S928B',
device: 'e3q',
androidVersion: '15',
androidApiLevel: 35,
resolution: '1440x3120',
dpi: '505dpi',
chipset: 'qcom',
build: 'UP1A.231005.007'
});
const { IgApiClient, RealtimeClient, useMultiFileAuthState } = require('nodejs-insta-private-api-mqt');
async function startBot() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState('./auth_info_ig');
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
const realtime = new RealtimeClient(ig);
realtime.on('connected', () => {
console.log('Bot is online and MQTT is connected!');
});
realtime.on('message_live', async (msg) => {
console.log(`[${msg.username}]: ${msg.text}`);
if (msg.text.toLowerCase() === 'ping') {
await realtime.directCommands.sendText({
threadId: msg.thread_id,
text: 'pong!'
});
}
});
if (!auth.hasSession()) {
await ig.login({
username: 'your_username',
password: 'your_password'
});
await auth.saveCreds(ig);
await realtime.startRealTimeListener();
await auth.saveMqttSession(realtime);
}
}
startBot().catch(console.error);
All MQTT direct messaging functionality is available through realtime.directCommands. These methods use proper payload formatting that matches the instagram_mqtt library format.
await realtime.directCommands.sendText({
threadId: '340282366841710300949128114477782749726',
text: 'Hello from MQTT!'
});
await realtime.directCommands.sendTextViaRealtime(threadId, 'Hello!');
await realtime.directCommands.replyToMessage(threadId, messageId, 'This is my reply');
await realtime.directCommands.editMessage(threadId, itemId, 'Updated text here');
await realtime.directCommands.deleteMessage(threadId, itemId);
await realtime.directCommands.sendHashtag({
threadId: threadId,
hashtag: 'photography',
text: 'Check this out'
});
await realtime.directCommands.sendLike({
threadId: threadId
});
await realtime.directCommands.sendLocation({
threadId: threadId,
locationId: '123456789',
text: 'Meet me here'
});
await realtime.directCommands.sendMedia({
threadId: threadId,
mediaId: 'media_id_here',
text: 'Check this post'
});
await realtime.directCommands.sendProfile({
threadId: threadId,
userId: '12345678',
text: 'Follow this account'
});
await realtime.directCommands.sendUserStory({
threadId: threadId,
storyId: 'story_id_here',
text: 'Did you see this?'
});
await realtime.directCommands.sendLink({
threadId: threadId,
link: 'https://example.com',
text: 'Check this link'
});
await realtime.directCommands.sendAnimatedMedia({
threadId: threadId,
id: 'giphy_id_here',
isSticker: false
});
await realtime.directCommands.sendVoice({
threadId: threadId,
uploadId: 'your_upload_id',
waveform: [0.1, 0.5, 0.8, 0.3],
waveformSamplingFrequencyHz: 10
});
await realtime.directCommands.sendReaction({
threadId: threadId,
itemId: messageId,
reactionType: 'like'
});
await realtime.directCommands.sendReaction({
threadId: threadId,
itemId: messageId,
reactionType: 'emoji',
emoji: '🔥'
});
await realtime.directCommands.removeReaction({
threadId: threadId,
itemId: messageId
});
Marks a message as seen (read receipt) in a DM thread. This uses Instagram's REST API internally and falls back to MQTT if needed. The method returns the server response — when successful, status will be "ok".
// Basic usage — mark a specific message as seen
const result = await realtime.directCommands.markAsSeen({
threadId: '340282366841710300949128114477782749726',
itemId: '32661457411201420841385410521202688'
});
console.log(result.status); // "ok"
A common pattern is to automatically mark every incoming message as seen, so the sender always sees the blue "Seen" indicator:
realtime.on('message', async (data) => {
const threadId = data.parsed?.threadId || data.message?.thread_id;
const itemId = data.parsed?.messageId || data.message?.item_id;
const status = data.parsed?.status;
// Only mark messages from other users (skip our own)
if (status === 'sent' || !threadId || !itemId) return;
try {
await realtime.directCommands.markAsSeen({ threadId, itemId });
} catch (err) {
console.error('Failed to mark as seen:', err.message);
}
});
If you want the bot to appear more human, you can add a small delay before sending the read receipt:
realtime.on('message', async (data) => {
const threadId = data.parsed?.threadId || data.message?.thread_id;
const itemId = data.parsed?.messageId || data.message?.item_id;
if (data.parsed?.status === 'sent' || !threadId || !itemId) return;
// Wait 1-3 seconds before marking as seen
const delay = 1000 + Math.random() * 2000;
await new Promise(resolve => setTimeout(resolve, delay));
await realtime.directCommands.markAsSeen({ threadId, itemId });
});
// Start typing
await realtime.directCommands.indicateActivity({
threadId: threadId,
isActive: true
});
// Stop typing
await realtime.directCommands.indicateActivity({
threadId: threadId,
isActive: false
});
Mark view-once photos/videos as seen. Works the same way as markAsSeen but specifically for disappearing media items:
await realtime.directCommands.markVisualMessageSeen({
threadId: threadId,
itemId: messageId
});
await realtime.directCommands.addMemberToThread(threadId, userId);
// Add multiple members
await realtime.directCommands.addMemberToThread(threadId, [userId1, userId2]);
await realtime.directCommands.removeMemberFromThread(threadId, userId);
await realtime.directCommands.leaveThread(threadId);
await realtime.directCommands.updateThreadTitle(threadId, 'New Group Name');
await realtime.directCommands.muteThread(threadId);
// Mute until specific time
await realtime.directCommands.muteThread(threadId, Date.now() + 3600000);
await realtime.directCommands.unmuteThread(threadId);
await realtime.directCommands.approveThread(threadId);
await realtime.directCommands.declineThread(threadId);
await realtime.directCommands.blockUserInThread(threadId, userId);
await realtime.directCommands.reportThread(threadId, 'spam');
await realtime.directCommands.sendDisappearingPhoto({
threadId: threadId,
uploadId: 'your_upload_id',
viewMode: 'once' // 'once' or 'replayable'
});
await realtime.directCommands.sendDisappearingVideo({
threadId: threadId,
uploadId: 'your_upload_id',
viewMode: 'once'
});
Send photos and videos that disappear after viewing — just like the Instagram app camera button in DMs. Instagram calls these "raven" messages internally.
How it works under the hood:
rupload.facebook.com/messenger_image/ (Instagram's dedicated raven upload endpoint)/direct_v2/threads/broadcast/raven_attachment/You don't need to worry about any of that — the library handles everything. Just pick a method and go.
Two view modes:
| Mode | What happens |
|---|---|
'once' | Recipient opens it once, then it's gone forever |
'replayable' | Recipient can replay it, but it still disappears from the chat |
The simplest way to send a disappearing photo. No MQTT setup needed — just login and send:
const { IgApiClient, useMultiFileAuthState, sendRavenPhotoOnce } = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
async function main() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState('./my_session');
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
// Login (or restore session)
await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
// Read any photo from disk
const photo = fs.readFileSync('./secret_photo.jpg');
// Send it as view-once — one line, that's it
const result = await sendRavenPhotoOnce(ig, photo, {
threadId: '340282366841710300949128114477782749726' // your DM thread ID
});
console.log('Sent! Status:', result.status); // "ok"
}
main().catch(console.error);
Same thing, but the recipient can replay it before it disappears:
const { sendRavenPhotoReplayable } = require('nodejs-insta-private-api-mqt');
const photo = fs.readFileSync('./photo.jpg');
const result = await sendRavenPhotoReplayable(ig, photo, {
threadId: 'your_thread_id'
});
console.log('Replayable photo sent!', result.status);
Experience the new and improved video sending! Our implementation is now fully compatible with the latest Instagram protocols.
const { IgApiClientExt } = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
async function sendMySecretVideo() {
const ig = new IgApiClientExt();
await ig.login({ username: 'your_username', password: 'your_password' });
// Just read your video file
const videoData = fs.readFileSync('./my_awesome_video.mp4');
// Send it instantly as a view-once message
const result = await ig.sendRavenVideoOnce(videoData, {
threadId: '1234567890', // The DM thread ID
duration: 10, // Video length in seconds
width: 720, // Width (default 720)
height: 1280 // Height (default 1280)
});
if (result.status === 'ok') {
console.log('Boom! Your secret video is on its way.');
}
}
If you want them to be able to see it one more time before it disappears:
const result = await ig.sendRavenVideoReplayable(videoData, {
threadId: '1234567890',
duration: 15
});
If you want to pick the view mode dynamically, use the base sendRavenPhoto or sendRavenVideo functions with the viewMode option:
const { sendRavenPhoto, sendRavenVideo } = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
// Photo — choose 'once' or 'replayable'
const photoResult = await sendRavenPhoto(ig, fs.readFileSync('./photo.jpg'), {
threadId: 'your_thread_id',
viewMode: 'once' // or 'replayable'
});
// Video — same pattern
const videoResult = await sendRavenVideo(ig, fs.readFileSync('./video.mp4'), {
threadId: 'your_thread_id',
viewMode: 'replayable', // or 'once'
duration: 8,
width: 720,
height: 1280
});
If you already have an MQTT connection running (for receiving messages in real-time), you can use directCommands to send raven media without importing anything extra:
// After setting up realtime connection...
const fs = require('fs');
const photo = fs.readFileSync('./secret.jpg');
// View-once photo
await realtime.directCommands.sendRavenPhotoOnce({
threadId: threadId,
photoBuffer: photo
});
// Replayable photo
await realtime.directCommands.sendRavenPhotoReplayable({
threadId: threadId,
photoBuffer: photo
});
// Or specify viewMode manually
await realtime.directCommands.sendRavenPhoto({
threadId: threadId,
photoBuffer: photo,
viewMode: 'once' // 'once' or 'replayable'
});
// View-once video
const video = fs.readFileSync('./clip.mp4');
await realtime.directCommands.sendRavenVideoOnce({
threadId: threadId,
videoBuffer: video,
duration: 10,
width: 720,
height: 1280
});
// Replayable video
await realtime.directCommands.sendRavenVideoReplayable({
threadId: threadId,
videoBuffer: video,
duration: 10
});
Here's a full working example of a bot that automatically replies to incoming messages with a view-once photo:
const {
IgApiClient,
withRealtime,
useMultiFileAuthState,
GraphQLSubscriptions,
SkywalkerSubscriptions,
sendRavenPhotoOnce,
sendRavenPhotoReplayable,
} = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
async function startBot() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState('./bot_session');
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
await ig.login({ username: 'bot_username', password: 'bot_password' });
await auth.saveCreds(ig);
const { realtime } = withRealtime(ig);
realtime.on('message', async (data) => {
// When someone sends you a text message
if (data.message && data.message.text) {
const threadId = data.message.thread_id;
const text = data.message.text.toLowerCase();
if (text === '!secret') {
// Reply with a view-once photo
const photo = fs.readFileSync('./secret_photo.jpg');
await sendRavenPhotoOnce(ig, photo, { threadId });
console.log('Sent view-once photo to', threadId);
}
if (text === '!replay') {
// Reply with a replayable photo
const photo = fs.readFileSync('./replay_photo.jpg');
await sendRavenPhotoReplayable(ig, photo, { threadId });
console.log('Sent replayable photo to', threadId);
}
}
});
await realtime.connect({
graphQlSubs: [GraphQLSubscriptions.getDirectTypingSubscription(ig.state.cookieUserId)],
skywalkerSubs: [SkywalkerSubscriptions.directSub(ig.state.cookieUserId)],
irisData: await ig.request.send({ url: '/api/v1/direct_v2/inbox/', method: 'GET' }),
});
console.log('Bot is running! Send "!secret" or "!replay" in DMs.');
}
startBot().catch(console.error);
| Function | What it does |
|---|---|
sendRavenPhoto(ig, buffer, { threadId, viewMode }) | Send disappearing photo with chosen mode |
sendRavenPhotoOnce(ig, buffer, { threadId }) | Send view-once photo (opens once, then gone) |
sendRavenPhotoReplayable(ig, buffer, { threadId }) | Send replayable photo (can replay, still disappears) |
sendRavenVideo(ig, buffer, { threadId, viewMode, duration, width, height }) | Send disappearing video with chosen mode |
sendRavenVideoOnce(ig, buffer, { threadId, duration, width, height }) | Send view-once video |
sendRavenVideoReplayable(ig, buffer, { threadId, duration, width, height }) | Send replayable video |
All functions return a response object with status: 'ok' on success and a payload containing the item_id and thread_id.
If you want full control over the process, you can handle the upload yourself and just call the broadcast. Important: media must be uploaded to rupload.facebook.com/messenger_image/ (not rupload_igphoto/) for raven to work.
// Step 1: Upload to messenger_image endpoint (see sendRavenPhoto.js for full headers)
// Step 2: Broadcast
const result = await ig.directThread.broadcastRaven({
uploadId: uploadId, // from the messenger_image upload response
attachmentFbid: mediaId, // media_id from the upload response
threadId: threadId,
viewMode: 'once' // 'once' or 'replayable'
});
await realtime.directCommands.sendScreenshotNotification({
threadId: threadId,
itemId: messageId
});
await realtime.directCommands.sendReplayNotification({
threadId: threadId,
itemId: messageId
});
These methods handle the full flow: upload via HTTP rupload, then broadcast to thread.
const fs = require('fs');
const photoBuffer = fs.readFileSync('./photo.jpg');
await realtime.directCommands.sendPhoto({
threadId: threadId,
photoBuffer: photoBuffer,
caption: 'Check this out',
mimeType: 'image/jpeg'
});
const fs = require('fs');
const videoBuffer = fs.readFileSync('./video.mp4');
await realtime.directCommands.sendVideo({
threadId: threadId,
videoBuffer: videoBuffer,
caption: 'Watch this',
duration: 15,
width: 720,
height: 1280
});
await realtime.directCommands.sendForegroundState({
inForegroundApp: true,
inForegroundDevice: true,
keepAliveTimeout: 60
});
| Method | Description |
|---|---|
sendText({ threadId, text }) | Send text message |
sendTextViaRealtime(threadId, text) | Send text (alternative) |
sendHashtag({ threadId, hashtag, text }) | Send hashtag |
sendLike({ threadId }) | Send heart/like |
sendLocation({ threadId, locationId, text }) | Send location |
sendMedia({ threadId, mediaId, text }) | Share a post |
sendProfile({ threadId, userId, text }) | Share a profile |
sendUserStory({ threadId, storyId, text }) | Share a story |
sendLink({ threadId, link, text }) | Send a link |
sendAnimatedMedia({ threadId, id, isSticker }) | Send GIF/sticker |
sendVoice({ threadId, uploadId, waveform }) | Send voice message |
sendReaction({ threadId, itemId, emoji }) | React to message |
removeReaction({ threadId, itemId }) | Remove reaction |
replyToMessage(threadId, messageId, text) | Quote reply |
editMessage(threadId, itemId, newText) | Edit message |
deleteMessage(threadId, itemId) | Delete message |
markAsSeen({ threadId, itemId }) | Mark as read |
indicateActivity({ threadId, isActive }) | Typing indicator |
markVisualMessageSeen({ threadId, itemId }) | Mark disappearing media seen |
addMemberToThread(threadId, userId) | Add group member |
removeMemberFromThread(threadId, userId) | Remove group member |
leaveThread(threadId) | Leave group |
updateThreadTitle(threadId, title) | Change group name |
muteThread(threadId, muteUntil) | Mute thread |
unmuteThread(threadId) | Unmute thread |
approveThread(threadId) | Accept message request |
declineThread(threadId) | Decline message request |
blockUserInThread(threadId, userId) | Block user |
reportThread(threadId, reason) | Report thread |
sendDisappearingPhoto({ threadId, uploadId }) | Send view-once photo (MQTT metadata only) |
sendDisappearingVideo({ threadId, uploadId }) | Send view-once video (MQTT metadata only) |
sendRavenPhoto({ threadId, photoBuffer, viewMode }) | Upload & send disappearing photo (full flow) |
sendRavenPhotoOnce({ threadId, photoBuffer }) | Upload & send view-once photo |
sendRavenPhotoReplayable({ threadId, photoBuffer }) | Upload & send replayable photo |
sendRavenVideo({ threadId, videoBuffer, viewMode }) | Upload & send disappearing video (full flow) |
sendRavenVideoOnce({ threadId, videoBuffer }) | Upload & send view-once video |
sendRavenVideoReplayable({ threadId, videoBuffer }) | Upload & send replayable video |
sendScreenshotNotification({ threadId, itemId }) | Screenshot alert |
sendReplayNotification({ threadId, itemId }) | Replay alert |
sendPhoto({ threadId, photoBuffer, caption }) | Upload & send photo |
sendVideo({ threadId, videoBuffer, caption }) | Upload & send video |
sendForegroundState(state) | Connection keepalive |
This feature provides persistent media download for Instagram DM messages.
const {
downloadContentFromMessage,
isViewOnceMedia
} = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
realtime.on('message', async (data) => {
const msg = data.message;
if (isViewOnceMedia(msg)) {
const stream = await downloadContentFromMessage(msg);
let buffer = Buffer.from([]);
for await (const chunk of stream) {
buffer = Buffer.concat([buffer, chunk]);
}
const ext = stream.mediaInfo.type.includes('video') ? 'mp4' : 'jpg';
fs.writeFileSync(`viewonce_${Date.now()}.${ext}`, buffer);
}
});
const { downloadMediaBuffer, hasMedia } = require('nodejs-insta-private-api-mqt');
realtime.on('message', async (data) => {
const msg = data.message;
if (hasMedia(msg)) {
const { buffer, mediaInfo } = await downloadMediaBuffer(msg);
fs.writeFileSync(`media_${Date.now()}.jpg`, buffer);
}
});
| Function | Description |
|---|---|
downloadContentFromMessage(message) | Download as stream |
downloadMediaBuffer(message) | Download as Buffer |
extractMediaUrls(message) | Get CDN URLs |
hasMedia(message) | Check if has media |
getMediaType(message) | Get media type |
isViewOnceMedia(message) | Check if disappearing |
The core of any bot built with this library is the connection.update event. It fires every time the MQTT connection state changes and tells you exactly what happened — whether you just connected, got disconnected, or the session expired. Combined with the message event for incoming DMs, these two hooks are all you need for most bots.
Both client.on('connection.update', ...) and client.ev.on('connection.update', ...) do exactly the same thing. The .ev property is just an alias for the client itself — use whichever style you prefer.
The smallest complete bot you can write: logs in, connects to MQTT, replies "pong" to any "ping" message, and handles session expiry gracefully.
const {
IgApiClient,
RealtimeClient,
useMultiFileAuthState,
GraphQLSubscriptions,
DisconnectReason,
} = require('nodejs-insta-private-api-mqt');
async function main() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState('./session');
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
if (auth.hasSession()) {
await auth.loadCreds(ig);
const valid = await auth.isSessionValid(ig).catch(() => false);
if (!valid) {
const result = await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
}
} else {
const result = await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
}
const realtime = new RealtimeClient(ig);
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'connecting') {
console.log('Connecting to Instagram MQTT...');
} else if (connection === 'open') {
console.log('Connected! Bot is live.');
} else if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
if (code === DisconnectReason.loggedOut) {
console.log('Session expired. Delete the ./session folder and re-run the bot.');
process.exit(1);
} else {
console.log('Disconnected, will reconnect automatically...');
}
}
});
realtime.on('message', async (data) => {
const msg = data.message || data.parsed;
if (!msg?.text || !msg.thread_id) return;
if (msg.text.toLowerCase() === 'ping') {
await realtime.directCommands.sendText({ threadId: msg.thread_id, text: 'pong!' });
}
});
const userId = ig.state.cookieUserId;
await realtime.connect({
graphQlSubs: [
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
GraphQLSubscriptions.getAppPresenceSubscription(),
GraphQLSubscriptions.getDirectStatusSubscription(),
].filter(Boolean),
});
await new Promise(() => {});
}
main().catch(console.error);
A production bot needs to survive everything: sessions that expire without warning, rate limits, network drops, and server hiccups. This example shows the full setup with every disconnect reason handled and a graceful shutdown on Ctrl+C.
const {
IgApiClient,
RealtimeClient,
useMultiFileAuthState,
GraphQLSubscriptions,
DisconnectReason,
} = require('nodejs-insta-private-api-mqt');
const SESSION_FOLDER = './bot_session';
const USERNAME = 'your_username';
const PASSWORD = 'your_password';
async function startBot() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState(SESSION_FOLDER);
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
// Restore or create session
if (auth.hasSession()) {
await auth.loadCreds(ig);
const valid = await auth.isSessionValid(ig).catch(() => false);
if (!valid) {
console.log('Session expired, logging in again...');
const result = await ig.login({ username: USERNAME, password: PASSWORD });
await auth.saveCreds(ig);
console.log('Logged in, session saved.');
} else {
console.log('Session restored.');
}
} else {
console.log('No session found, logging in...');
const result = await ig.login({ username: USERNAME, password: PASSWORD });
await auth.saveCreds(ig);
console.log('Logged in, session saved.');
}
const realtime = new RealtimeClient(ig);
const userId = ig.state.cookieUserId;
// Connection lifecycle — this is how you react to every state change
realtime.ev.on('connection.update', ({ connection, lastDisconnect, isNewLogin }) => {
if (connection === 'connecting') {
console.log('[BOT] Connecting...');
return;
}
if (connection === 'open') {
console.log(`[BOT] Connected${isNewLogin ? ' (first login)' : ''}`);
return;
}
if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
const msg = lastDisconnect?.error?.message;
switch (code) {
case DisconnectReason.loggedOut:
// Instagram invalidated the session — must log in fresh
console.error('[BOT] Logged out by Instagram. Delete session and re-run.');
process.exit(1);
break;
case DisconnectReason.rateLimited:
// Too many requests — library backs off automatically
console.warn('[BOT] Rate limited. Backing off before next reconnect...');
break;
case DisconnectReason.connectionClosed:
// You called realtime.disconnect() — expected
console.log('[BOT] Disconnected intentionally.');
break;
case DisconnectReason.timedOut:
// Keepalive ping failed — library will reconnect
console.warn('[BOT] Connection timed out, reconnecting...');
break;
case DisconnectReason.reconnectFailed:
// All retry attempts exhausted
console.error(`[BOT] Reconnect failed after all attempts: ${msg}`);
process.exit(1);
break;
default:
// Network drop, server error, etc. — library auto-reconnects
console.warn(`[BOT] Disconnected (code ${code}): ${msg}`);
}
}
});
// Fires when the library successfully reconnects after a drop
realtime.on('reconnected', ({ attempt }) => {
console.log(`[BOT] Reconnected on attempt #${attempt}`);
});
// Fires when all reconnect attempts have been exhausted
realtime.on('reconnect_failed', ({ attempts, lastErrorType }) => {
console.error(`[BOT] Failed to reconnect after ${attempts} attempts. Last error type: ${lastErrorType}`);
process.exit(1);
});
// Fires after 3+ consecutive auth failures
realtime.on('auth_failure', ({ count }) => {
console.error(`[BOT] Auth failed ${count} times in a row. Session is probably expired.`);
});
// Non-fatal warnings (bad payload, unknown topic, etc.)
realtime.on('warning', (w) => {
console.warn('[BOT] Warning:', w?.message || w);
});
// Incoming DMs
realtime.on('message', async (data) => {
const msg = data.message || data.parsed;
if (!msg?.text || !msg.thread_id) return;
const threadId = msg.thread_id;
const itemId = msg.item_id || msg.messageId;
const text = msg.text.trim();
// Mark as seen
if (itemId) {
await realtime.directCommands.markAsSeen({ threadId, itemId }).catch(() => {});
}
console.log(`[MSG] ${msg.from_user_id || msg.userId}: "${text}"`);
if (text.toLowerCase() === 'ping') {
await realtime.directCommands.sendText({ threadId, text: 'pong!' });
}
});
// Typing indicators
realtime.on('typing', (data) => {
console.log(`[TYPING] Thread ${data?.thread_id || data?.threadId}`);
});
// Fetch inbox for iris data (needed for full message sync)
let irisData = null;
try {
irisData = await ig.request.send({ url: '/api/v1/direct_v2/inbox/', method: 'GET' });
} catch (_) {}
// Connect with subscriptions
await realtime.connect({
irisData: irisData || undefined,
graphQlSubs: [
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
GraphQLSubscriptions.getAppPresenceSubscription(),
GraphQLSubscriptions.getDirectStatusSubscription(),
userId && GraphQLSubscriptions.getAsyncAdSubscription(userId),
GraphQLSubscriptions.getClientConfigUpdateSubscription(),
].filter(Boolean),
});
console.log('[BOT] Bot is running. Send it a DM to test.');
// Graceful shutdown
process.on('SIGINT', () => {
console.log('[BOT] Shutting down...');
realtime.disconnect();
process.exit(0);
});
await new Promise(() => {});
}
startBot().catch(console.error);
This pattern is what makes a bot feel human: mark the message as seen right away, show a typing indicator while "thinking", send the reply, then react to the original message.
realtime.on('message', async (data) => {
const msg = data.message || data.parsed;
if (!msg?.text || !msg.thread_id) return;
const threadId = msg.thread_id;
const itemId = msg.item_id || msg.messageId;
const text = msg.text.toLowerCase().trim();
// 1. Mark as seen immediately (sender sees blue "Seen")
if (itemId) {
await realtime.directCommands.markAsSeen({ threadId, itemId }).catch(() => {});
}
// 2. Show typing indicator
await realtime.directCommands.indicateActivity({ threadId, isActive: true });
// 3. Small delay to look human
await new Promise(r => setTimeout(r, 1500 + Math.random() * 1500));
// 4. Send reply based on message content
if (text === 'hi' || text === 'hello' || text === 'hey') {
await realtime.directCommands.sendText({ threadId, text: 'Hey! What can I do for you?' });
// React to the greeting
if (itemId) {
await realtime.directCommands.sendReaction({
threadId,
itemId,
reactionType: 'emoji',
emoji: '👋',
});
}
} else if (text === 'ping') {
await realtime.directCommands.sendText({ threadId, text: 'pong!' });
} else if (text === 'status') {
await realtime.directCommands.sendText({
threadId,
text: `I'm online. MQTT connected: ${realtime._mqttConnected ? 'yes' : 'no'}`,
});
} else {
// Default reply
await realtime.directCommands.sendText({
threadId,
text: "I got your message! I don't know how to respond to that yet.",
});
}
// 5. Stop typing indicator
await realtime.directCommands.indicateActivity({ threadId, isActive: false });
});
When the connection drops, lastDisconnect.error.output.statusCode gives you a numeric code. The DisconnectReason enum maps every code to a human-readable name so you don't have to remember the numbers:
const { DisconnectReason } = require('nodejs-insta-private-api-mqt');
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection !== 'close') return;
const code = lastDisconnect?.error?.output?.statusCode;
const message = lastDisconnect?.error?.message;
const when = lastDisconnect?.date;
console.log('Disconnected at:', when?.toISOString());
console.log('Reason code:', code);
console.log('Message:', message);
if (code === DisconnectReason.loggedOut) {
// 401 — Instagram says your session is invalid
// Only fix: delete the session folder and log in again
console.log('Need to re-login');
} else if (code === DisconnectReason.rateLimited) {
// 429 — sent too many requests too fast
// The library automatically applies a longer backoff for rate limits
} else if (code === DisconnectReason.connectionLost) {
// 408 — connection dropped with no specific reason
// This is the most common disconnect — usually just a network blip
} else if (code === DisconnectReason.connectionClosed) {
// 428 — you called realtime.disconnect() intentionally
} else if (code === DisconnectReason.timedOut) {
// 504 — MQTT keepalive ping timed out
} else if (code === DisconnectReason.networkError) {
// 503 — ECONNRESET, ETIMEDOUT, DNS failure, etc.
} else if (code === DisconnectReason.protocolError) {
// 500 — Thrift or MQTT parsing failed
} else if (code === DisconnectReason.serverError) {
// 502 — Instagram server returned a 5xx
} else if (code === DisconnectReason.reconnectFailed) {
// 503 — all automatic reconnect attempts were exhausted
process.exit(1);
}
});
All DisconnectReason values at a glance:
| Name | Code | When it happens |
|---|---|---|
loggedOut | 401 | Session invalid — fresh login required |
rateLimited | 429 | Too many requests — library backs off automatically |
connectionLost | 408 | Unexpected drop, no specific error info |
connectionClosed | 428 | You called disconnect() yourself |
timedOut | 504 | MQTT ping/keepalive timeout |
networkError | 503 | ECONNRESET, ETIMEDOUT, or DNS failure |
protocolError | 500 | Thrift or MQTT parse error |
serverError | 502 | Instagram returned a 5xx response |
reconnectFailed | 503 | All auto-reconnect attempts exhausted |
startRealTimeListener() is a convenience wrapper around connect() that automatically builds the recommended set of GraphQL subscriptions for you. It also accepts optional health monitor and logger config:
const realtime = new RealtimeClient(ig);
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
// same as above
});
realtime.on('message', async (data) => {
// handle incoming DMs
});
// Simplest connect — auto-builds subscriptions
await realtime.startRealTimeListener();
// Or with health monitoring and file logging enabled:
await realtime.startRealTimeListener({
credentials: { username: 'your_username', password: 'your_password' },
enablePersistentLogger: true,
logDir: './mqtt-logs',
});
The health monitor periodically calls /api/v1/accounts/current_user/ to check if the session is still valid and automatically re-logs in if it detects expiry. The persistent logger writes all MQTT events to rotating files in logDir, which is useful for debugging disconnects that happen when you're not watching.
When the health monitor is enabled, these additional events fire on realtime:
realtime.on('health_check', ({ status, stats }) => {
// status: 'ok' or 'failed'
// stats: { totalUptimeHuman, uptimePercent, reconnects, ... }
console.log(`Health: ${status}, uptime: ${stats.totalUptimeHuman}`);
});
realtime.on('session_expired', () => {
console.log('Session detected as expired by health monitor');
});
realtime.on('relogin_success', () => {
console.log('Automatically re-logged in after session expiry');
});
realtime.on('relogin_failed', ({ error }) => {
console.error('Auto re-login failed:', error?.message);
});
You can also pull stats at any time:
const health = realtime.getHealthStats();
console.log('Uptime:', health.uptimePercent + '%');
console.log('Reconnects so far:', health.reconnects);
console.log('Session started:', health.sessionStart);
const authState = await useMultiFileAuthState('./auth_folder');
| Method | Description |
|---|---|
hasSession() | Check if credentials exist |
hasMqttSession() | Check if MQTT session exists |
loadCreds(ig) | Load saved credentials |
saveCreds(ig) | Save current credentials |
isSessionValid(ig) | Validate with Instagram |
loadMqttSession() | Get saved MQTT session |
saveMqttSession(realtime) | Save MQTT session |
clearSession() | Delete all session files |
// Login
await ig.login({
username: 'your_username',
password: 'your_password'
});
// Save session
const serialized = ig.state.serialize();
fs.writeFileSync('session.json', JSON.stringify(serialized));
// Load session
const session = JSON.parse(fs.readFileSync('session.json'));
await ig.state.deserialize(session);
// Get inbox
const inbox = await ig.direct.getInbox();
// Get thread
const thread = await ig.direct.getThread(threadId);
// Send text by username (looks up user, finds/creates thread, sends)
await ig.direct.send({ to: 'target_username', message: 'Hello!' });
// Send text by user ID (no username lookup needed)
await ig.direct.sendToUserId('12345678', 'Hello!');
// Send text to an existing thread (by thread ID)
await ig.directThread.sendToGroup({ threadId: '340282366841710300...', message: 'Hello!' });
// Low-level broadcast (full control over payload)
await ig.directThread.broadcast({
threadIds: ['340282366841710300...'],
item: 'text',
form: { text: 'Hello!' },
});
Everything below uses the REST HTTP endpoints, not MQTT. You don't need RealtimeClient for any of this — just IgApiClient and a valid session.
const { IgApiClient } = require('nodejs-insta-private-api-mqt');
const ig = new IgApiClient();
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
await ig.login({ username: 'your_username', password: 'your_password' });
// you're ready to use any of the methods below
await ig.login({ username: 'your_username', password: 'your_password' });
// or the shorter way
await ig.account.login('your_username', 'your_password');
When Instagram asks for a 2FA code, login() throws an error that contains the two_factor_identifier. Catch it and finish the flow:
try {
await ig.account.login('username', 'password');
} catch (err) {
if (err.response?.body?.two_factor_required) {
const twoFactorId = err.response.body.two_factor_info.two_factor_identifier;
// user enters the code from their authenticator app or SMS
const code = '123456';
await ig.account.twoFactorLogin('username', code, twoFactorId, '1');
// verificationMethod: '1' = SMS, '3' = TOTP app
console.log('2FA login successful');
}
}
Set up authenticator-app-based 2FA for your account. This generates a seed you can add to Google Authenticator, Authy, etc.
// generate a TOTP seed (this is the secret key for your authenticator app)
const seed = await ig.totp.generateSeed();
console.log('Add this to your authenticator app:', seed.totp_seed);
// after adding it, verify with a code from the app to confirm
const code = '123456'; // from authenticator app
await ig.totp.enable(code);
console.log('TOTP 2FA is now enabled');
// get backup codes in case you lose your authenticator
const backupCodes = await ig.totp.getBackupCodes();
console.log('Save these somewhere safe:', backupCodes);
// disable TOTP 2FA
await ig.totp.disable();
// enable SMS 2FA
await ig.totp.smsTwoFactorEnable('+1234567890');
// confirm with the code you received
await ig.totp.smsTwoFactorConfirm('123456');
// disable it later
await ig.totp.disableSmsTwoFactor();
When Instagram triggers a security checkpoint (suspicious login, new location, etc.), you need to resolve the challenge:
try {
await ig.account.login('username', 'password');
} catch (err) {
if (err.response?.body?.challenge) {
const challengeUrl = err.response.body.challenge.api_path;
// option 1: automatic resolution (tries to handle it for you)
const result = await ig.challenge.auto(challengeUrl);
console.log('Challenge result:', result);
// option 2: manual step-by-step
// first, see what verification methods are available
const page = await ig.challenge.getChallengePage(challengeUrl);
// choose SMS (0) or email (1)
await ig.challenge.selectVerifyMethod(challengeUrl, 1);
// enter the code you received
await ig.challenge.sendSecurityCode(challengeUrl, '123456');
}
}
const me = await ig.account.currentUser();
console.log('Username:', me.user.username);
console.log('Follower count:', me.user.follower_count);
await ig.account.editProfile({
fullName: 'John Doe',
biography: 'building cool stuff',
externalUrl: 'https://example.com',
email: 'john@example.com',
phoneNumber: '+1234567890',
username: 'johndoe_new'
});
await ig.account.setBiography('i like building things that work');
await ig.account.setExternalUrl('https://mywebsite.com');
await ig.account.removeBioLinks();
await ig.account.changePassword('old_password_here', 'new_password_here');
await ig.account.setPrivate();
await ig.account.setPublic();
// 1 = male, 2 = female, 3 = prefer not to say, 4 = custom
await ig.account.setGender(1);
// you need an upload_id from a previous photo upload
await ig.account.profilePictureChange(uploadId);
await ig.account.profilePictureRemove();
// get the public keys for Instagram's password encryption (needed for some flows)
const keys = await ig.account.passwordPublicKeys();
// send password recovery via email
await ig.account.sendRecoveryFlowEmail('user@example.com');
// or via SMS
await ig.account.sendRecoveryFlowSms('+1234567890');
// by username
const user = await ig.user.infoByUsername('instagram');
console.log('User ID:', user.pk);
console.log('Followers:', user.follower_count);
// by user ID
const userById = await ig.user.info('25025320');
const userId = await ig.user.userIdFromUsername('instagram');
// returns '25025320'
const username = await ig.user.usernameFromUserId('25025320');
// returns 'instagram'
const results = await ig.user.search('john', 20);
results.users.forEach(u => {
console.log(u.username, '-', u.full_name);
});
// exact match
const exact = await ig.user.searchExact('johndoe');
await ig.user.follow('25025320');
await ig.user.unfollow('25025320');
await ig.user.block('25025320');
await ig.user.unblock('25025320');
// see all blocked users
const blocked = await ig.user.getBlockedUsers();
// mute posts, stories, or both
await ig.user.mute('25025320', { mutePosts: true, muteStories: true });
await ig.user.unmute('25025320', { unmutePosts: true, unmuteStories: true });
// get up to 200 followers at a time
const followers = await ig.user.getFollowers('25025320', 200);
console.log('Got', followers.users.length, 'followers');
// pagination — pass the maxId from the previous response
const moreFollowers = await ig.user.getFollowers('25025320', 200, followers.next_max_id);
// same for following
const following = await ig.user.getFollowing('25025320', 200);
// grab the latest 50 posts
const posts = await ig.user.getUserMedias('25025320', 50);
posts.items.forEach(item => {
console.log(item.pk, '-', item.caption?.text?.substring(0, 50));
});
// next page
const morePosts = await ig.user.getUserMedias('25025320', 50, posts.next_max_id);
const reels = await ig.user.getUserReels('25025320', 50);
const clips = await ig.user.getUserClips('25025320', 50);
const stories = await ig.user.getUserStories('25025320');
stories.reel?.items.forEach(story => {
console.log('Story:', story.pk, 'taken at:', story.taken_at);
});
const tagged = await ig.user.getUserTags('25025320');
const mutual = await ig.user.getMutualFollowers('25025320');
await ig.user.removeFollower('25025320');
// reason: 1 = spam, 2 = inappropriate, etc.
await ig.user.report('25025320', 1);
const suggestions = await ig.user.getSuggested();
// check relationship status with multiple users at once
const statuses = await ig.user.getFriendshipStatuses(['12345', '67890', '11111']);
const info = await ig.media.info('3193593212003331660');
console.log('Type:', info.items[0].media_type);
console.log('Likes:', info.items[0].like_count);
These are super useful when you have a post URL and need the numeric ID, or the other way around.
const { MediaRepository } = require('nodejs-insta-private-api-mqt');
// convert shortcode to numeric PK
const pk = MediaRepository.mediaPkFromCode('CxR7Bsejq5M');
// '3193593212003331660'
// convert PK back to shortcode
const code = MediaRepository.mediaCodeFromPk('3193593212003331660');
// 'CxR7Bsejq5M'
// extract PK directly from a full URL
const pkFromUrl = MediaRepository.mediaPkFromUrl('https://www.instagram.com/p/CxR7Bsejq5M/');
// '3193593212003331660'
await ig.media.like('3193593212003331660');
await ig.media.unlike('3193593212003331660');
const comment = await ig.media.comment('3193593212003331660', 'great shot!');
console.log('Comment ID:', comment.comment.pk);
await ig.media.replyToComment('3193593212003331660', '17858893269000001', '@user thanks!');
await ig.media.likeComment('3193593212003331660', '17858893269000001');
await ig.media.unlikeComment('3193593212003331660', '17858893269000001');
await ig.media.pinComment('3193593212003331660', '17858893269000001');
await ig.media.unpinComment('3193593212003331660', '17858893269000001');
// single
await ig.media.deleteComment('3193593212003331660', '17858893269000001');
// bulk delete
await ig.media.bulkDeleteComments('3193593212003331660', [
'17858893269000001',
'17858893269000002',
'17858893269000003'
]);
const comments = await ig.media.comments('3193593212003331660', null, 20);
// next page:
const moreComments = await ig.media.comments('3193593212003331660', comments.next_min_id, 20);
const thread = await ig.media.commentThreadComments('3193593212003331660', '17858893269000001');
const likers = await ig.media.likers('3193593212003331660');
likers.users.forEach(u => console.log(u.username));
await ig.media.save('3193593212003331660');
// save to a specific collection
await ig.media.save('3193593212003331660', 'collection_id_here');
await ig.media.unsave('3193593212003331660');
await ig.media.archive('3193593212003331660');
await ig.media.unarchive('3193593212003331660');
// mediaType: 'PHOTO', 'VIDEO', 'CAROUSEL'
await ig.media.delete('3193593212003331660', 'PHOTO');
await ig.media.edit('3193593212003331660', 'new caption goes here', {
usertags: { in: [{ user_id: '12345', position: [0.5, 0.5] }] }
});
await ig.media.disableComments('3193593212003331660');
await ig.media.enableComments('3193593212003331660');
// download by URL
const buffer = await ig.media.downloadByUrl('https://instagram.cdnurl.com/...');
// download by PK (photo or video)
const photo = await ig.media.downloadPhoto('3193593212003331660');
const video = await ig.media.downloadVideo('3193593212003331660');
const oembed = await ig.media.oembed('https://www.instagram.com/p/CxR7Bsejq5M/');
console.log(oembed.title, '-', oembed.author_name);
const user = await ig.media.getUser('3193593212003331660');
Upload and browse Reels through the REST API.
const result = await ig.clip.upload({
videoBuffer: fs.readFileSync('./reel.mp4'),
caption: 'check this out',
coverImage: fs.readFileSync('./cover.jpg'), // optional
duration: 15,
width: 1080,
height: 1920,
audisMuted: false,
});
console.log('Reel PK:', result.media?.pk);
const configured = await ig.clip.configure({
upload_id: uploadId,
caption: 'my reel',
duration: 15,
width: 1080,
height: 1920,
});
// the explore-style reels feed
const discover = await ig.clip.discoverReels(10);
discover.items.forEach(reel => {
console.log(reel.media.code, '-', reel.media.caption?.text?.substring(0, 40));
});
// paginate
const more = await ig.clip.discoverReels(10, discover.paging_info?.max_id);
// connected reels (similar reels after watching one)
const connected = await ig.clip.connectedReels(10);
const reelBuffer = await ig.clip.download('3193593212003331660');
// or from URL
const reelFromUrl = await ig.clip.downloadByUrl('https://instagram.cdnurl.com/...');
const music = await ig.clip.musicInfo({ music_canonical_id: '12345' });
const tray = await ig.story.getFeed();
tray.tray.forEach(reel => {
console.log(reel.user.username, '- stories:', reel.media_count);
});
const stories = await ig.story.getUserStories('25025320');
stories.reel?.items.forEach(item => {
console.log('Type:', item.media_type, 'Taken at:', item.taken_at);
});
const result = await ig.story.upload({
file: fs.readFileSync('./story.jpg'),
caption: 'hello world', // optional
});
console.log('Story ID:', result.media?.pk);
const result = await ig.story.uploadVideo({
file: fs.readFileSync('./story.mp4'),
duration: 10,
width: 1080,
height: 1920,
});
await ig.story.seen([
{ id: 'media_id_1', taken_at: 1700000000, user: { pk: '25025320' } }
]);
await ig.story.react({
mediaId: '3193593212003331660',
reelId: '25025320',
emoji: '🔥'
});
const highlights = await ig.highlights.getHighlightsTray('25025320');
highlights.tray.forEach(h => {
console.log(h.id, '-', h.title);
});
const highlight = await ig.highlights.getHighlight('highlight:12345678');
await ig.highlights.create('My Trip', ['story_id_1', 'story_id_2'], 'cover_media_id');
await ig.highlights.edit('highlight_id', 'Updated Title', ['new_story_id']);
await ig.highlights.addStories('highlight_id', ['story_id_3', 'story_id_4']);
await ig.highlights.removeStories('highlight_id', ['story_id_1']);
await ig.highlights.updateCover('highlight_id', 'cover_media_id');
await ig.highlights.delete('highlight_id');
const upload = await ig.upload.photo({
file: fs.readFileSync('./photo.jpg'),
});
const post = await ig.upload.configurePhoto({
upload_id: upload.upload_id,
caption: 'sunset vibes',
usertags: {
in: [{ user_id: '12345', position: [0.5, 0.5] }]
}
});
console.log('Posted! PK:', post.media?.pk);
const upload = await ig.upload.video({
file: fs.readFileSync('./video.mp4'),
duration: 30,
width: 1080,
height: 1920,
});
const post = await ig.upload.configureVideo({
upload_id: upload.upload_id,
caption: 'check this clip',
duration: 30,
width: 1080,
height: 1920,
});
const reel = await ig.upload.configureToClips({
upload_id: upload.upload_id,
caption: 'my first reel',
duration: 15,
width: 1080,
height: 1920,
});
const story = await ig.upload.configureToStory({
upload_id: upload.upload_id,
});
const carousel = await ig.feed.uploadCarousel({
caption: 'summer trip highlights',
children: [
{ type: 'photo', file: fs.readFileSync('./pic1.jpg') },
{ type: 'photo', file: fs.readFileSync('./pic2.jpg') },
{ type: 'video', file: fs.readFileSync('./vid1.mp4'), duration: 10, width: 1080, height: 1080 },
]
});
const feed = await ig.timeline.getFeed();
feed.feed_items?.forEach(item => {
const media = item.media_or_ad;
if (media) console.log(media.user.username, '-', media.caption?.text?.substring(0, 40));
});
const tagFeed = await ig.feed.getTag('photography');
const locFeed = await ig.feed.getLocation('213385402');
const liked = await ig.feed.getLiked();
const saved = await ig.feed.getSaved();
const tray = await ig.feed.reelsTray();
const explore = await ig.feed.getExploreFeed();
const reels = await ig.feed.getReelsFeed();
const userReels = await ig.feed.getUserReelsFeed('25025320');
// get stories for multiple users at once
const reelsMedia = await ig.feed.reelsMedia(['25025320', '12345678']);
// get reels from your timeline
const reels = await ig.timeline.reels(10);
// explore reels
const exploreReels = await ig.timeline.exploreReels(10);
The REST-based DM methods. These work without MQTT — they're regular HTTP requests.
const inbox = await ig.direct.getInbox();
inbox.inbox.threads.forEach(t => {
console.log(t.thread_title || t.users[0]?.username, '- last:', t.last_permanent_item?.text);
});
// paginate
const page2 = await ig.direct.getInbox(inbox.inbox.oldest_cursor, 20);
const pending = await ig.direct.getPendingInbox();
const thread = await ig.direct.getThread(threadId);
thread.thread.items.forEach(msg => {
console.log(msg.user_id, ':', msg.text || `[${msg.item_type}]`);
});
There are multiple ways to send a text message via the REST API:
// Method 1: By username (auto-resolves user ID and thread)
await ig.direct.send({ to: 'target_username', message: 'hey there!' });
// Method 2: By user ID (skips username lookup)
await ig.direct.sendToUserId('25025320', 'hey there!');
// Method 3: By thread ID (for existing conversations / groups)
await ig.directThread.sendToGroup({ threadId: threadId, message: 'hey there!' });
// Method 4: Low-level broadcast (full control)
// Use threadIds to send to existing threads:
await ig.directThread.broadcast({
threadIds: [threadId],
item: 'text',
form: { text: 'hey there!' },
});
// Or use userIds to send to users directly (creates thread if needed):
await ig.directThread.broadcast({
userIds: ['25025320'],
item: 'text',
form: { text: 'hey there!' },
});
All convenience methods use to (Instagram username) to identify the recipient:
await ig.direct.sendImage({
to: 'target_username',
imagePath: './photo.jpg',
});
await ig.direct.sendVideo({
to: 'target_username',
videoPath: './video.mp4',
});
await ig.direct.sendLink({
to: 'target_username',
text: 'check this out',
urls: ['https://example.com'],
});
await ig.direct.sendMediaShare({ to: 'target_username', mediaId: '3193593212003331660' });
await ig.direct.sendProfile({ to: 'target_username', profileUserId: '25025320' });
await ig.direct.sendHashtag({ to: 'target_username', hashtag: 'photography' });
await ig.direct.sendLocation({ to: 'target_username', locationId: '213385402' });
const group = await ig.direct.createGroupThread(['user_id_1', 'user_id_2'], 'Project Team');
console.log('Thread ID:', group.thread_id);
const recipients = await ig.direct.rankedRecipients('raven', 'john');
const presence = await ig.direct.getPresence();
await ig.direct.markAsSeen(threadId, itemId);
await ig.direct.hideThread(threadId);
Send disappearing photos and videos in DMs. These use the REST endpoint, no MQTT needed. The helpers handle the full flow — upload to rupload.facebook.com/messenger_image/ + broadcast via /direct_v2/threads/broadcast/raven_attachment/ — in a single call.
const { sendRavenPhotoOnce, sendRavenPhotoReplayable, sendRavenVideoOnce } = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
// view-once photo (disappears after one view)
const photoBuffer = fs.readFileSync('./secret.jpg');
await sendRavenPhotoOnce(ig, photoBuffer, {
threadId: threadId
});
// replayable photo (can replay, still disappears)
await sendRavenPhotoReplayable(ig, photoBuffer, {
threadId: threadId
});
// view-once video
const videoBuffer = fs.readFileSync('./secret_clip.mp4');
await sendRavenVideoOnce(ig, videoBuffer, {
threadId: threadId,
duration: 8,
width: 720,
height: 1280
});
await ig.friendship.create('25025320'); // follow
await ig.friendship.destroy('25025320'); // unfollow
const status = await ig.friendship.show('25025320');
console.log('Following:', status.following);
console.log('Followed by:', status.followed_by);
console.log('Blocking:', status.blocking);
// bulk check
const many = await ig.friendship.showMany(['12345', '67890']);
await ig.friendship.approve('25025320');
await ig.friendship.ignore('25025320');
// list pending requests
const pending = await ig.friendship.getPendingRequests();
await ig.friendship.block('25025320');
await ig.friendship.unblock('25025320');
const blocked = await ig.friendship.getBlockedUsers();
await ig.friendship.restrict('25025320');
await ig.friendship.unrestrict('25025320');
await ig.friendship.mute('25025320', { muteStories: true, mutePosts: true });
await ig.friendship.unmute('25025320', { unmuteStories: true, unmutePosts: true });
// add someone to close friends
await ig.friendship.setCloseFriend('25025320', true);
// remove
await ig.friendship.setCloseFriend('25025320', false);
// bulk update close friends list
await ig.friendship.setBesties(['user1', 'user2'], ['user3']); // add, remove
await ig.friendship.setFavorite('25025320');
await ig.friendship.unsetFavorite('25025320');
const favorites = await ig.friendship.getFavoriteFriends();
await ig.friendship.removeFollower('25025320');
const followers = await ig.friendship.getFollowers('25025320');
const following = await ig.friendship.getFollowing('25025320');
const mutual = await ig.friendship.getMutuafFollowers('25025320');
Instagram's unified search endpoint. Covers users, hashtags, places, and music.
const results = await ig.fbsearch.topSearch('coffee shop');
console.log('Users:', results.users?.length);
console.log('Places:', results.places?.length);
console.log('Hashtags:', results.hashtags?.length);
// flat version (simpler output)
const flat = await ig.fbsearch.topSearchFlat('coffee', 30);
const users = await ig.fbsearch.searchUsers('johndoe', 20);
const hashtags = await ig.fbsearch.searchHashtags('photography', 20);
const places = await ig.fbsearch.searchPlaces('new york');
const music = await ig.fbsearch.searchMusic('trending');
const recent = await ig.fbsearch.getRecentSearches();
// clear it
await ig.fbsearch.clearRecentSearches();
// manually add something to recent searches
await ig.fbsearch.registerRecentSearch('25025320', 'user');
const suggested = await ig.fbsearch.getSuggestedSearches('users');
const nullState = await ig.fbsearch.nullStateDynamic();
const explore = await ig.explore.topicalExplore({
module: 'explore_popular',
cluster_id: 'explore_all:0',
});
// basic explore
const basic = await ig.explore.explore();
// paginate
const page2 = await ig.explore.explore(basic.next_max_id);
await ig.explore.reportExploreMedia('3193593212003331660', 1);
await ig.explore.markAsSeen();
Instagram Notes — the little status messages that show up in the DM tab.
// get all notes from people you follow
const notes = await ig.note.getNotes();
const followingNotes = await ig.note.getNotesFollowing();
// create a note (audience: 0 = followers, 1 = close friends)
await ig.note.createNote('currently building something cool', 0);
// delete your note
await ig.note.deleteNote('note_id_here');
// mark notes as seen
await ig.note.lastSeenUpdateNote();
Requires a business or creator account.
// account-level insights
const accountInsights = await ig.insights.account({
ig_drop_table: 'is_feed',
follower_type: 'followers',
timeframe: 'one_week',
query_params: JSON.stringify({ access_token: '', id: '' })
});
// insights for a specific post
const mediaInsights = await ig.insights.media('3193593212003331660');
// reel insights
const reelInsights = await ig.insights.reelInsights('3193593212003331660');
// story insights
const storyInsights = await ig.insights.storyInsights('3193593212003331660');
// all media feed insights
const allMedia = await ig.insights.mediaFeedAll({ count: 20 });
Fine-grained control over push notification settings.
// mute all notifications for 8 hours
await ig.notification.muteAll('8_hour');
// disable all notifications
await ig.notification.disableAll();
// control individual notification types
await ig.notification.likes('off');
await ig.notification.comments('off');
await ig.notification.newFollower('off');
await ig.notification.commentLikes('off');
await ig.notification.directShareActivity('off');
await ig.notification.login('off');
await ig.notification.reminders('off');
// and many more: userTagged, firstPost, followRequestAccepted,
// connection, taggedInBio, pendingDirectShare, directGroupRequests,
// fundraiserSupporter, announcements, reportUpdated...
// search for music
const tracks = await ig.track.search('trending pop');
// get track info
const trackInfo = await ig.track.infoById('track_id_here');
const trackByCanonical = await ig.track.infoByCanonicalId('canonical_id');
// download audio
const audioBuffer = await ig.track.downloadByUrl('https://instagram.cdnurl.com/...');
Programmatic account creation flow.
// check if email/username is available
const emailCheck = await ig.signup.checkEmail('user@example.com');
const usernameCheck = await ig.signup.checkUsername('desired_username');
// get signup config
const config = await ig.signup.getSignupConfig();
// check age eligibility
await ig.signup.checkAgeEligibility(1995, 6, 15);
// send verification email and confirm
await ig.signup.sendVerifyEmail('user@example.com');
await ig.signup.checkConfirmationCode('user@example.com', '123456');
// phone-based signup
await ig.signup.sendSignupSmsCode('+1234567890');
await ig.signup.validateSignupSmsCode('+1234567890', '123456');
// get username suggestions
const suggestions = await ig.signup.getSuggestedUsernames('John Doe', 'john@example.com');
// create the account
const newAccount = await ig.signup.accountsCreate({
username: 'johndoe_2026',
password: 'securepassword123',
email: 'john@example.com',
first_name: 'John',
});
const family = await ig.multipleAccounts.getAccountFamily();
const featured = await ig.multipleAccounts.getFeaturedAccounts();
const info = await ig.multipleAccounts.getAccountInfo();
// switch to another logged-in account
await ig.multipleAccounts.switchAccount('other_user_id');
// get fundraiser info
const info = await ig.fundraiser.standaloneFundraiserInfo('fundraiser_pk');
// create a charity fundraiser
const fundraiser = await ig.fundraiser.createCharityFundraiser({
title: 'Help Local School',
description: 'Raising funds for supplies',
charity_id: 'charity_pk',
goal_amount: 5000,
});
// donate
await ig.fundraiser.donateFundraiser('fundraiser_pk', 25);
// get challenge form (when Instagram shows a captcha)
const form = await ig.captcha.getChallengeForm('/api/v1/challenge/1234/');
// submit reCAPTCHA or hCaptcha response
await ig.captcha.submitRecaptchaResponse('/api/v1/challenge/1234/', 'recaptcha_token');
await ig.captcha.submitHCaptchaResponse('/api/v1/challenge/1234/', 'hcaptcha_token');
// decode a share code (from QR codes, NFC tags, etc.)
const info = ig.share.shareInfo('base64_encoded_code');
// returns { type: 'user', pk: '25025320' }
// parse from URL
const fromUrl = ig.share.shareInfoByUrl('https://www.instagram.com/share/abc123');
// extract share code from URL
const code = ig.share.shareCodeFromUrl('https://www.instagram.com/share/abc123');
Low-level access to Instagram's Bloks framework. Used internally by some flows.
await ig.bloks.action({
action_name: 'some.action.name',
params: { key: 'value' }
});
const layout = await ig.bloks.getLayoutData({
layout_name: 'layout.name',
params: {}
});
// bloks-based password change
await ig.bloks.changePassword('old_pass', 'new_pass');
Here's every repository and what it covers at a glance.
| Repository | Access | What it does |
|---|---|---|
ig.account | Account management | Login, 2FA, edit profile, change password, privacy |
ig.user | User operations | Info, search, follow, block, mute, get medias/reels/stories |
ig.media | Media operations | Like, comment, pin, delete, save, archive, download |
ig.clip | Reels / Clips | Upload, discover, download reels |
ig.story | Stories | Upload, view, react, highlights |
ig.highlights | Highlights | Create, edit, delete, manage cover |
ig.feed | Content feeds | Timeline, hashtag, location, saved, liked, explore |
ig.timeline | Timeline reels | Reels feed, explore reels |
ig.upload | Upload & configure | Photo/video upload, configure to feed/story/clips |
ig.direct | Direct messages | Inbox, send text/media/links, raven (view-once), group threads |
ig.friendship | Relationships | Follow, block, restrict, close friends, favorites |
ig.fbsearch | Search | Users, hashtags, places, music, search history |
ig.explore | Explore page | Topical explore, report, mark seen |
ig.insights | Analytics | Account, media, reel, story insights |
ig.note | Notes | Create, delete, view notes |
ig.notification | Notification settings | Enable/disable per-type notifications |
ig.totp | 2FA management | TOTP setup, SMS 2FA, backup codes |
ig.challenge | Challenge resolver | Auto-resolve, verify methods, security codes |
ig.signup | Account creation | Email/phone verification, username check, create account |
ig.track | Music / Audio | Search tracks, get info, download |
ig.share | Share codes | Decode QR/NFC share codes, parse URLs |
ig.bloks | Bloks engine | Low-level Instagram UI actions |
ig.fundraiser | Fundraisers | Create, donate, get info |
ig.multipleAccounts | Multi-account | Switch accounts, account family |
ig.captcha | Captcha handling | reCAPTCHA / hCaptcha submission |
ig.location | Locations | Location search and info |
ig.hashtag | Hashtags | Hashtag info and feed |
ig.news | Activity feed | Activity inbox |
ig.collection | Collections | Saved collections management |
ig.closeFriends | Close friends | Close friends list management |
All three clients (RealtimeClient, FbnsClient, MQTToTClient) emit a connection.update event whenever the connection state changes. Each client also exposes an ev property that is an alias for itself, so both client.on(...) and client.ev.on(...) work identically.
Event shape:
{
connection: 'connecting' | 'open' | 'close';
lastDisconnect?: {
error: BoomError; // @hapi/boom wrapped error
date: Date;
};
isNewLogin?: boolean; // true on the first successful connect (RealtimeClient only)
}
Usage with RealtimeClient:
const { RealtimeClient } = require('nodejs-insta-private-api-mqt');
const realtime = new RealtimeClient(ig);
// Both forms work identically:
realtime.on('connection.update', (update) => {
const { connection, lastDisconnect } = update;
if (connection === 'connecting') {
// attempting to connect
} else if (connection === 'open') {
console.log('Connected');
} else if (connection === 'close') {
const statusCode = lastDisconnect?.error?.output?.statusCode;
const message = lastDisconnect?.error?.message;
console.log('Disconnected:', statusCode, message);
}
});
// Using .ev alias:
realtime.ev.on('connection.update', ({ connection }) => {
console.log('State:', connection);
});
await realtime.connect({ /* options */ });
Usage with FbnsClient:
const fbns = ig.fbns; // or new FbnsClient(ig)
fbns.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'open') {
console.log('FBNS connected');
} else if (connection === 'close') {
console.log('FBNS disconnected:', lastDisconnect?.error?.message);
}
});
Inspecting the Boom error:
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'close' && lastDisconnect) {
const err = lastDisconnect.error;
console.log(err.message); // human-readable message
console.log(err.output.statusCode); // HTTP status code (503, 408, etc.)
console.log(err.isBoom); // always true
console.log(lastDisconnect.date.toISOString()); // when the disconnect happened
}
});
DisconnectReason — structured disconnect codes:
The library exports a DisconnectReason enum so you can branch on the precise cause without string-matching error messages:
const { DisconnectReason } = require('nodejs-insta-private-api-mqt');
realtime.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection !== 'close') return;
const code = lastDisconnect?.error?.output?.statusCode;
switch (code) {
case DisconnectReason.loggedOut:
// Credentials expired — re-login required
break;
case DisconnectReason.rateLimited:
// Too many requests — back off before reconnecting
break;
case DisconnectReason.connectionLost:
case DisconnectReason.networkError:
// Network drop — library will auto-reconnect
break;
case DisconnectReason.connectionClosed:
// You called disconnect() intentionally
break;
case DisconnectReason.reconnectFailed:
// All reconnect attempts exhausted
break;
case DisconnectReason.timedOut:
// MQTT keepalive / ping timeout
break;
case DisconnectReason.protocolError:
// Thrift/MQTT parse error
break;
case DisconnectReason.serverError:
// Instagram backend returned 5xx
break;
}
});
Full DisconnectReason reference:
| Key | HTTP status code | When it fires |
|---|---|---|
loggedOut | 401 | Auth / session invalid — re-login required |
rateLimited | 429 | Instagram rate-limited the connection |
connectionLost | 408 | Unexpected connection drop (no error info) |
connectionClosed | 428 | You called disconnect() intentionally |
timedOut | 504 | MQTT keepalive / ping failure |
networkError | 503 | Network-level failure (ECONNRESET, ETIMEDOUT…) |
protocolError | 500 | Thrift / MQTT protocol parse error |
serverError | 502 | Instagram backend returned a 5xx response |
reconnectFailed | 503 | All automatic reconnect attempts exhausted |
unknown | 500 | Unclassified error |
| Event | Description |
|---|---|
connection.update | Connection state changed (connecting / open / close) |
connected | MQTT connected |
disconnected | MQTT disconnected |
message | New message received |
message_live | Live message with parsed data |
typing | Typing indicator |
presence | User presence update |
error | Connection error |
warning | Non-fatal issue (payload errors, etc.) |
reconnected | Successfully reconnected after a drop |
reconnect_failed | All reconnect attempts exhausted |
auth_failure | 3+ consecutive authentication errors (credentials likely expired) |
These events provide deeper access to Instagram's MQTT protocol. They were added for full compatibility with the instagram_mqtt library.
| Event | Description |
|---|---|
realtimeSub | Raw realtime subscription data (all MQTT messages) |
direct | Direct message events with parsed data |
subscription | Legacy subscription event (backwards compatible) |
directTyping | When someone is typing in a DM thread |
appPresence | User online/offline status updates |
directStatus | DM thread status changes |
liveWave | Instagram Live wave notifications |
liveRealtimeComments | Real-time comments on Instagram Live |
liveTypingIndicator | Typing indicator in Live comments |
mediaFeedback | Media engagement feedback |
clientConfigUpdate | Client configuration updates |
The realtimeSub event gives you access to all raw MQTT messages. This is useful for debugging or implementing custom message handling:
realtime.on('realtimeSub', ({ data, topic }) => {
console.log('Raw MQTT data received:', data);
console.log('Topic:', topic);
});
The direct event provides parsed direct message updates with automatic JSON parsing of nested values:
realtime.on('direct', (data) => {
console.log('Direct update:', data);
// data.op contains the operation type (e.g., 'add', 'replace', 'remove')
// data.path contains the affected path
// data.value contains the parsed message data
if (data.op === 'add' && data.value) {
console.log('New message:', data.value.text);
}
});
These events are automatically emitted when Instagram sends specific subscription updates:
// Listen for typing indicators
realtime.on('directTyping', (data) => {
console.log('User is typing:', data);
});
// Listen for presence updates (online/offline status)
realtime.on('appPresence', (data) => {
console.log('Presence update:', data);
});
// Listen for DM status changes
realtime.on('directStatus', (data) => {
console.log('Direct status changed:', data);
});
// Listen for Instagram Live comments
realtime.on('liveRealtimeComments', (data) => {
console.log('Live comment:', data);
});
A full bot that uses every major event — connection lifecycle, messages, typing, presence, and raw MQTT data for debugging. This is a good starting template to copy and trim down to what you actually need.
const {
IgApiClient,
RealtimeClient,
useMultiFileAuthState,
GraphQLSubscriptions,
DisconnectReason,
} = require('nodejs-insta-private-api-mqt');
async function startAdvancedBot() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState('./auth_info_ig');
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
if (auth.hasSession()) {
await auth.loadCreds(ig);
const valid = await auth.isSessionValid(ig).catch(() => false);
if (!valid) {
const result = await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
}
} else {
const result = await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
}
const realtime = new RealtimeClient(ig);
const userId = ig.state.cookieUserId;
// Connection lifecycle — fires on every state change
realtime.ev.on('connection.update', ({ connection, lastDisconnect, isNewLogin }) => {
if (connection === 'connecting') {
console.log('[CONN] Connecting...');
} else if (connection === 'open') {
console.log(`[CONN] Connected${isNewLogin ? ' (new session)' : ''}`);
} else if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
const msg = lastDisconnect?.error?.message;
if (code === DisconnectReason.loggedOut) {
console.error('[CONN] Session invalid — need to re-login');
process.exit(1);
} else if (code === DisconnectReason.rateLimited) {
console.warn('[CONN] Rate limited, backing off...');
} else if (code === DisconnectReason.reconnectFailed) {
console.error('[CONN] All reconnect attempts failed:', msg);
process.exit(1);
} else {
console.warn(`[CONN] Disconnected (${code}): ${msg}`);
}
}
});
realtime.on('reconnected', ({ attempt }) => {
console.log(`[CONN] Reconnected on attempt #${attempt}`);
});
// Standard parsed message
realtime.on('message_live', (msg) => {
if (msg?.text) {
console.log(`[MSG] ${msg.username || msg.userId}: "${msg.text}"`);
}
});
// Full message with raw data (catches all item types)
realtime.on('message', async (data) => {
const msg = data.message || data.parsed;
if (!msg?.thread_id) return;
const threadId = msg.thread_id;
const text = msg.text?.toLowerCase().trim();
if (text === 'ping') {
await realtime.directCommands.sendText({ threadId, text: 'pong!' });
}
});
// Direct message patch operations (add/replace/remove on thread items)
realtime.on('direct', (data) => {
if (data.op === 'add') {
console.log('[DIRECT] New item in thread');
} else if (data.op === 'remove') {
console.log('[DIRECT] Item removed from thread');
}
});
// Typing indicators
realtime.on('directTyping', (data) => {
console.log('[TYPING] Someone is typing...');
});
// User presence (online/offline)
realtime.on('appPresence', (data) => {
console.log('[PRESENCE] User status changed:', data?.user_id);
});
// Raw MQTT — useful for debugging unknown events
realtime.on('realtimeSub', ({ data, topic }) => {
// Only uncomment this when debugging — it's very noisy
// console.log('[RAW MQTT]', topic, JSON.stringify(data).substring(0, 100));
});
// Connect with full subscription set
await realtime.connect({
graphQlSubs: [
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
GraphQLSubscriptions.getAppPresenceSubscription(),
GraphQLSubscriptions.getDirectStatusSubscription(),
userId && GraphQLSubscriptions.getAsyncAdSubscription(userId),
GraphQLSubscriptions.getClientConfigUpdateSubscription(),
].filter(Boolean),
});
console.log('[BOT] All event listeners active. Send a DM to test.');
process.on('SIGINT', () => {
realtime.disconnect();
process.exit(0);
});
await new Promise(() => {});
}
startAdvancedBot().catch(console.error);
Media files (photos, videos) are always uploaded via HTTP rupload first. MQTT only sends metadata/references, never the raw bytes. The sendPhoto() and sendVideo() methods handle this automatically. Raven (view-once) media uses a different upload path — rupload.facebook.com/messenger_image/ instead of rupload_igphoto/ — and then broadcasts via /direct_v2/threads/broadcast/raven_attachment/. The sendRavenPhoto() / sendRavenVideo() helpers handle the entire flow.
Instagram has strict rate limits. Add delays between rapid-fire messages to avoid temporary bans.
Always save your session after login to avoid repeated logins which can trigger verification.
This update focuses on long-term MQTT stability. If you've been getting random disconnections or rate limit bans, most of that should be gone now. Here's what changed and how to use the new stuff.
IgApiClientExt with exportState() / importState() for dead-simple session save/restorewithRealtime(), withFbns(), withFbnsAndRealtime() to set up clients in one lineGraphQLSubscriptions, SkywalkerSubscriptions, QueryIDs now exported for custom subscription setupslisten() now returns an unsubscribe functionFbnsClient fully working out of the box (no extra packages needed)MQTToTClient, MQTToTConnection, mqttotConnectFlow exported for advanced use415.0.0.36.76, Samsung Galaxy S24 device profileThe old error handler just retried everything with the same delay. The new one actually looks at what went wrong and reacts differently depending on the error type.
| Type | Triggers On | Base Delay | Max Delay | Multiplier |
|---|---|---|---|---|
rate_limit | "too many requests", "action blocked", 429 | 60s | 10 min | 1.5x |
auth_failure | "login_required", "checkpoint", 401, 403 | 10s | 2 min | 2x |
network | ECONNRESET, ETIMEDOUT, DNS failures | 2s | 1 min | 2x |
server | 500, 502, 503 | 5s | 2 min | 2x |
protocol | Thrift parse errors, CONNACK issues | 5s | 1 min | 2x |
All delays include random jitter (0-2s) to prevent multiple bots from hammering the server at the same time.
The cleanest way to handle all error scenarios is through connection.update with DisconnectReason. But there are also a few standalone error events that fire in specific situations:
const { IgApiClient, RealtimeClient, DisconnectReason } = require('nodejs-insta-private-api-mqt');
const ig = new IgApiClient();
const realtime = new RealtimeClient(ig);
// The main lifecycle hook — every disconnect comes through here with a reason code
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection !== 'close') return;
const code = lastDisconnect?.error?.output?.statusCode;
if (code === DisconnectReason.loggedOut) {
// Session is dead — only a fresh login can fix this
console.log('Session expired, logging in again...');
// re-login logic here
} else if (code === DisconnectReason.rateLimited) {
console.log('Rate limited — the library is backing off automatically');
} else if (code === DisconnectReason.reconnectFailed) {
console.log('All reconnect attempts exhausted');
process.exit(1);
}
});
// Fires after 3+ consecutive auth failures — session is almost certainly dead
realtime.on('auth_failure', ({ count, error }) => {
console.log(`Auth failed ${count} times in a row: ${error}`);
console.log('Session expired — need to log in again');
});
// Fires for unexpected low-level errors
realtime.on('error', (err) => {
console.error('MQTT error:', err?.message || err);
});
// Non-fatal issues — payload parse errors, unknown topics, etc.
// Good to log for debugging, usually not actionable
realtime.on('warning', (w) => {
console.warn('Warning:', w?.message || w);
});
The error handler keeps a rolling history of the last 50 errors. Handy for dashboards or diagnostics:
// after connecting...
const stats = realtime.errorHandler.getErrorStats();
console.log('Total errors:', stats.errorCount);
console.log('Can still retry:', stats.canRetry);
console.log('Currently rate limited:', stats.isRateLimited);
console.log('Rate limit expires in:', stats.rateLimitRemainingMs, 'ms');
console.log('Breakdown by type:', stats.typeBreakdown);
// e.g. { network: 2, rate_limit: 1 }
The ReconnectManager works together with the error handler. When the connection drops, it doesn't just blindly retry - it checks what kind of error caused the disconnect and adjusts the backoff.
All delays include up to 30% jitter so if you're running multiple bots they don't all reconnect at the exact same second.
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'open') {
console.log('Back online');
}
if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
// DisconnectReason.reconnectFailed (503) means all attempts are used up
if (code === DisconnectReason.reconnectFailed) {
console.log('Could not reconnect after all attempts');
// restart the process, send yourself a notification, etc.
}
}
});
realtime.on('reconnected', ({ attempt }) => {
console.log(`Back online after ${attempt} attempt(s)`);
});
realtime.on('reconnect_failed', ({ attempts, lastErrorType }) => {
console.log(`Gave up after ${attempts} attempts. Last error type: ${lastErrorType}`);
});
You don't need to configure any of this - it's all automatic. The RealtimeClient uses the ErrorHandler + ReconnectManager for high-level reconnection, while MQTToTClient has its own low-level reconnect loop for transport-level drops. They work independently but cover different failure scenarios.
If you're coming from instagram_mqtt, you might be used to withRealtime() and exportState(). We've got those now.
const { IgApiClientExt } = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
const ig = new IgApiClientExt();
// first run - login and save
await ig.login({ username: 'myuser', password: 'mypass' });
const state = await ig.exportState();
fs.writeFileSync('saved_state.json', state);
// next run - restore from file
const ig2 = new IgApiClientExt();
const saved = fs.readFileSync('saved_state.json', 'utf8');
await ig2.importState(saved);
// ig2 is now logged in, no need to call login() again
exportState() serializes the entire client state (cookies, tokens, device info) through a hook system. You can add your own hooks if you need to persist additional data:
ig.addStateHook({
name: 'myCustomData',
onExport: async (client) => {
return { lastSyncTime: Date.now(), someConfig: 'value' };
},
onImport: async (data, client) => {
console.log('Restoring custom data:', data);
// do whatever you need with the restored data
}
});
const { IgApiClient, withRealtime } = require('nodejs-insta-private-api-mqt');
const ig = new IgApiClient();
// this upgrades ig to IgApiClientExt and adds a lazy .realtime property
const client = withRealtime(ig);
await client.login({ username: 'myuser', password: 'mypass' });
// .realtime is created lazily - only when you first access it
client.realtime.on('message_live', (msg) => {
console.log(`${msg.username}: ${msg.text}`);
});
await client.realtime.startRealTimeListener();
// and since it's an IgApiClientExt, you can export the full state
const state = await client.exportState();
Note: withRealtime() doesn't create the RealtimeClient immediately. It sets up a lazy getter, so the client is only instantiated when you first access client.realtime. No surprise connections.
Attach FBNS (Facebook Notification Service) for push notifications. The FBNS auth state is automatically included when you call exportState(), so you don't need to re-authenticate every time you restart:
const { IgApiClient, withFbns } = require('nodejs-insta-private-api-mqt');
const ig = new IgApiClient();
const client = withFbns(ig);
// client.fbns is now available
// FBNS auth state is automatically included in exportState()
const { IgApiClient, withFbnsAndRealtime } = require('nodejs-insta-private-api-mqt');
const ig = new IgApiClient();
const client = withFbnsAndRealtime(ig);
// client.realtime -> RealtimeClient (lazy)
// client.fbns -> FbnsClient (ready to use, no extra packages needed)
// client.exportState() / client.importState() -> full state persistence
All the with* functions work on existing IgApiClient instances. They use Object.setPrototypeOf internally, so your existing event listeners, state, and references are all preserved - nothing gets copied or lost.
const ig = new IgApiClient();
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
await ig.login({ username: 'user', password: 'pass' });
// this is safe - ig keeps all its internal state
const upgraded = withRealtime(ig);
// upgraded === ig (same object, just with extra methods now)
Ever sent 3 messages in a row and they showed up in the wrong order on Instagram? That's because MQTT doesn't guarantee delivery order. This library fixes that automatically.
When RealtimeClient starts up, it wraps directCommands methods with per-thread queues (using p-queue with concurrency: 1). So if you do:
await realtime.directCommands.sendText({ threadId: thread, text: 'Hey' });
await realtime.directCommands.sendText({ threadId: thread, text: 'How are you?' });
await realtime.directCommands.sendText({ threadId: thread, text: 'Check this out' });
They will always arrive in that exact order on Instagram's side, even if some of them take longer to deliver than others. This happens transparently - you don't need to write any extra code.
The queue is per-thread, so messages to different threads can still go out in parallel. Only messages within the same thread are serialized.
Methods that are queued: sendText, sendLink, sendPhoto, sendVideo, sendVoice, sendLike, sendPost
These are now exported so you can build custom subscription configurations instead of relying on the defaults.
const {
GraphQLSubscriptions,
SkywalkerSubscriptions,
QueryIDs
} = require('nodejs-insta-private-api-mqt');
// GraphQLSubscriptions has factory methods for each subscription type
const directSub = GraphQLSubscriptions.getDirectTypingSubscription('your_user_id');
const presenceSub = GraphQLSubscriptions.getAppPresenceSubscription();
// SkywalkerSubscriptions works the same way
const liveSub = SkywalkerSubscriptions.directSub('your_user_id');
// QueryIDs contains the numeric IDs Instagram uses internally
// useful if you need to match incoming messages to subscription types
console.log(QueryIDs);
const realtime = new RealtimeClient(ig);
await realtime.connect({
graphQlSubs: [
GraphQLSubscriptions.getDirectTypingSubscription(ig.state.userId),
GraphQLSubscriptions.getAppPresenceSubscription(),
],
skywalkerSubs: [
SkywalkerSubscriptions.directSub(ig.state.userId),
],
irisData: inbox
});
The low-level listen() method on MQTToTClient now returns a function you can call to stop listening. This prevents memory leaks when you only need to listen temporarily.
// listen with a topic config object (applies transformer)
const remove = realtime.mqtt.listen(
{ topic: '/ig_message_sync', transformer: myTransformer },
(data) => {
console.log('Got transformed data:', data);
}
);
// or listen to a raw topic string (no transformer, just raw messages)
const removeRaw = realtime.mqtt.listen('/ig_send_message_response', (msg) => {
console.log('Raw message on topic:', msg.payload);
});
// later, when you're done:
remove();
removeRaw();
FbnsClient handles Facebook Notification Service - the push notification system that Instagram uses under the hood. Even when the official app is "in the background", FBNS is what delivers those new follower, like, comment, and DM notifications. This library now has full FBNS support built in - no extra packages needed.
For most DM bot use cases you don't need FBNS because RealtimeClient already handles real-time message delivery. But if you want to receive push-style notifications for things like new followers, comments on your posts, story mentions, or live broadcast alerts - FBNS is the way to go. You can also run both at the same time.
const { IgApiClient, FbnsClient, useMultiFileAuthState } = require('nodejs-insta-private-api-mqt');
async function startFbns() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState('./auth_session');
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
// login or restore session
if (auth.hasSession()) {
await auth.loadCreds(ig);
} else {
await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
}
// create the FBNS client and wire up events
const fbns = new FbnsClient(ig);
// Connection lifecycle — same pattern as RealtimeClient
fbns.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'connecting') {
console.log('FBNS connecting...');
} else if (connection === 'open') {
console.log('FBNS connected, push notifications active');
} else if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
const msg = lastDisconnect?.error?.message;
console.log(`FBNS disconnected (${code}): ${msg}`);
}
});
fbns.on('push', (notification) => {
console.log('Got push notification!');
console.log('Title:', notification.title);
console.log('Message:', notification.message);
console.log('Type:', notification.collapseKey);
});
// connect - this handles everything: TLS handshake, device auth,
// token registration, and push subscription
await fbns.connect();
console.log('FBNS connected and listening for push notifications');
// keep the process alive
await new Promise(() => {});
}
startFbns().catch(console.error);
That's it. Once connect() resolves, you're registered for push notifications and the push event will fire whenever Instagram sends one.
| Event | Payload | When it fires |
|---|---|---|
push | { title, message, collapseKey, igAction, pushId, pushCategory, badgeCount } | New push notification received |
auth | { clientId, deviceId, userId, deviceSecret } | Device authenticated with FBNS servers |
message | Raw FBNS message payload | Any FBNS message (low-level) |
logging | Experiment/logging data from Instagram | Instagram sends telemetry config |
error | Error object | Connection or protocol error |
warning | Warning info | Non-fatal issues (empty payloads, etc.) |
disconnect | Disconnect reason string | FBNS connection dropped |
The collapseKey field tells you what kind of notification it is. Here are the common ones:
| collapseKey | What it means |
|---|---|
direct_v2_message | New DM received |
direct_v2_group | New group DM |
follow | Someone followed you |
like | Someone liked your post |
comment | Someone commented on your post |
mention | Someone mentioned you |
story_mention | Someone mentioned you in their story |
live_broadcast | Someone you follow went live |
story_reshare | Someone reshared your story |
comment_like | Someone liked your comment |
tagged_in_photo | Someone tagged you in a photo |
fbns.on('push', (notification) => {
switch (notification.collapseKey) {
case 'direct_v2_message':
console.log('New DM! Badge:', notification.badgeCount?.direct);
// notification.igAction contains the thread URL you can parse
// e.g. "direct_v2?id=34028236...&x=32658200..."
break;
case 'follow':
console.log('New follower!', notification.message);
break;
case 'like':
case 'comment':
console.log('Engagement:', notification.message);
break;
case 'live_broadcast':
console.log('Someone went live:', notification.message);
break;
default:
console.log(`[${notification.collapseKey}]`, notification.message || notification.title);
}
});
You can run both FBNS (push notifications) and RealtimeClient (DM messaging) side by side. They use different MQTT connections - Realtime connects to edge-mqtt.facebook.com for DMs, while FBNS connects to mqtt-mini.facebook.com for push notifications.
const {
IgApiClient,
RealtimeClient,
FbnsClient,
useMultiFileAuthState,
GraphQLSubscriptions,
DisconnectReason,
} = require('nodejs-insta-private-api-mqt');
async function startBot() {
const ig = new IgApiClient();
const auth = await useMultiFileAuthState('./auth_session');
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
if (auth.hasSession()) {
await auth.loadCreds(ig);
const valid = await auth.isSessionValid(ig).catch(() => false);
if (!valid) {
const result = await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
}
} else {
const result = await ig.login({ username: 'your_username', password: 'your_password' });
await auth.saveCreds(ig);
}
const userId = ig.state.cookieUserId;
// Realtime MQTT for DMs
const realtime = new RealtimeClient(ig);
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'open') {
console.log('[REALTIME] Connected');
} else if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
if (code === DisconnectReason.loggedOut) {
console.error('[REALTIME] Session expired — re-login required');
process.exit(1);
} else {
console.warn(`[REALTIME] Disconnected (${code}), reconnecting...`);
}
}
});
realtime.on('message_live', (msg) => {
if (msg?.text) {
console.log(`[DM] ${msg.username}: ${msg.text}`);
}
});
await realtime.connect({
graphQlSubs: [
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
GraphQLSubscriptions.getAppPresenceSubscription(),
GraphQLSubscriptions.getDirectStatusSubscription(),
].filter(Boolean),
});
// FBNS for push notifications
const fbns = new FbnsClient(ig);
fbns.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'open') {
console.log('[FBNS] Connected, push notifications active');
} else if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
console.warn(`[FBNS] Disconnected (${code}): ${lastDisconnect?.error?.message}`);
}
});
fbns.on('push', (notif) => {
console.log(`[PUSH] [${notif.collapseKey}]`, notif.message || notif.title);
});
await fbns.connect();
console.log('Both Realtime and FBNS are running!');
process.on('SIGINT', async () => {
realtime.disconnect();
await fbns.disconnect();
process.exit(0);
});
await new Promise(() => {});
}
startBot().catch(console.error);
If you're using IgApiClientExt for state management, withFbns() automatically includes the FBNS auth state in exportState() / importState(). This means you don't have to re-authenticate with FBNS servers every time you restart.
const { IgApiClient, withFbnsAndRealtime } = require('nodejs-insta-private-api-mqt');
const fs = require('fs');
const ig = new IgApiClient();
const client = withFbnsAndRealtime(ig);
// first run - login and save everything
await client.login({ username: 'user', password: 'pass' });
const state = await client.exportState();
fs.writeFileSync('full_state.json', state);
// next run - restore from file, FBNS auth included
const saved = fs.readFileSync('full_state.json', 'utf8');
await client.importState(saved);
// client.fbns is ready to connect with saved device credentials
client.fbns.on('push', (notif) => console.log('Push:', notif.message));
await client.fbns.connect();
await fbns.connect({
autoReconnect: true, // auto-reconnect on disconnect (default: true)
enableTrace: false, // enable MQTT packet tracing for debugging
});
// graceful disconnect
await fbns.disconnect();
// if you need to reconnect later
await fbns.connect();
For anyone curious about what happens under the hood:
FbnsClient creates a TLS connection to mqtt-mini.facebook.com:443 using Instagram's MQTToT protocol (MQTT over Thrift)deviceId, clientId, and deviceSecret for this deviceThe whole flow takes about 1-2 seconds. The registration response can arrive very fast (sometimes before connect() even returns), so the library sets up the response listener before connecting to avoid any timing issues.
For advanced users who want to work directly with Instagram's MQTT-over-Thrift protocol:
const {
MQTToTClient,
MQTToTConnection,
mqttotConnectFlow,
INSTAGRAM_VERSION
} = require('nodejs-insta-private-api-mqt');
console.log('Current Instagram version:', INSTAGRAM_VERSION);
// '415.0.0.36.76'
// MQTToTClient extends mqtts.MqttClient with Instagram-specific behavior:
// - automatic keepalive pinging (every 8 min + jitter)
// - consecutive ping failure detection (3 fails = forced reconnect)
// - reconnect loop with rate limit awareness
// - listen() with cleanup function
// MQTToTConnection is used to build the Thrift connection payload
// that Instagram expects in the CONNECT packet
// mqttotConnectFlow creates the CONNECT/CONNACK handshake flow
// with keepAlive: 60 and proper error handling
The previous version had 4 overlapping timers sending various keepalive signals, which generated a lot of unnecessary traffic. Instagram could potentially flag this as suspicious bot behavior. This has been streamlined:
| Timer | Old Value | New Value | Purpose |
|---|---|---|---|
| Foreground pulse | 15s | 60s + 0-5s jitter | Tells Instagram the app is in foreground |
| GraphQL sync | 45s | 5 min + 0-10s jitter | Refreshes subscription data |
| Traffic watchdog | 60s check / 60s threshold | 60s check / 5 min threshold | Detects dead connections, tries ping before reconnecting |
| MQTT heartbeat | 15s | 4 min + 0-5s jitter | MQTT-level ping to keep the TCP connection alive |
| MQTToT keepalive | 10 min | 8 min + 0-30s jitter | Low-level protocol keepalive |
| Active query | 25s | Disabled | Was redundant with the above |
The result is roughly 10x less keepalive traffic, which means less chance of Instagram flagging your connection as automated.
Send disappearing photos and videos in DMs using Instagram's raven protocol:
sendRavenPhoto() in EnhancedDirectCommands — full upload + REST broadcast flow (accessible via directCommands)sendRavenVideo() in EnhancedDirectCommands — same flow for video with duration/dimensionsbroadcastRaven() in DirectThreadRepository — low-level REST broadcast to /direct_v2/threads/broadcast/raven_attachment/sendmedia.sendRavenPhoto() / sendmedia.sendRavenVideo() — standalone helpers that work without MQTTephemeralMediaViewMode: 0) and replayable (ephemeralMediaViewMode: 1)direct_v2: '1' flag to mark media as DM content32 REST API Repositories — full coverage of every Instagram private endpoint:
New repositories added:
ClipRepository - Upload, configure, discover, and download Reels/ClipsTimelineRepository - Timeline reels feed and explore reelsInsightsRepository - Account, media, reel, and story insights for business/creator accountsNoteRepository - Create, delete, and view Instagram NotesNotificationRepository - Fine-grained per-type notification settingsSignupRepository - Full account creation flow with email/SMS verificationTOTPRepository - TOTP 2FA setup, SMS 2FA, backup codesBloksRepository - Low-level Bloks engine actionsChallengeRepository - Auto-resolve challenges, verify methods, security codesShareRepository - Decode QR/NFC share codes, parse share URLsTrackRepository - Music/audio search, info, downloadExploreRepository - Topical explore, report, mark seenFBSearchRepository - Unified search (users, hashtags, places, music), search historyFundraiserRepository - Create, donate, get fundraiser infoMultipleAccountsRepository - Account family, switch accountsCaptchaRepository - reCAPTCHA / hCaptcha submission for challengesHighlightsRepository - Create, edit, delete highlights, manage stories and coversEnhanced existing repositories:
AccountRepository - Added editProfile, setBiography, setExternalUrl, removeBioLinks, setGender, setPrivate/Public, profilePictureChange/Remove, passwordPublicKeys, sendRecoveryFlowEmail/Sms, encryptPasswordUserRepository - Added getUserMedias/getUserReels/getUserClips with pagination, userIdFromUsername, usernameFromUserId, block/unblock, mute/unmute, removeFollower, getBlockedUsers, getMutualFollowersMediaRepository - Added pinComment/unpinComment, bulkDeleteComments, replyToComment, likeComment/unlikeComment, save/unsave, archive/unarchive, disableComments/enableComments, commentThreadComments, downloadPhoto/downloadVideo, oembed, static mediaPkFromCode/mediaCodeFromPk/mediaPkFromUrlFriendshipRepository - Added restrict/unrestrict, setCloseFriend, setBesties, getFavoriteFriends, setFavorite/unsetFavorite, getPendingRequests, getMutuafFollowers, getBlockedUsersFeedRepository - Added uploadCarousel, reelsMedia, reelsTray, getReelsFeed, getUserReelsFeed, getExploreFeedStoryRepository - Added createHighlight/editHighlight/deleteHighlight, uploadVideo, configureStoryVideo, reactDirectRepository - Added sendPhoto/sendVideo/sendLink/sendMediaShare/sendProfile/sendHashtag/sendLocation, getPendingInbox, createGroupThread, hideThreadUploadRepository - Added configureToClips, configurePhoto, configureVideo, configureToStoryAll 32 repositories registered in client.js and exported from index.js.
FbnsClient now fully working out of the box:
mqtt-shim.js bridge module so FBNS no longer requires any external MQTT packagescreateFbnsUserAgent, notUndefined, listenOnce) that FBNS depends onmqtt-mini.facebook.com:443 via MQTToT, handles device auth, token registration, and push deliverypush, auth, message, logging, error, warning, disconnectwithFbns() / withFbnsAndRealtime() includes FBNS device auth in exportState()SessionHealthMonitor (native in RealtimeClient):
/api/v1/accounts/current_user/ (every 30min + jitter)health_check, session_expired, relogin_start, relogin_success, relogin_failed, relogin_challenge, relogin_neededPersistentLogger (native in RealtimeClient):
getRecentLines(count) for quick debuggingIntegration:
realtime.enableHealthMonitor({ credentials, autoRelogin: true }) - start monitoringrealtime.enablePersistentLogger({ logDir: './logs' }) - start file loggingrealtime.getHealthStats() - get uptime/session statsrealtime.getLoggerStats() - get logger statsstartRealTimeListener() optionsSessionHealthMonitor and PersistentLogger exported from indexUsage:
const realtime = new RealtimeClient(ig);
// Option A: Auto via startRealTimeListener
await realtime.startRealTimeListener({
credentials: { username: 'user', password: 'pass' },
enablePersistentLogger: true,
logDir: './mqtt-logs',
});
// Option B: Manual enable
realtime.enableHealthMonitor({
credentials: { username: 'user', password: 'pass' },
autoRelogin: true,
checkIntervalMs: 30 * 60 * 1000,
});
realtime.enablePersistentLogger({ logDir: './mqtt-logs' });
// Monitor events
realtime.on('health_check', ({ status, stats }) => {
console.log(`Session ${status}, uptime: ${stats.totalUptimeHuman}`);
});
realtime.on('session_expired', () => console.log('Session expired!'));
realtime.on('relogin_success', () => console.log('Re-logged in!'));
// Get stats anytime
const stats = realtime.getHealthStats();
console.log(`Uptime: ${stats.uptimePercent}%, ${stats.reconnects} reconnects`);
14 New MQTT Topics:
/ig_msg_dr - Delivery receipts (emits deliveryReceipt)/ig_conn_update - Connection status updates (emits connectionUpdate)/notify_disconnect - Server-initiated disconnects (emits notifyDisconnect)/t_thread_typing - Per-thread typing indicators (emits threadTyping)/iris_server_reset - Iris state reset with auto-resubscribe (emits irisServerReset)/t_ig_family_navigation_badge - Badge/notification counts (emits badgeCount)/t_entity_presence - User presence status (emits entityPresence)/opened_thread - Thread open tracking (emits threadOpened)/buddy_list - Online buddy list (emits buddyList)/webrtc, /webrtc_response, /onevc - Call events (emits callEvent)GraphQL Live Subscriptions:
liveComment, liveLikeCount, liveWave, liveTyping, liveViewerCount, mediaFeedback, directTyping, appPresence, callStateChange, liveInteractivity, graphqlEventPubsub Event Handler:
direct)directTyping)pubsubEventNew REST API Repositories:
NewsRepository - Activity feed inbox, follow requests, mark as seenCollectionRepository - List, create, edit, delete saved collections; collection feedCloseFriendsRepository - List, add/remove besties, suggestionsEnhanced MediaRepository:
replyToComment(mediaId, commentId, text) - Reply to specific commentslikeComment(mediaId, commentId) / unlikeComment() - Comment likesbulkDeleteComments(mediaId, commentIds[]) - Bulk comment deletionsave(mediaId, collectionId?) / unsave(mediaId) - Save/unsave mediaarchive(mediaId) / unarchive(mediaId) - Archive/unarchive mediadisableComments(mediaId) / enableComments(mediaId) - Toggle commentscommentThreadComments(mediaId, commentId) - Get reply threadEnhanced UserRepository:
getFriendshipStatuses(userIds[]) - Bulk friendship status checkgetReelsTrayFeed() - Stories tray feedgetUserTags(userId) - Tagged photos feedsetSelfBio(biography) - Update own biographyreport(userId, reason) - Report usergetSuggested() - Suggested users feedExports:
NewsRepository, CollectionRepository, CloseFriendsRepository, MediaRepository, UserRepository now exported from indexMIT
For issues, bugs, or feature requests: https://github.com/Kunboruto20/nodejs-insta-private-api/issues
Documentation: https://github.com/Kunboruto20/nodejs-insta-private-api
The EnhancedDirectCommands class (see enhanced.direct.commands.js) implements robust location sending for Instagram Direct by trying story-with-location-sticker → share story to thread → fallback to link. Below are ready-to-copy examples showing how to use the location-related methods and payloads exposed by the class.
These examples assume:
realtimeis an instance ofRealtimeClient.realtime.directCommandsis an instance ofEnhancedDirectCommands.- You have a valid
threadId(target DM thread).realtime.igexists when examples require publishing a story via the private IG client.
venue object (recommended)// venue shape expected by sendLocation:
// { id, name, address, lat, lng, facebook_places_id, external_source }
const venue = {
id: "213385402",
name: "McDonald's Unirii",
address: "Piața Unirii, Bucharest",
lat: 44.4268,
lng: 26.1025,
facebook_places_id: "213385402",
external_source: "facebook_places"
};
await realtime.directCommands.sendLocation({
threadId: "340282366841710300949128114477782749726",
venue,
text: "Meet me here at 18:00"
});
What happens:
realtime.ig.publish.story.storyId is returned, sendUserStory (reel_share) is used to share the story to the thread.https://www.instagram.com/explore/locations/{placeId}/ via itemType: 'link'.Use searchAndSendLocation() when you only have a search query or coordinates:
await realtime.directCommands.searchAndSendLocation({
threadId: "340282366841710300949128114477782749726",
query: "Starbucks Piata Unirii",
lat: 44.4268,
lng: 26.1025
});
This helper calls Instagram's /fbsearch/places/ private endpoint (via realtime.ig.request) and then normalizes the first result into the venue shape before calling sendLocation().
If you want direct control over the sticker object or to publish your own image, you can use createLocationStickerFromVenue() and realtime.ig.publish.story() directly:
const venue = {
id: "213385402",
name: "McDonald's Unirii",
address: "Piața Unirii, Bucharest",
lat: 44.4268,
lng: 26.1025,
facebook_places_id: "213385402"
};
// create sticker compatible with publish.story helpers
const sticker = realtime.directCommands.createLocationStickerFromVenue(venue);
// create a tiny placeholder image (1x1 PNG) or your real photo buffer
const SINGLE_PIXEL_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=";
const photoBuffer = Buffer.from(SINGLE_PIXEL_PNG_BASE64, 'base64');
// publish story with the sticker (if realtime.ig.publish.story exists)
const publishResult = await realtime.ig.publish.story({
file: photoBuffer,
stickers: [sticker]
});
// try to resolve returned story id and then share it
const storyId = publishResult?.media?.pk || publishResult?.item_id || publishResult?.upload_id;
if (storyId) {
await realtime.directCommands.sendUserStory({
threadId: "340282366841710300949128114477782749726",
storyId,
text: "Location for tonight"
});
} else {
// fallback: send explore link manually
await realtime.directCommands.sendLink({
threadId: "340282366841710300949128114477782749726",
link: `https://www.instagram.com/explore/locations/${venue.facebook_places_id || venue.id}/`,
text: venue.name
});
}
If you don't want to publish a story and only need the location link in DM:
const placeId = "213385402";
await realtime.directCommands.sendLink({
threadId: "340282366841710300949128114477782749726",
link: `https://www.instagram.com/explore/locations/${placeId}/`,
text: "Meet here"
});
try {
await realtime.directCommands.sendLocation({ threadId, venue, text: "See you" });
console.log("Location sent!");
} catch (err) {
console.error("Failed to send location:", err);
// fallback to explicit link if needed
await realtime.directCommands.sendLink({
threadId,
link: `https://www.instagram.com/explore/locations/${venue.facebook_places_id || venue.id}/`,
text: venue.name || "Location"
});
}
If you need verbose logs, enable debug for the realtime/enhanced module:
# in your environment (example)
DEBUG="realtime:enhanced-commands" node your_bot.js
# or to see broader realtime logs
DEBUG="realtime:*" node your_bot.js
realtime.ig must exist and expose publish.story(...) (a private client publish helper).venue must include either facebook_places_id or id.realtime.ig.publish.story is unavailable or Instagram rejects the sticker, the library automatically falls back to sending a DM link to the Explore locations page.These examples are appended to the original README in order to keep the whole original file intact while adding clear, English examples for location flows.
FAQs
Complete Instagram MQTT protocol with full-featured REALTIME and REST API — all in one project.
We found that nodejs-insta-private-api-mqt demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.