How to Build an Instagram Bot for Business in 2026
There are two approaches to Instagram automation. The grey-zone approach uses browser automation or scraping libraries to simulate human actions — it works until Meta bans your account, usually within weeks. The right approach uses the official Meta Graph API, which is slower to set up but runs indefinitely without risk.
This guide covers the official approach: Instagram Graph API + Webhooks, handling DMs, comment replies, and story mentions.
What the Official API Allows
The Instagram Graph API (part of Meta's platform) allows:
- Receive and reply to DMs (Instagram Messaging)
- Reply to comments on your posts
- Receive story mention notifications and respond
- Send template messages (text, buttons, quick replies)
- Get user profile info (name, username) of users who message you
What requires special Meta review approval:
- Sending proactive DMs to users who haven't messaged you first
- Bulk messaging campaigns
For most business use cases (autoresponders, lead capture via DM, comment management), no special approval beyond basic Business account verification is needed.
Architecture
Instagram User
↓ (sends DM / comments)
Meta Servers
↓ (webhook POST)
Your Server (HTTPS)
↓
Message Handler
↓
Meta Graph API (send reply)
↓
Instagram User receives reply
Step 1: Meta App Setup
- Go to developers.facebook.com
- Create a new App → Business type
- Add Instagram Graph API product
- Connect your Instagram Professional account (Business or Creator)
- Set up a webhook (explained below)
Required permissions for your app:
instagram_basicinstagram_manage_messagesinstagram_manage_commentspages_manage_metadata
For development, use Meta's Test Users so you don't need app review.
Step 2: Webhook Setup
Meta sends all events (DMs, comments, story mentions) to your webhook URL.
// src/routes/instagram-webhook.ts
import { Router, Request, Response } from 'express';
import crypto from 'crypto';
export const instagramWebhookRouter = Router();
const VERIFY_TOKEN = process.env.META_VERIFY_TOKEN!;
const APP_SECRET = process.env.META_APP_SECRET!;
// Webhook verification (one-time, during setup)
instagramWebhookRouter.get('/webhook', (req: Request, res: Response) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
console.log('Webhook verified');
return res.status(200).send(challenge);
}
res.sendStatus(403);
});
// Receive events
instagramWebhookRouter.post('/webhook', (req: Request, res: Response) => {
// Always respond 200 immediately — Meta retries if you're slow
res.sendStatus(200);
// Verify the request came from Meta
if (!verifySignature(req)) {
console.error('Invalid webhook signature');
return;
}
const body = req.body;
if (body.object !== 'instagram') return;
for (const entry of body.entry ?? []) {
// Direct Messages
for (const messagingEvent of entry.messaging ?? []) {
handleIncomingMessage(messagingEvent).catch(console.error);
}
// Comments
for (const change of entry.changes ?? []) {
if (change.field === 'comments') {
handleComment(change.value).catch(console.error);
}
if (change.field === 'mentions') {
handleMention(change.value).catch(console.error);
}
}
}
});
function verifySignature(req: Request): boolean {
const signature = req.headers['x-hub-signature-256'] as string;
if (!signature) return false;
const expected = crypto
.createHmac('sha256', APP_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expected)
);
}
Step 3: Send Messages via Graph API
// src/services/instagram.service.ts
const GRAPH_API_BASE = 'https://graph.instagram.com/v21.0';
const PAGE_ACCESS_TOKEN = process.env.META_PAGE_ACCESS_TOKEN!;
interface SendMessageOptions {
recipientId: string;
text?: string;
buttons?: Array<{ type: 'postback' | 'web_url'; title: string; payload?: string; url?: string }>;
quickReplies?: Array<{ title: string; payload: string }>;
}
export async function sendInstagramMessage(options: SendMessageOptions): Promise<void> {
const message: Record<string, unknown> = {};
if (options.buttons && options.buttons.length > 0) {
message.attachment = {
type: 'template',
payload: {
template_type: 'button',
text: options.text,
buttons: options.buttons.map(btn => ({
type: btn.type,
title: btn.title,
...(btn.payload ? { payload: btn.payload } : {}),
...(btn.url ? { url: btn.url } : {}),
})),
},
};
} else {
message.text = options.text;
}
if (options.quickReplies) {
message.quick_replies = options.quickReplies.map(qr => ({
content_type: 'text',
title: qr.title,
payload: qr.payload,
}));
}
const response = await fetch(`${GRAPH_API_BASE}/me/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${PAGE_ACCESS_TOKEN}`,
},
body: JSON.stringify({
recipient: { id: options.recipientId },
message,
messaging_type: 'RESPONSE',
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Meta API error: ${JSON.stringify(error)}`);
}
}
export async function replyToComment(commentId: string, message: string): Promise<void> {
const response = await fetch(`${GRAPH_API_BASE}/${commentId}/replies`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${PAGE_ACCESS_TOKEN}`,
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Comment reply error: ${JSON.stringify(error)}`);
}
}
Step 4: Conversation State with Redis
Multi-step flows (lead capture, order intake) need to remember where each user is in the conversation.
// src/services/conversation.service.ts
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
redis.connect().catch(console.error);
interface ConversationState {
step: string;
data: Record<string, string>;
updatedAt: number;
}
const CONVERSATION_TTL = 60 * 60; // 1 hour idle timeout
export async function getState(userId: string): Promise<ConversationState | null> {
const raw = await redis.get(`ig:conv:${userId}`);
return raw ? JSON.parse(raw) : null;
}
export async function setState(userId: string, state: ConversationState): Promise<void> {
await redis.setEx(
`ig:conv:${userId}`,
CONVERSATION_TTL,
JSON.stringify({ ...state, updatedAt: Date.now() })
);
}
export async function clearState(userId: string): Promise<void> {
await redis.del(`ig:conv:${userId}`);
}
Step 5: Build a Lead Capture Flow
A common use case: user sends "price" or "info" → bot collects their name + phone number → sends to CRM.
// src/handlers/dm-handler.ts
import { sendInstagramMessage } from '../services/instagram.service';
import { getState, setState, clearState } from '../services/conversation.service';
import { saveLead } from '../services/crm.service';
interface IncomingMessage {
sender: { id: string };
recipient: { id: string };
timestamp: number;
message: {
mid: string;
text?: string;
quick_reply?: { payload: string };
};
}
const TRIGGER_KEYWORDS = ['price', 'цена', 'стоимость', 'info', 'buy', 'купить', 'order', 'заказать'];
export async function handleIncomingMessage(event: IncomingMessage): Promise<void> {
const userId = event.sender.id;
const text = event.message.text?.toLowerCase().trim() ?? '';
const quickReplyPayload = event.message.quick_reply?.payload;
const state = await getState(userId);
// Handle quick reply responses
if (quickReplyPayload) {
await handleQuickReply(userId, quickReplyPayload, state);
return;
}
// No active conversation — check for trigger keywords
if (!state) {
if (TRIGGER_KEYWORDS.some(kw => text.includes(kw))) {
await startLeadFlow(userId);
} else {
await sendDefaultReply(userId);
}
return;
}
// Continue active conversation
await continueLeadFlow(userId, text, state);
}
async function startLeadFlow(userId: string): Promise<void> {
await setState(userId, { step: 'ask_name', data: {}, updatedAt: Date.now() });
await sendInstagramMessage({
recipientId: userId,
text: "Hi! I'd love to help you. What's your name?",
quickReplies: [
{ title: 'Skip', payload: 'SKIP_NAME' },
],
});
}
async function continueLeadFlow(
userId: string,
text: string,
state: { step: string; data: Record<string, string> }
): Promise<void> {
switch (state.step) {
case 'ask_name': {
const name = text.length > 0 ? text : 'Unknown';
await setState(userId, { step: 'ask_phone', data: { name }, updatedAt: Date.now() });
await sendInstagramMessage({
recipientId: userId,
text: `Nice to meet you, ${name}! What's your phone number so we can call you back?`,
quickReplies: [
{ title: 'Skip', payload: 'SKIP_PHONE' },
],
});
break;
}
case 'ask_phone': {
const phone = text;
const { name } = state.data;
await completeLead(userId, name, phone);
break;
}
}
}
async function handleQuickReply(
userId: string,
payload: string,
state: { step: string; data: Record<string, string> } | null
): Promise<void> {
if (payload === 'SKIP_NAME') {
await setState(userId, { step: 'ask_phone', data: { name: 'Unknown' }, updatedAt: Date.now() });
await sendInstagramMessage({
recipientId: userId,
text: "No worries! What's your phone number?",
});
}
if (payload === 'SKIP_PHONE') {
const name = state?.data.name ?? 'Unknown';
await completeLead(userId, name, '');
}
}
async function completeLead(userId: string, name: string, phone: string): Promise<void> {
// Save to your CRM
await saveLead({
instagramUserId: userId,
name,
phone,
source: 'instagram_dm',
createdAt: new Date(),
});
await clearState(userId);
await sendInstagramMessage({
recipientId: userId,
text: '✅ Thank you! Our manager will contact you within 1 hour.',
buttons: [
{
type: 'web_url',
title: 'View Our Services',
url: 'https://aunimeda.com/services',
},
],
});
}
async function sendDefaultReply(userId: string): Promise<void> {
await sendInstagramMessage({
recipientId: userId,
text: 'Hi! 👋 How can we help you today?',
quickReplies: [
{ title: '💰 Pricing', payload: 'INTENT_PRICE' },
{ title: '📋 Services', payload: 'INTENT_SERVICES' },
{ title: '📞 Call Me', payload: 'INTENT_CALL' },
],
});
}
Step 6: Comment Auto-Reply
Automatically reply to comments that contain specific keywords — useful for giveaways ("Comment PRICE to get a quote in DM") or product inquiries.
// src/handlers/comment-handler.ts
import { replyToComment, sendInstagramMessage } from '../services/instagram.service';
interface CommentEvent {
id: string;
text: string;
from: { id: string; username: string };
media: { id: string };
timestamp: string;
}
const COMMENT_TRIGGERS: Array<{ keywords: string[]; reply: string; sendDm?: string }> = [
{
keywords: ['price', 'cost', 'how much', 'цена', 'сколько'],
reply: 'Hi @{username}! 👋 Check your DMs — we sent you the details.',
sendDm: "Hi! You asked about pricing in our comments. Here's what we offer:\n\n💼 Telegram Bot: from $800\n🌐 Web App: from $1,500\n📱 Mobile App: from $3,000\n\nReply here to discuss your project!",
},
{
keywords: ['dm', 'inbox', 'message me'],
reply: 'Hey @{username}! Check your DMs 📩',
sendDm: "Hi! You wanted to connect — how can we help you?",
},
];
export async function handleComment(event: CommentEvent): Promise<void> {
const commentText = event.text.toLowerCase();
const username = event.from.username;
for (const trigger of COMMENT_TRIGGERS) {
if (trigger.keywords.some(kw => commentText.includes(kw))) {
// Reply publicly on the comment
const publicReply = trigger.reply.replace('{username}', username);
await replyToComment(event.id, publicReply);
// Send private DM if configured
if (trigger.sendDm) {
await sendInstagramMessage({
recipientId: event.from.id,
text: trigger.sendDm,
});
}
break; // Only apply first matching trigger
}
}
}
Step 7: Story Mention Response
When users tag your account in their story — a high-engagement moment — you can automatically respond:
// src/handlers/mention-handler.ts
import { sendInstagramMessage } from '../services/instagram.service';
interface MentionEvent {
media_id: string;
comment_id?: string;
sender_id: string;
}
export async function handleMention(event: MentionEvent): Promise<void> {
await sendInstagramMessage({
recipientId: event.sender_id,
text: "Thanks for mentioning us in your story! 🙌\n\nWant us to feature your story on our page?",
quickReplies: [
{ title: '✅ Yes, please!', payload: 'REPOST_YES' },
{ title: '❌ No thanks', payload: 'REPOST_NO' },
],
});
}
Deployment Checklist
- Meta App in Live Mode (not Development) before going to production
- Webhook URL is HTTPS with a valid SSL certificate
-
APP_SECRETandPAGE_ACCESS_TOKENstored as environment variables, not in code - Respond to webhook events within 200ms (process async, respond 200 immediately)
- Handle duplicate events (Meta may send the same event twice — use
midfor deduplication) - Set up token refresh — Page Access Tokens expire, use long-lived tokens or system users
- Log all incoming events for debugging
Aunimeda builds Instagram automation systems using the official Meta Graph API — DM flows, lead capture, comment management, and CRM integration.
Contact us to discuss your Instagram bot. See also: Instagram Bot Development, WhatsApp Bot Development, Business Automation