npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@msgly/instagram

v1.4.0

Published

Instagram Direct adapter for Msgly

Readme

@msgly/instagram

Instagram Direct adapter for Msgly. Send and receive Instagram DMs through the unified hub — text, image, video, quick replies, reactions. Zero classes, runs in Node, Next.js, and Edge runtimes.

📖 Docs & channel reference: https://ayushjain070401.github.io/msgly/

Install

npm install @msgly/core @msgly/instagram

Quick start

import express from 'express';
import { createHub } from '@msgly/core';
import { createInstagramAdapter } from '@msgly/instagram';

const hub = createHub();

hub.register(
  createInstagramAdapter({
    pageAccessToken: process.env.INSTAGRAM_PAGE_TOKEN!,
    appSecret: process.env.META_APP_SECRET!,
    verifyToken: process.env.META_VERIFY_TOKEN!,
  }),
);

await hub.connect({ throwOnFailure: true });

hub.on('message', async (msg) => {
  if (msg.channel === 'instagram' && msg.content.type === 'text') {
    await hub.send({
      channel: 'instagram',
      account: msg.account,
      contact: msg.contact,
      content: { type: 'text', text: `You said: ${msg.content.text}` },
    });
  }
});

const app = express();
app.use(express.json({ verify: (req, _r, buf) => ((req as any).rawBody = new Uint8Array(buf)) }));

const handlers = hub.createWebhookHandler();
app.get('/webhook/:channel', handlers.get);
app.post('/webhook/:channel', handlers.post);

app.listen(3000);

Config

interface InstagramConfig {
  /** IG-enabled Page access token (from Messenger → Instagram Settings, or via Instagram Login). */
  pageAccessToken: string;

  /** App secret — used for X-Hub-Signature-256 verification and token exchanges. */
  appSecret: string;

  /** Your chosen string for the webhook GET handshake. */
  verifyToken: string;

  /**
   * Facebook App ID (from App Dashboard → General Information → App ID).
   * Required only when using the Instagram Login OAuth helpers.
   */
  appId?: string;

  /** Override for tests. Defaults to https://graph.facebook.com. */
  apiBase?: string;

  /** Graph API version. Defaults to v20.0. */
  apiVersion?: string;
}

Authentication

Two ways to obtain a pageAccessToken:

Option A — Facebook Login (traditional)

  1. Open your Meta App → Messenger → Instagram Settings → Generate Token.
  2. Copy the token and set INSTAGRAM_PAGE_TOKEN. This is a short-lived token (~60 days in dev mode). Exchange for a long-lived one via the token debug tool, or use a System User.

Option B — Instagram Login (new in 0.3.0)

Instagram Login uses api.instagram.com/graph.instagram.com endpoints and is the preferred approach for apps that need users to log in with their Instagram Business accounts:

import { createInstagramAdapter } from '@msgly/instagram';

const adapter = createInstagramAdapter({
  appId: process.env.INSTAGRAM_APP_ID!,
  appSecret: process.env.INSTAGRAM_APP_SECRET!,
  pageAccessToken: '', // placeholder — will be set after OAuth
  verifyToken: process.env.META_VERIFY_TOKEN!,
});

// 1. Redirect user here to grant permissions
app.get('/auth/instagram', (req, res) => {
  const url = adapter.getAuthUrl({
    appId: process.env.INSTAGRAM_APP_ID!,
    redirectUri: 'https://myapp.example.com/auth/instagram/callback',
    scopes: ['instagram_business_basic', 'instagram_business_manage_messages'],
    state: req.session.csrf,
  });
  res.redirect(url);
});

// 2. Exchange the code for a short-lived token, then upgrade to long-lived
app.get('/auth/instagram/callback', async (req, res) => {
  const { code } = req.query;

  const short = await adapter.exchangeCode(code as string, {
    appId: process.env.INSTAGRAM_APP_ID!,
    appSecret: process.env.INSTAGRAM_APP_SECRET!,
    redirectUri: 'https://myapp.example.com/auth/instagram/callback',
  });

  const long = await adapter.getLongLivedToken(short.accessToken);
  // long.accessToken lasts ~60 days, long.expiresIn is in seconds

  // Persist long.accessToken and refresh before it expires:
  // await adapter.refreshToken(savedToken) — resets the 60-day clock
  res.json({ token: long.accessToken, expiresIn: long.expiresIn });
});

Token lifecycle:

  • Short-lived: exchangeCode() — ~1 hour
  • Long-lived: getLongLivedToken(short) — ~60 days
  • Refreshed: refreshToken(long) — resets to 60 days (call at least 24 h before expiry)

Setup (15 minutes)

Prerequisites:

  1. An Instagram Business or Creator account (Instagram app: Settings → Account → Switch to Professional Account).
  2. A Facebook Page linked to that Instagram account (Page Settings → Linked Accounts → Instagram).
  3. A Meta App with the Messenger product already added (same App used by @msgly/messenger). All three Meta channels share one App and one App Secret.

