Claude by Anthropic is one of the most capable AI models for customer-facing conversations. It follows instructions precisely, avoids hallucination better than most alternatives, and handles nuanced multi-turn dialogue naturally. This guide shows you how to connect Claude to your Instagram DMs through InstantDM's webhook and API.
By the end, you'll have a production-ready Claude agent that receives Instagram DMs, generates intelligent replies, maintains conversation context, and sends responses back - all automatically.
Why Claude for Instagram DMs?
Claude has specific strengths that make it well-suited for Instagram DM automation:
| Strength | Why It Matters for DMs |
|---|---|
| Instruction following | Claude stays on-brand and follows your rules consistently |
| Low hallucination rate | Won't make up product details or pricing |
| Long context window | Remembers full conversation history (200K tokens) |
| Natural tone | Responses feel human, not robotic |
| Built-in safety | Less likely to generate inappropriate content |
| Tool use support | Can look up products, check inventory, book appointments |
Architecture
Instagram User
|
v
InstantDM (receives DM via Meta Graph API)
|
v Webhook POST
Your Server (Node.js or Python)
|
v
Anthropic Claude API
|
v
Your Server (filters response)
|
v InstantDM REST API
InstantDM (delivers reply as Instagram DM)
What You Need
| Requirement | Details |
|---|---|
| InstantDM account | Trendsetter plan or higher (API access required) |
| InstantDM API key | Settings > API in your dashboard |
| Anthropic API key | From console.anthropic.com |
| Server with public URL | Node.js or Python, deployed to Vercel, Railway, or AWS |
Step 1: Configure the InstantDM Webhook
- Go to Settings > API in your InstantDM dashboard
- Copy your API Key
- Set your Webhook URL to your server endpoint (e.g.
https://your-app.railway.app/webhook/instantdm) - Enable the
dm_receivedevent - Save
Step 2: Build the Webhook Receiver
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const Anthropic = require('@anthropic-ai/sdk');
const app = express();
app.use(express.json());
const INSTANTDM_API_KEY = process.env.INSTANTDM_API_KEY;
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
function verifySignature(req) {
const signature = req.headers['x-webhook-signature'];
if (!signature) return false;
const expected = crypto
.createHmac('sha256', INSTANTDM_API_KEY)
.update(JSON.stringify(req.body))
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.post('/webhook/instantdm', async (req, res) => {
res.status(200).json({ received: true });
if (!verifySignature(req)) {
console.error('Invalid webhook signature');
return;
}
const { event, data } = req.body;
if (event === 'dm_received') {
await handleDM(data);
}
});
app.listen(3000, () => console.log('Claude agent running on port 3000'));
Python (Flask)
import os
import hmac
import hashlib
import threading
from flask import Flask, request, jsonify
import anthropic
app = Flask(__name__)
INSTANTDM_API_KEY = os.environ['INSTANTDM_API_KEY']
client = anthropic.Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
def verify_signature(req):
signature = req.headers.get('X-Webhook-Signature', '')
expected = hmac.new(
INSTANTDM_API_KEY.encode(), req.get_data(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhook/instantdm', methods=['POST'])
def webhook():
if not verify_signature(request):
return jsonify({'error': 'Invalid signature'}), 401
payload = request.get_json()
if payload.get('event') == 'dm_received':
thread = threading.Thread(target=handle_dm, args=(payload['data'],))
thread.start()
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
Step 3: Process Messages with Claude
Node.js
// Conversation memory (use Redis in production)
const conversations = new Map();
const SYSTEM_PROMPT = `You are the customer support assistant for [Your Brand].
Your role:
- Answer product questions accurately
- Help with sizing, shipping, and returns
- Qualify leads by understanding their needs
- Keep responses under 280 characters (Instagram DM best practice)
- Be warm and conversational, not corporate
- Never invent product details or prices
- If unsure, offer to connect them with a human
- Do not use markdown formatting (Instagram doesn't render it)
Product info:
- [Add your actual product catalog, pricing, FAQs here]`;
async function handleDM(data) {
const { instagram_user_id, message_text } = data;
// Load conversation history
if (!conversations.has(instagram_user_id)) {
conversations.set(instagram_user_id, []);
}
const history = conversations.get(instagram_user_id);
history.push({ role: 'user', content: message_text });
// Trim to last 20 messages
if (history.length > 20) history.splice(0, history.length - 20);
try {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 250,
system: SYSTEM_PROMPT,
messages: history,
});
const reply = response.content[0].text;
history.push({ role: 'assistant', content: reply });
await sendReply(instagram_user_id, reply);
} catch (err) {
console.error('Claude error:', err);
await sendReply(instagram_user_id,
"Hey! I'm having a quick technical hiccup. A team member will get back to you shortly."
);
}
}
Python
conversations = {}
SYSTEM_PROMPT = """You are the customer support assistant for [Your Brand].
Your role:
- Answer product questions accurately
- Help with sizing, shipping, and returns
- Qualify leads by understanding their needs
- Keep responses under 280 characters (Instagram DM best practice)
- Be warm and conversational, not corporate
- Never invent product details or prices
- If unsure, offer to connect them with a human
- Do not use markdown formatting (Instagram doesn't render it)
Product info:
- [Add your actual product catalog, pricing, FAQs here]"""
def handle_dm(data):
user_id = data['instagram_user_id']
message = data['message_text']
if user_id not in conversations:
conversations[user_id] = []
history = conversations[user_id]
history.append({'role': 'user', 'content': message})
# Keep last 20 messages
if len(history) > 20:
conversations[user_id] = history[-20:]
history = conversations[user_id]
try:
response = client.messages.create(
model='claude-sonnet-4-20250514',
max_tokens=250,
system=SYSTEM_PROMPT,
messages=history,
)
reply = response.content[0].text
history.append({'role': 'assistant', 'content': reply})
send_reply(user_id, reply)
except Exception as e:
print(f'Claude error: {e}')
send_reply(user_id,
"Hey! I'm having a quick technical hiccup. A team member will get back to you shortly."
)
Step 4: Send Replies via InstantDM API
Node.js
async function sendReply(recipientId, text) {
// Clean up for Instagram
if (text.length > 1000) text = text.substring(0, 997) + '...';
text = text.replace(/[*_~`#]/g, '');
const res = await fetch('https://api.instantdm.com/api-webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${INSTANTDM_API_KEY}`,
},
body: JSON.stringify({
action: 'send_message',
type: 'dm',
recipient_id: recipientId,
message: { type: 'text', text: text.trim() },
}),
});
if (!res.ok) throw new Error(`InstantDM API error: ${res.status}`);
return res.json();
}
Python
import requests
def send_reply(recipient_id, text):
if len(text) > 1000:
text = text[:997] + '...'
for ch in ['*', '_', '~', '`', '#']:
text = text.replace(ch, '')
resp = requests.post(
'https://api.instantdm.com/api-webhook',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {INSTANTDM_API_KEY}',
},
json={
'action': 'send_message',
'type': 'dm',
'recipient_id': recipient_id,
'message': {'type': 'text', 'text': text.strip()},
},
)
resp.raise_for_status()
return resp.json()
Step 5: Add Claude Tool Use (Function Calling)
Claude can call functions to look up real data before responding. This prevents hallucination and lets the agent check inventory, search products, or book appointments.
Node.js
const tools = [
{
name: 'search_products',
description: 'Search the product catalog by name, category, or keyword',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
},
required: ['query'],
},
},
{
name: 'check_stock',
description: 'Check if a specific product is in stock',
input_schema: {
type: 'object',
properties: {
product_id: { type: 'string' },
size: { type: 'string' },
},
required: ['product_id'],
},
},
];
async function executeTool(name, input) {
switch (name) {
case 'search_products':
return await db.products.search(input.query);
case 'check_stock':
return await db.inventory.check(input.product_id, input.size);
default:
return { error: 'Unknown tool' };
}
}
async function handleDMWithTools(data) {
const { instagram_user_id, message_text } = data;
const history = await getConversation(instagram_user_id);
history.push({ role: 'user', content: message_text });
let response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 300,
system: SYSTEM_PROMPT,
messages: history,
tools: tools,
});
// Handle tool use loop
while (response.stop_reason === 'tool_use') {
const toolBlock = response.content.find(b => b.type === 'tool_use');
const result = await executeTool(toolBlock.name, toolBlock.input);
history.push({ role: 'assistant', content: response.content });
history.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: toolBlock.id,
content: JSON.stringify(result),
}],
});
response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 300,
system: SYSTEM_PROMPT,
messages: history,
tools: tools,
});
}
const reply = response.content.find(b => b.type === 'text')?.text || '';
history.push({ role: 'assistant', content: response.content });
await saveConversation(instagram_user_id, history);
await sendReply(instagram_user_id, reply);
}
Step 6: Production Conversation Storage with Redis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function getConversation(userId) {
const data = await redis.get(`conv:${userId}`);
return data ? JSON.parse(data) : [];
}
async function saveConversation(userId, history) {
await redis.set(
`conv:${userId}`,
JSON.stringify(history.slice(-20)),
'EX', 86400 // 24 hour TTL
);
}
Model Selection Guide
| Model | Best For | Speed | Cost |
|---|---|---|---|
| claude-sonnet-4-20250514 | Most DM conversations - great balance of quality and speed | Fast | ~$0.003/1K input, $0.015/1K output |
| claude-haiku-3-20240307 | High-volume simple replies (FAQs, link delivery) | Very fast | ~$0.00025/1K input, $0.00125/1K output |
| claude-opus-4-20250514 | Complex reasoning, multi-step tool use | Slower | ~$0.015/1K input, $0.075/1K output |
For most Instagram DM use cases, Claude Sonnet is the right choice. Use Haiku for simple keyword-triggered responses where speed matters most. Use Opus only for complex sales conversations with multiple tool calls.
Cost Estimate
At ~200 tokens per response with Claude Sonnet:
| Daily DMs | Monthly Cost (AI only) |
|---|---|
| 100 | ~$2 |
| 500 | ~$10 |
| 1,000 | ~$20 |
| 5,000 | ~$100 |
Add InstantDM plan cost ($9.99/mo Trendsetter) and hosting ($0-5/mo on Railway or Vercel).
Troubleshooting
| Issue | Fix |
|---|---|
| Claude responses too long | Set max_tokens to 200-250 and add "keep under 280 characters" to system prompt |
| Slow responses | Switch to claude-haiku for faster replies |
| Claude ignoring instructions | Move critical rules to the top of the system prompt |
| Rate limited by Anthropic | Implement exponential backoff or queue messages |
| Conversation context lost | Switch from in-memory Map to Redis (see Step 6) |
What's Next
- Deploy the basic agent and test with real Instagram DMs
- Add Redis for persistent conversation storage
- Implement tool use for product lookups and appointment booking
- Connect to your CRM with the HubSpot integration guide
- Set up Slack notifications for human handoff alerts