InstantGram
MCP server that enables AI agents to interact with Instagram DMs by managing sessions, sending messages, and configuring webhooks through the InstantGram API.
README
InstantGram
A TypeScript Node.js Instagram DM API server with webhook capabilities, an interactive + JSON CLI, an OpenAPI 3.1 spec, and an MCP server for AI agents.
⚠️ Disclaimer: This project uses the unofficial
instagram-private-apilibrary, which is not affiliated with or endorsed by Instagram/Meta. Automating Instagram accounts may violate their Terms of Service and can lead to temporary or permanent account restrictions. Use at your own risk — use only with accounts you own, and rate-limit yourself sensibly.
Features
- Multi-session management — create, login, restore, delete, and export sessions in memory, persisted to disk
- Login with 2FA — detects two-factor requirements and completes them via a verification code; handles checkpoints (auto-solve attempt + manual challenge URL fallback)
- Custom User-Agent & proxy support (http/https/socks5)
- Messaging API — send text and media (image/video) DMs to a username or numeric user id
- User lookup — resolve usernames to user ids
- Webhook system — poll the inbox for new DMs and POST HMAC-SHA256-signed events to your URL, with retries and exponential backoff on rate limits
- Events:
message.received,message.sent,session.error,webhook.test - AI-friendly surfaces —
/openapi.json(full OpenAPI 3.1 spec),/llms.txt+/llms-full.txtdocs, and a one-shot--jsonCLI mode - MCP server — Model Context Protocol tools over stdio (
npm run mcp) or Streamable HTTP (/mcp), so AI assistants can create sessions, send DMs, and manage webhooks - Interactive CLI — create/login sessions, run the full send-message wizard, configure webhooks, watch live webhook events, export/import sessions
- Session persistence — serialized states (cookies + device) saved to
./sessions/*.session.jsonand auto-loaded on boot
Installation
# Node.js >= 18 required
npm install
# Configure environment
cp .env.example .env
# Start the API server (development, with hot reload)
npm run dev
# Or production:
npm run build && npm start
# Interactive CLI (in a second terminal):
npm run cli
Environment variables (.env)
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP port for the API server |
WEBHOOK_SECRET |
— | Secret used to sign webhook payloads (X-Webhook-Signature header) |
DEFAULT_POLL_INTERVAL |
5000 |
Default inbox poll interval in ms |
LOG_LEVEL |
info |
debug | info | warn | error |
SESSION_DIR |
./sessions |
Directory for persisted session states |
HTTP_PROXY / per-session proxy |
— | Proxy for Instagram API traffic |
API_BASE_URL |
http://localhost:3000 |
Base URL the CLI, the JSON mode, and the stdio MCP server use to reach the API server |
AUTH_TOKEN |
— | Bearer auth — if set, every request except OPTIONS preflights and GET /health must send Authorization: Bearer <AUTH_TOKEN>. Opt-in kill-switch for locking down a public instance; CLI/JSON/stdio-MCP attach it automatically from the same .env |
INSTAGRAM_USERNAME / INSTAGRAM_PASSWORD |
— | Fallback credentials for npm run cli -- login (keeps secrets out of shell history) |
IG_APP_VERSION / IG_APP_VERSION_CODE |
347.0.0.25.106 / 441775334 |
Instagram app version fingerprint sent in the User-Agent. The library bundles an ancient version (222.x) that Instagram rejects with unsupported_version/checkpoint_required — bump these if sessions start getting flagged |
API Reference
All responses use a consistent envelope:
{
"success": true,
"data": { },
"meta": { "timestamp": "2026-08-09T12:00:00.000Z", "sessionId": "…" }
}
Errors return { "success": false, "error": "…", "meta": { … } }.
Bearer auth (AUTH_TOKEN)
If AUTH_TOKEN is set in .env, the server rejects every request (except CORS OPTIONS preflights and the GET /health liveness probe) unless it carries the header:
Authorization: Bearer <AUTH_TOKEN>
This is an opt-in kill-switch: set the variable to instantly lock down a publicly exposed instance with no code changes, unset it to return to open access. The interactive CLI, one-shot JSON mode, and stdio MCP bridge read AUTH_TOKEN from the same .env and send it automatically, so local workflows keep working. Unauthorized requests get 401 with the standard error envelope.
curl -s -X POST localhost:3000/api/session/create \
-H 'Authorization: Bearer <AUTH_TOKEN>' \
-H 'Content-Type: application/json' -d '{}'
AI-friendly surfaces
| Method | Route | Description |
|---|---|---|
GET |
/ |
JSON service index — points agents at the endpoints below |
GET |
/api |
Route-group index |
GET |
/health |
Liveness probe ({ status, uptime, sessions }) |
GET |
/openapi.json |
OpenAPI 3.1 document of every endpoint, schema, and error — the single machine-readable API contract |
GET |
/llms.txt |
LLM-friendly documentation index (llms.txt convention) |
GET |
/llms-full.txt |
Full markdown reference for LLM ingestion |
GET / POST / DELETE |
/mcp |
MCP server over Streamable HTTP (see below) |
Sessions
| Method | Route | Body | Description |
|---|---|---|---|
POST |
/api/session/create |
{ userAgent?, cookies?, proxy? } |
Create a session; returns { sessionId } |
POST |
/api/session/login |
{ sessionId, username, password } |
Log in. If 2FA is required returns data.twoFactor; if a checkpoint is hit returns data.challenge |
POST |
/api/session/2fa |
{ sessionId, code, verificationMethod? } |
Complete a pending 2FA login. For approval-based 2FA (verification method 2 — a push to your phone, no code) send code: "" after approving on the device. Pass verificationMethod when Instagram didn't report it |
POST |
/api/session/logout |
{ sessionId } |
Log out |
POST |
/api/session/restore |
{ data: SerializedSessionFile, preferredId? } |
Restore a session from exported JSON |
POST |
/api/session/restore-cookies |
{ cookies, userAgent?, proxy? } |
Create a session from raw browser cookies (see below) |
GET |
/api/session |
— | List all sessions with status |
GET |
/api/session/:id |
— | Single session details |
GET |
/api/session/:id/export |
— | Export serialized session (cookies + device) |
DELETE |
/api/session/:id |
— | Delete a session (stops polling too) |
Messages
| Method | Route | Body / Query | Description |
|---|---|---|---|
POST |
/api/message/send |
{ sessionId, recipient, text?, mediaBase64?, mediaType? } |
Send a DM. recipient may be a username or numeric user id. mediaBase64 is base64-encoded file data; mediaType is image | video (auto-detected from bytes if omitted) |
GET |
/api/user/lookup/:username |
?sessionId=… |
Resolve a username to a numeric user id |
Webhooks
| Method | Route | Body | Description |
|---|---|---|---|
POST |
/api/webhook/configure |
{ sessionId, url, secret?, events? } |
Set the delivery URL (and optional per-session signing secret) |
POST |
/api/webhook/start |
{ sessionId, intervalMs? } |
Start inbox polling (min 1000 ms) |
POST |
/api/webhook/stop |
{ sessionId } |
Stop polling |
POST |
/api/webhook/test |
{ sessionId } |
Send a synthetic webhook.test event through the full pipeline |
GET |
/api/webhook/status/:sessionId |
— | Polling state, URL, interval, last delivery |
GET |
/api/webhook/logs/:sessionId |
?limit=50 |
Recent delivery log entries |
Webhook payloads
Every event is POSTed to your URL as JSON:
{
"event": "message.received",
"timestamp": 1786298000000,
"sessionId": "8f0e9a2b-…",
"payload": {
"messageId": "340282366841710300949128155123748551874",
"threadId": "340282366841710300949128155123748551874",
"userId": "123456789",
"username": "someone",
"text": "Hey! 👋",
"itemType": "text"
}
}
| Field | Type | Description |
|---|---|---|
event |
string |
message.received | message.sent | session.error | webhook.test |
timestamp |
number |
Unix epoch ms |
sessionId |
string |
The session that produced the event |
payload.messageId |
string |
Instagram item id |
payload.threadId |
string |
Direct thread id |
payload.userId |
string |
The other party's user id |
payload.username |
string |
Their username (when known) |
payload.text |
string |
Message text (text items) |
payload.mediaUrl |
string |
CDN URL for media items (photo/video/voice/clip) |
payload.itemType |
string |
Raw Instagram item type |
payload.error |
string |
Error detail (session.error events) |
Signature verification
Each request carries an X-Webhook-Signature header:
X-Webhook-Signature: sha256=<hex-encoded HMAC-SHA256 of the raw request body>
X-Webhook-Event: message.received
The signature is computed with the WEBHOOK_SECRET (or the per-session secret configured via /api/webhook/configure).
Node.js receiver example:
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
const expected = `sha256=${crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex')}`;
if (!crypto.timingSafeEqual(
Buffer.from(req.get('x-webhook-signature')),
Buffer.from(expected),
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
console.log(`${event.event} from session ${event.sessionId}`, event.payload);
res.sendStatus(200);
});
Use
express.raw()(or read the raw body) so the signature covers the exact bytes that were POSTed — re-serializing parsed JSON will break verification.
Python receiver example:
import hashlib, hmac, json
from flask import Flask, request
app = Flask(__name__)
SECRET = b"your-webhook-secret-here"
@app.post("/webhook")
def webhook():
body = request.get_data()
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(request.headers.get("X-Webhook-Signature", ""), expected):
return {"error": "invalid signature"}, 401
print(json.loads(body))
return "ok", 200
Restoring a session from raw browser cookies
No username/password required — paste the cookies you can copy from your browser's DevTools (Application → Cookies → i.instagram.com). Only sessionid is mandatory; csrftoken is strongly recommended (state-changing requests like sending messages need it). The user id is derived from the first segment of the sessionid value when ds_user_id isn't provided.
curl -s -X POST localhost:3000/api/session/restore-cookies -H 'Content-Type: application/json' -d '{
"cookies": "sessionid=38499037450%3ABX1JtIMMfeCui8%3A10%3AAYhKmCtPNU97naqRsPuZpCUA49KbUT4SsWi2KDPUgg; csrftoken=abc123; ds_user_id=38499037450; mid=xyz",
"userAgent": "Instagram 276.0.0.24.105 Android (30/11; 420dpi; 1080x2340; samsung; SM-G991B; o1s; exynos2100; en_US; 319462287)"
}'
# → { "success": true, "data": { "sessionId": "…", "userId": "38499037450", "status": "logged_in" }, … }
A cookie-header string and a structured object are both accepted:
// structured form
{ "cookies": { "sessionid": "…", "csrftoken": "…", "ds_user_id": "…" } }
The server verifies the cookies in the background (account.currentUser()); if Instagram rejects them (expired/invalid/flagged), the session status flips to error with an explanatory message — check GET /api/session. Cookies are bound to the device fingerprint, so passing the matching userAgent you copied from the browser helps keep the session alive.
CLI usage
npm run cli
The CLI is a self-contained tester that talks to the running API server (start it with npm run dev). Menu map:
Main menu
├── Session Management
│ ├── Create new session (custom UA / cookies / proxy)
│ ├── Login to session (auto-detects 2FA and prompts for the code)
│ ├── Complete 2FA login
│ ├── Restore session from JSON file
│ ├── Restore session from raw cookies
│ ├── List active sessions
│ ├── Logout from session
│ └── Delete session
├── Send Message (select session → recipient → text/media → confirm)
├── Webhook Management
│ ├── Configure webhook URL (URL + optional secret + event filter)
│ ├── Start polling (custom interval)
│ ├── Stop polling
│ ├── Send test webhook
│ ├── View webhook status
│ ├── View webhook logs
│ └── Watch live webhook events (press any key to return)
├── Utilities
│ ├── Lookup user ID from username
│ ├── Export session to file
│ └── Import session from file
└── Exit
One-shot JSON mode (AI / scripting friendly)
npm run cli -- <command> [--flag value ...] runs a single command and prints exactly one JSON document to stdout — nothing else — so output pipes straight into jq, shells, and AI tooling:
npm run cli -- list-sessions
# → {"success":true,"data":[{"id":"…","status":"logged_in",…}]}
npm run cli -- send-message --session-id <id> --recipient friend --text "hi"
# → {"success":true,"data":{"messageId":"…","threadId":"…","recipientUserId":"…","itemType":"text"}}
Errors print { "success": false, "error": "…" } and exit with code 1. Credentials for login can come from INSTAGRAM_USERNAME / INSTAGRAM_PASSWORD to keep secrets out of shell history.
Commands: health, create-session, login, complete-2fa, logout, restore-session (--file), restore-cookies, list-sessions, session-info, export-session, delete-session, send-message, lookup-user, webhook-configure, webhook-start, webhook-stop, webhook-test, webhook-status, webhook-logs, help. Run npm run cli -- help for per-command flags.
MCP server (Model Context Protocol)
InstantGram speaks MCP so AI assistants can drive it directly — create/login sessions, send DMs, look up users, manage webhooks. Every REST endpoint is exposed as a tool, and the docs are available as resources.
Tools: create_session, login, complete_2fa, logout, restore_session, restore_cookies, list_sessions, session_info, export_session, delete_session, send_message, lookup_user, webhook_configure, webhook_start, webhook_stop, webhook_test, webhook_status, webhook_logs, health
Resources: instantgram://openapi.json, instantgram://llms-full.txt
stdio (Claude Desktop, Cursor, …)
The MCP server bridges to the running API server via API_BASE_URL — start the API with npm run dev first, then:
npm run mcp
Register it in claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"instantgram": {
"command": "npm",
"args": ["run", "mcp"],
"cwd": "/absolute/path/to/instantgram"
}
}
}
Prefer a compiled entry? npm run build then point command at node dist/mcp.js.
HTTP (Streamable HTTP)
The API server hosts MCP at /mcp (CORS-enabled; stateful sessions use the Mcp-Session-Id header):
GET http://localhost:3000/mcp # initialize / SSE handshake
POST http://localhost:3000/mcp # JSON-RPC messages
DELETE http://localhost:3000/mcp # end the session
Point any MCP client (MCP Inspector, web apps, remote assistants) at http://localhost:3000/mcp.
Quick start (end-to-end)
# Terminal 1 — run the server
cp .env.example .env
npm install
npm run dev
# Terminal 2 — run the CLI
npm run cli
# From the CLI:
# 1. Session Management → Create new session → Login to session
# 2. (if 2FA) → enter code
# 3. Webhook Management → Configure webhook URL → Start polling
# 4. Send Message → pick session → recipient → text → confirm
# 5. Webhook Management → Watch live webhook events
Alternatively, drive everything with curl:
# Create a session
curl -s -X POST localhost:3000/api/session/create -H 'Content-Type: application/json' \
-d '{"userAgent":"Instagram 347.0.0.25.106 Android (30/11; 420dpi; 1080x2340; samsung; SM-G991B; o1s; exynos2100; en_US; 441775334)"}'
# → {"success":true,"data":{"sessionId":"<id>"},...}
# Login
curl -s -X POST localhost:3000/api/session/login -H 'Content-Type: application/json' \
-d '{"sessionId":"<id>","username":"myaccount","password":"hunter2"}'
# Send a DM to a username
curl -s -X POST localhost:3000/api/message/send -H 'Content-Type: application/json' \
-d '{"sessionId":"<id>","recipient":"some.friend","text":"Hello from InstantGram!"}'
# Look up a user id
curl -s "localhost:3000/api/user/lookup/some.friend?sessionId=<id>"
# Configure a webhook and start polling
curl -s -X POST localhost:3000/api/webhook/configure -H 'Content-Type: application/json' \
-d '{"sessionId":"<id>","url":"https://your-server.example.com/webhook"}'
curl -s -X POST localhost:3000/api/webhook/start -H 'Content-Type: application/json' \
-d '{"sessionId":"<id>","intervalMs":5000}'
# Send media (base64 of a jpg) — use mediaType image/video
IMG_B64=$(base64 -i photo.jpg | tr -d '\n')
curl -s -X POST localhost:3000/api/message/send -H 'Content-Type: application/json' \
-d "{\"sessionId\":\"<id>\",\"recipient\":\"some.friend\",\"mediaBase64\":\"$IMG_B64\",\"mediaType\":\"image\"}"
2FA methods
Instagram's two_factor_info.verification_method tells you which challenge was triggered:
| Method | Type | What to do |
|---|---|---|
0 |
SMS | Enter the code texted to your phone |
1 |
Authenticator app (TOTP) | Enter the 6-digit code from your app |
2 |
Approval from another device | A push notification appears on your logged-in phone — tap "It was me", then complete with an empty code (code: "") |
3 |
Backup codes | Enter one of your one-time backup codes |
The CLI detects method 2 automatically: it tells you to approve on your phone and completes the login without asking for a code.
⏱️ Approval pushes are short-lived (usually one or two minutes). Approve on your phone and confirm in the CLI promptly — if it expires, just run Login to session again to trigger a fresh push. Tapping a stale notification from an earlier login attempt always shows "expired"; use the one from the current attempt. Pending challenges older than 10 minutes are auto-cleared.
# Approval-based 2FA via the API (verificationMethod: 2 = approval push)
curl -s -X POST localhost:3000/api/session/2fa -H 'Content-Type: application/json' \
-d '{"sessionId":"<id>","code":"","verificationMethod":"2"}'
Session persistence
After a successful login (or 2FA completion), the session's serialized state — cookies, device fingerprint, and constants — is written to SESSION_DIR (default ./sessions) as <sessionId>.session.json.
On server boot, every file in that directory is loaded back into memory, so sessions stay logged in across restarts without re-entering credentials.
- Export:
GET /api/session/:id/exportor CLI Utilities → Export session to file. The exported JSON can be moved to another machine and restored withPOST /api/session/restoreor CLI Import session from file. - Rotation: If Instagram invalidates a session (e.g. password change), delete the session and its file, then log in again.
🔒 Treat
sessions/like passwords — it contains live auth cookies. It is git-ignored by default.
How it works
- Instagram has no public DM webhooks — this server polls
ig.feed.directInbox()on a configurable interval and diffs new item ids against a per-session seen-set. - New incoming items become
message.receivedevents; outbound API sends becomemessage.sentevents; poll failures becomesession.errorevents. - Each event is signed with HMAC-SHA256 and delivered with up to 3 retries (1s/2s/4s backoff). On rate-limit or network errors the poll interval backs off exponentially up to 5 minutes.
- Sessions are isolated in memory (
Map<sessionId, InstagramService>), each with its own device fingerprint, cookies, proxy, and webhook config.
Error classification
Instagram errors are normalized into categories that map to HTTP statuses and retry behavior:
| Category | Trigger | API response |
|---|---|---|
network |
Connection/parse failures | 502 |
auth |
Bad password, invalid user, logged out | 401 |
rate_limit |
Action spam / too many requests | 429 (+ poll backoff) |
not_found |
searchExact miss |
404 |
challenge |
Checkpoint / unsupported version | 403 (with solve URL) |
two_factor |
Login-time flows | handled inline |
Why you might see
checkpoint_required/unsupported_version: the bundledinstagram-private-apireports an app version from ~2021 (222.0.0.13.114). Instagram flags that fingerprint. This project patches the version to347.0.0.25.106by default (overridable viaIG_APP_VERSION/IG_APP_VERSION_CODE, or derived from your custom User-Agent). If Instagram tightens checks again, bump those env values.
Project structure
src/
├── index.ts # Express server entry (wires services + routes)
├── cli.ts # Inquirer.js interactive CLI
├── cli-json.ts # One-shot JSON mode (AI / scripting friendly)
├── mcp.ts # MCP server entry (stdio bridge to the REST API)
├── openapi.ts # OpenAPI 3.1 document (served at /openapi.json)
├── types/index.ts # All shared interfaces
├── services/
│ ├── InstagramService.ts # IgApiClient wrapper (auth, messaging, polling)
│ ├── SessionManager.ts # Multi-session registry + disk persistence
│ └── WebhookService.ts # Polling, HMAC delivery, retries, logs
├── routes/
│ ├── session.ts # /api/session/*
│ ├── message.ts # /api/message/*, /api/user/lookup/*
│ └── webhook.ts # /api/webhook/*
├── mcp/
│ ├── apiClient.ts # Envelope-aware REST client (shared by MCP + JSON CLI)
│ ├── tools.ts # MCP tool definitions (one per REST endpoint)
│ └── http.ts # Streamable HTTP transport mounted at /mcp
└── utils/
├── api.ts # Response envelope helpers
├── crypto.ts # HMAC-SHA256 sign/verify
└── logger.ts # Leveled, colored logging
Scripts
| Script | Purpose |
|---|---|
npm run dev |
Run the API server with tsx watch (hot reload) |
npm run build |
Compile TypeScript to dist/ |
npm start |
Run the compiled server |
npm run cli |
Launch the interactive CLI |
npm run cli -- <cmd> |
One-shot JSON mode (AI / scripting friendly) |
npm run mcp |
Start the MCP server over stdio (requires the API server) |
npm run typecheck |
Type-check without emitting |
License
MIT
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.