Steps:

  1. Add Instagram to your App. Meta App → Messenger → Instagram Settings tab → Add or Remove Pages → tick the Page linked to your IG account.
  2. Generate the IG-enabled token. Same Instagram Settings tab → Generate Token. Set as INSTAGRAM_PAGE_TOKEN.
  3. Subscribe webhooks. Webhooks section on the Instagram Settings tab:
    • Callback URL: <PUBLIC_URL>/webhook/instagram
    • Verify token: same META_VERIFY_TOKEN as Messenger / WhatsApp
    • Subscribe to messages
  4. Allow message access on the IG side. Instagram app: Settings → Privacy → Messages → "Allow access to messages" must be ON. Without this, your bot cannot receive DMs.
  5. Test. From a different IG account, send a DM to your Business account.

Capabilities

| Feature | Supported | | ------------- | --------- | | text | ✓ | | image | ✓ | | video | ✓ | | audio | — | | file | — | | location | ✓ | | buttons | — | | quick replies | ✓ | | templates | — | | reactions | ✓ | | typing | ✓ |

Instagram does not support audio sends, file attachments, or persistent buttons. Attempts throw a MsglyError with code: 'UnsupportedFeature':

import { isMsglyError } from '@msgly/core';

try {
  await hub.send({ channel: 'instagram', /* ... */ content: { type: 'audio', mediaRef } });
} catch (err) {
  if (isMsglyError(err, 'UnsupportedFeature')) {
    // Instagram does not support audio sends
  }
}

24-hour messaging window

Like Messenger, Instagram restricts free-form replies to the 24-hour standard messaging window after the user's last inbound message. Outside that window, only specific message tags (e.g. HUMAN_AGENT) or the Instagram Private Reply API may apply. For a chatbot, keep replies inside the 24-hour window or design proactive flows around the user initiating contact.

Sending examples

Image

await hub.send({
  channel: 'instagram',
  account, contact,
  content: {
    type: 'image',
    mediaRef: { kind: 'url', value: 'https://example.com/cat.jpg' },
  },
});

The URL must be public HTTPS.

Quick replies

await hub.send({
  channel: 'instagram',
  account, contact,
  content: {
    type: 'interactive',
    text: 'Pick one:',
    buttons: [
      { id: 'a', label: 'Option A' },
      { id: 'b', label: 'Option B' },
    ],
  },
});

Rendered as Instagram quick-reply chips. User tap → your message handler receives a text message whose content.text matches the chosen button's id.

Formatting

Instagram DMs are plain text — markdown and HTML are not rendered. The fmt export is provided so code that imports fmt from any adapter compiles uniformly; each helper returns the text unchanged:

import { fmt } from '@msgly/instagram';

fmt.bold('Hello')  // → 'Hello'
fmt.italic('world') // → 'world'
fmt.link('click', 'https://example.com') // → 'click'

Common pitfalls

  • Bot doesn't receive DMs: the IG Business account must have "Allow access to messages" ON (Instagram app → Settings → Privacy → Messages).
  • (#10) Application does not have permission: the IG account isn't linked to the FB Page, or the Page isn't connected in the Instagram Settings tab of the Meta App.
  • Webhook verify fails in console: META_VERIFY_TOKEN mismatch, or server unreachable at the public URL when you click Verify.
  • InvalidSignature: wrong appSecret, or your Express setup isn't capturing the raw body. The verify callback in express.json() is essential.
  • (#100) Invalid parameter on audio send: Instagram does not support audio. Catch UnsupportedFeature errors before sending, or check adapter.capabilities.media.audio === false.
  • Page token suddenly invalid: short-lived tokens expire (~60 days in dev mode). Exchange for a long-lived token via /oauth/access_token?grant_type=fb_exchange_token, or use a System User token in Live mode.

Documentation

Full setup walkthrough and multi-channel usage: https://github.com/AyushJain070401/msgly

Publishing a post

Feed posts are content publishing, not messaging. A post has no recipient, so it gets its own method rather than being forced through send():

const instagram = createInstagramAdapter({
  ...cfg,
  igUserId: process.env.IG_USER_ID!,   // the IG account id, NOT the Page id
});

await instagram.publishPost({
  imageUrl: 'https://cdn.acme.com/promo.jpg',
  caption: 'Spring sale is live 🌸',
});

// Reels
await instagram.publishPost({ videoUrl: 'https://cdn.acme.com/clip.mp4', isReel: true });

Three things that trip people up:

  • igUserId is not the Page id. Fetch it with GET /{page-id}?fields=instagram_business_account.
  • Instagram fetches the media itself, server-side. A presigned S3 URL works; a localhost URL never will. That is the most common container failure, and the thrown error says so.
  • The account must be Business or Creator, and Instagram caps you at 50 posts per rolling 24 hours.

Publishing is a two-step flow — create a container, then publish it — and both steps happen inside publishPost(). The returned containerId is there for debugging: a video container needs a few seconds of processing, so a publish immediately after upload can fail while the container itself is fine.

License

MIT