Integration Guide

How to Build a Claude AI Agent for Instagram DMs with InstantDM

Build a Claude-powered Instagram DM agent using Anthropic's API and InstantDM webhooks. Complete Node.js and Python guide with conversation memory, tool use, content filtering, and production deployment.

Meta Business Partner
30,000+ creators
$9.99/mo flat

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

  1. Go to Settings > API in your InstantDM dashboard
  2. Copy your API Key
  3. Set your Webhook URL to your server endpoint (e.g. https://your-app.railway.app/webhook/instantdm)
  4. Enable the dm_received event
  5. 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

Ready to Automate Your Instagram DMs?

Join 30,000+ creators and brands using InstantDM today.

Start Your Free Trial

No credit card required. Setup in under 15 minutes.