Skip to main content
Glama
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