InstantGram
README.md
# 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-api`](https://github.com/dilame/instagram-private-api) library, 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.txt` docs, and a one-shot `--json` CLI 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.json` and auto-loaded on boot
---
## Installation
```bash
# 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:
```jsonc
{
"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.
```bash
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 `POST`ed to your URL as JSON:
```jsonc
{
"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:**
```js
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:**
```python
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.
```bash
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:
```jsonc
// 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
```bash
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:
```bash
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:
```bash
npm run mcp
```
Register it in `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
```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)
```bash
# 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:
```bash
# 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.
```bash
# 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/export` or CLI *Utilities → Export session to file*. The exported JSON can be moved to another machine and restored with `POST /api/session/restore` or 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
1. **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.
2. New incoming items become `message.received` events; outbound API sends become `message.sent` events; poll failures become `session.error` events.
3. 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.
4. 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 bundled `instagram-private-api` reports an app version from ~2021 (`222.0.0.13.114`). Instagram flags that fingerprint. This project patches the version to `347.0.0.25.106` by default (overridable via `IG_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
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessUnresponsive