If you're adding Instagram DM features to your app, there are two paths. You can build directly against Meta's Graph API, which means managing OAuth flows, token refreshes, webhooks, and rate limits yourself. Or you can use Zernio's unified messaging API, which handles all of that and gives you DMs across 7 platforms from one endpoint. In this guide we will cover both approaches with working TypeScript code, so you can pick the right one for your build.
Table of contents
- What are the Instagram Messaging API requirements?
- How to integrate the Instagram Messaging API directly with Meta
- How to integrate Instagram messaging using Zernio
- Direct Meta API vs Zernio: which should you use?
- Frequently asked questions
What are the Instagram Messaging API requirements?
The Instagram Messaging API is part of Meta's Graph API ecosystem. It only works with Instagram Business or Creator accounts, and it requires a few moving parts before you can send or receive a single message.
| Requirement | Details |
|---|---|
| Account type | Instagram Business or Creator account |
| Facebook Page | Must be linked to a Facebook Page |
| Meta Developer App | Registered app at developers.facebook.com |
| Permission | instagram_business_manage_messages scope |
| App Review | Required for production access beyond 25 test users |
The instagram_business_manage_messages permission is the one that unlocks DM access. Without it, you can authenticate and make API calls, but the messages endpoint will return a permissions error. Make sure this permission is included in your OAuth scope list from the start.
For development and testing, you can work with up to 25 test users without going through App Review. For production access with real users at scale, you'll need to submit for Meta App Review.
How to integrate the Instagram Messaging API directly with Meta
This is the full-control approach. You're building directly against Meta's Graph API: managing your own OAuth flow, storing and refreshing tokens, and hosting your own webhook endpoint. It's the right choice if you're already deep in a Meta-only integration and don't need other platforms.
For the official API reference, see Meta's Messenger Platform documentation.
Step 1: Set up your Meta Developer App
- Go to developers.facebook.com and create a new app
- Select "Business" as the app type
- Add the "Instagram Graph API" product to your app
- Configure your OAuth redirect URIs
- Note your App ID and App Secret
// Environment variables needed for Instagram API integration
interface InstagramConfig {
clientId: string; // Meta App ID
clientSecret: string; // Meta App Secret
redirectUri: string; // Your OAuth callback URL
webhookVerifyToken: string; // For webhook verification
}
const config: InstagramConfig = {
clientId: process.env.INSTAGRAM_CLIENT_ID || '',
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET || '',
redirectUri: process.env.INSTAGRAM_REDIRECT_URI || '',
webhookVerifyToken: process.env.INSTAGRAM_WEBHOOK_VERIFY_TOKEN || '',
};
Note: Never commit API secrets to version control. Use environment variables or a secrets manager.
Step 2: How does Instagram Messaging API authentication work?
The API uses OAuth 2.0, meaning a two-step token exchange. Users authorize your app, you get a short-lived token, then exchange it for a long-lived token that lasts 60 days.
Step 2a: Generate the authorization URL
function getInstagramAuthUrl(state: string): string {
const scopes = [
'instagram_business_basic',
'instagram_business_content_publish',
'instagram_business_manage_messages', // Required for DMs
];
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: scopes.join(','),
response_type: 'code',
auth_type: 'rerequest', // Force re-permission prompt
state: state, // CSRF protection
});
return `https://www.instagram.com/oauth/authorize?${params.toString()}`;
}
Step 2b: Exchange the code for a long-lived token
After the user authorizes your app, Instagram redirects to your callback URL with a code parameter. Exchange it for a long-lived token immediately — short-lived tokens expire in 1 hour.
interface TokenResponse {
accessToken: string;
refreshToken: string;
userId: string;
expiresIn: number;
}
async function exchangeCodeForToken(
code: string
): Promise<TokenResponse> {
// Step 1: Exchange code for short-lived token
const tokenResponse = await fetch(
'https://api.instagram.com/oauth/access_token',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: config.clientId,
client_secret: config.clientSecret,
grant_type: 'authorization_code',
redirect_uri: config.redirectUri,
code: code,
}),
}
);
if (!tokenResponse.ok) {
const error = await tokenResponse.text();
throw new Error(`Token exchange failed: ${error}`);
}
const shortLivedData = await tokenResponse.json();
const shortLivedToken = shortLivedData.access_token;
const userId = shortLivedData.user_id;
// Step 2: Exchange for long-lived token (60 days)
const longLivedResponse = await fetch(
`https://graph.instagram.com/access_token?` +
`grant_type=ig_exchange_token&` +
`client_secret=${config.clientSecret}&` +
`access_token=${shortLivedToken}`,
{ method: 'GET' }
);
if (!longLivedResponse.ok) {
const error = await longLivedResponse.text();
throw new Error(`Long-lived token exchange failed: ${error}`);
}
const longLivedData = await longLivedResponse.json();
return {
accessToken: longLivedData.access_token,
refreshToken: longLivedData.access_token, // Instagram uses same token
userId: userId,
expiresIn: longLivedData.expires_in, // ~60 days in seconds
};
}
Refreshing tokens before they expire
Long-lived tokens expire after 60 days. Refresh them while they still have validity. If a token expires, your users need to re-authorize.
async function refreshAccessToken(
currentToken: string
): Promise<{ accessToken: string; expiresIn: number }> {
const response = await fetch(
`https://graph.instagram.com/refresh_access_token?` +
`grant_type=ig_refresh_token&` +
`access_token=${currentToken}`,
{ method: 'GET' }
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Token refresh failed: ${response.status} ${errorText}`
);
}
const data = await response.json();
if (data.error) {
throw new Error(
`Token refresh error: ${data.error.message}`
);
}
if (!data.access_token) {
throw new Error('Token refresh did not return an access_token');
}
return {
accessToken: data.access_token,
expiresIn: data.expires_in,
};
}
Note: A good rule: refresh when the token has fewer than 7 days remaining. Build a scheduled job that checks expiry dates daily.
Step 3. How do you set up webhooks for incoming Instagram messages?
To receive messages in real time, Instagram sends POST requests to your webhook endpoint whenever a user messages your business account. You need to verify the endpoint first, then handle the incoming events.
Webhook verification (GET)
Instagram sends a GET request to confirm your endpoint before activating it:
import { Request, Response } from 'express';
function handleWebhookVerification(
req: Request,
res: Response
): void {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === config.webhookVerifyToken) {
console.log('Webhook verified successfully');
res.status(200).send(challenge);
return;
}
console.error('Webhook verification failed');
res.status(403).send('Verification failed');
}
How do you send a DM using the Instagram Messaging API?
The send message endpoint is POST /{ig-user-id}/messages. You need your Instagram Business Account ID (igUserId) and the recipient's Instagram-scoped user ID. Scoped user IDs come from the webhook event when a user messages you first — you can't cold-message users who haven't initiated a conversation.
interface SendMessageOptions {
accessToken: string;
igUserId: string; // Your Instagram Business Account ID
recipientId: string; // The user's Instagram-scoped ID
text: string;
}
interface SendMessageResponse {
messageId: string;
}
async function sendTextMessage(
options: SendMessageOptions
): Promise<SendMessageResponse> {
const { accessToken, igUserId, recipientId, text } = options;
const response = await fetch(
`https://graph.instagram.com/${igUserId}/messages`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
recipient: {
id: recipientId,
},
message: {
text: text,
},
}),
}
);
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to send message: ${error}`);
}
const data = await response.json();
return { messageId: data.message_id };
}
What are the Instagram Messaging API rate limits?
| Limit type | Threshold | Window |
|---|---|---|
| API calls | 200 calls | Per hour per user token |
| Messages sent | Varies | Based on conversation history and account standing |
| Webhook response | Must respond within 20 seconds | Per incoming event |
The 24-hour messaging window is the most important limit to understand. Once a user messages you, you have 24 hours to reply freely. After that window closes, you can only send Human Agent messages for up to 7 days. You can't initiate cold outreach through the official API.
How to integrate Instagram messaging using Zernio
The direct Meta API approach works, but it's a real build. You're writing OAuth flows, scheduling token refreshes, hosting webhook infrastructure, and handling rate limit retries. That's appropriate if Instagram DMs are the entire product. For most teams adding Instagram messaging as one feature among many, it's a significant maintenance burden.
Zernio's messaging API handles the OAuth, token refresh, webhook normalization, and rate limiting for you. Here's the same send-and-receive flow with Zernio:
await fetch(
"https://zernio.com/api/v1/inbox/conversations/CONV_ID/messages",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
accountId: "ACC_ID",
message: "Hey! How can I help you today?"
})
}
);
Token management is automatic. Zernio refreshes tokens before they expire, so you don't need the scheduled refresh job. Webhook verification is handled on Zernio's end — you get normalized events regardless of which platform they came from.
Webhook event on the Instagram message received:
{
"event": "message.received",
"data": {
"messageId": "msg_abc123",
"conversationId": "conv_456",
"platform": "instagram",
"direction": "incoming",
"text": "Hi, I have a question",
"sender": { "id": "user_789", "name": "John" },
"timestamp": "2025-01-15T10:30:00Z"
}
}
What else does Zernio cover beyond Instagram DMs?
Instagram messaging is one endpoint. Zernio covers the full social layer: publishing, comments, DMs, analytics, and ads — across 15 platforms.
For Instagram specifically:
- Post to Instagram: images, videos, carousels, Reels, Stories
- Instagram comments API: read, reply, moderate, and hide comments
- Instagram analytics: impressions, reach, follower history, demographics
- Comment-to-DM automation: auto-DM users who comment specific keywords — built in, no extra code
The DMs API covers 7 platforms with the same endpoint: Instagram, Facebook, WhatsApp, Twitter/X, Reddit, Bluesky, and Telegram. If your product adds WhatsApp messaging next quarter, it's the same integration you already have.
For AI agent builders, Zernio has a hosted MCP server at mcp.zernio.com with 280+ tools. An agent can post content, check engagement, reply to comments, and send DMs through one tool server.
Stop building social integrations from scratch.
One API call to publish, schedule, and manage posts across 15+ platforms.
Direct Meta API vs Zernio: which should you use?
| Direct Meta API | Zernio | |
|---|---|---|
| Setup time | 4-8 hours | Under 30 minutes |
| Platforms covered | Instagram only | 15 platforms, 7 with DMs |
| Token management | Manual (refresh every 60 days) | Automatic |
| Webhook infrastructure | You build and host | Normalized, managed |
| Rate limit handling | You implement | Built in |
| Comments, analytics, ads | Separate integration needed | Same API |
| Comment-to-DM automation | Build from scratch | Included |
| Pricing | Free (Meta API) + your infra costs | First 2 accounts free, additional accounts ~$6/mo/account |
Choose the direct Meta API if you have a vendor compliance requirement or if you're building a Meta-specific integration that will never extend to other platforms.
For most developers adding Instagram DM features to a product, especially if you'll eventually need comments, posting, or other platforms, Zernio saves the build time and keeps your integration path open. You're not locked in: Zernio uses Meta's official platform APIs under the hood, so your users' accounts connect through the same OAuth flow.
Frequently asked questions
Is the Instagram Messaging API free?
The API itself is free. You need a Meta Developer account and an approved Meta App, and you must pass App Review for production access. There are no per-message fees from Meta. Your costs are infrastructure (hosting your OAuth server and webhook endpoint) and any API middleware you use.
Can I send automated DMs to anyone on Instagram?
No. The API only lets you message users who have already sent a message to your business account. Once a user messages you, a 24-hour window opens for free-form replies. After that, you can still send Human Agent messages for up to 7 days, but only for customer support purposes. Unsolicited outreach isn't possible through the official API.
What's the difference between the Instagram Messaging API and the Graph API?
The Instagram Graph API covers all Instagram functionality: posting, reading insights, managing comments, and messaging. The Instagram Messaging API is the messaging subset of the Graph API, using the /messages endpoint. Both use the same OAuth 2.0 authentication.
Do I need App Review to use the Instagram Messaging API?
Yes, for production use. During development you can test with up to 25 test users without App Review. To message real users at scale, you need the instagram_business_manage_messages permission approved through Meta App Review. It typically takes weeks to months. You can use Zernio to skip that infrastructure and get DMs across 7 platforms from one endpoint running in under an hour.