DEV Community

SolutionsCraft
SolutionsCraft

Posted on Originally published at solutionscraft.com on

Turn Instagram Comments Into DMs With n8n

"Comment LINK and I'll send it to you!" works great for engagement, right up until you're the one copy-pasting the same DM forty times an hour, and inevitably missing a few in the scroll. Instagram comment-to-DM automation fixes exactly this: a keyword in a comment triggers a workflow that replies publicly and privately messages the commenter, in the seconds after they post, every time, with nobody at the keyboard.

This walkthrough builds the whole thing on n8n and Meta's own Graph API, no third-party automation SaaS in the middle. It's more setup than a no-code app promises, but it's also the same API those apps are calling behind the scenes, so you're not trading capability for control.

Before you start

You'll need an n8n instance (a free n8n Cloud trial works fine for this, and if this is your first time wiring up an n8n webhook from scratch, the folder-watcher walkthrough covers the editor basics this one builds on) and an Instagram Professional (Business or Creator) account. You'll also need a Meta Developer App with the Instagram product added, so create one at developers.facebook.com if you don't have one yet.

Disclosure: the n8n Cloud link above is an affiliate link — if you sign up through it, we may earn a commission at no extra cost to you. See our affiliate disclosure for details.

One thing worth knowing before you sink an evening into this: sending the private DM needs the instagram_manage_messages permission, and Meta classes it as a sensitive scope. In development mode it works fine against your own account and any test users you've added, but taking it live for real commenters means submitting the app for App Review first, and reviews for messaging permissions routinely take longer than the more common scopes. Budget for that lag before you promise this to anyone.

Step 1: Wire up the comment-to-DM automation webhook

In your Meta app, open Webhooks, choose the Instagram product, and subscribe to the comments field. It'll ask for a callback URL and a verify token, both of which point at an n8n Webhook node you create for this, so turn on "Allow Multiple HTTP Methods" on that node so it accepts both the one-time verification GET and the real POST events afterward.

Meta's verification handshake is simple but exacting: it sends a GET with hub.mode, hub.verify_token, and hub.challenge query params, and expects the raw hub.challenge value echoed back as plain text if your token matches, not JSON, not wrapped in anything else. Branch on the HTTP method with an IF node, and on the GET branch use a Respond to Webhook node set to "Text" with the response body {{ $json.query["hub.challenge"] }}, guarded by a check that $json.query["hub.verify_token"] matches the token you set in the Meta dashboard.

I don't have a live Meta app to fire a real request at in this environment, so I tested that matching logic standalone against the exact shape Meta's docs describe, rather than n8n's actual node internals:

function handleVerification(query, expectedVerifyToken) {
  if (query["hub.mode"] === "subscribe" && query["hub.verify_token"] === expectedVerifyToken) {
    return { status: 200, body: String(query["hub.challenge"]) };
  }
  return { status: 403, body: "Verification failed" };
}

// handleVerification({ "hub.mode": "subscribe", "hub.verify_token": "my-secret", "hub.challenge": "12345" }, "my-secret")
// -> { status: 200, body: "12345" }
Enter fullscreen mode Exit fullscreen mode

Ran that in plain Node against a matching token and a deliberately wrong one, and it echoes the challenge only when the token lines up, 403s otherwise, which is exactly the contract Meta's docs describe.

Step 2: Filter for your trigger keyword

Every comment on every post you own fires this webhook, so the next node's job is to throw out everything that isn't the keyword you care about, and to ignore the echo of your own reply so the workflow doesn't loop on itself. Add a Code node on the POST branch:

const entry = $input.first().json.body.entry?.[0];
const change = entry?.changes?.[0];
if (!change || change.field !== "comments") return [];

const value = change.value;
const OWN_IG_USER_ID = "<your IG user id>";
if (value.from?.id === OWN_IG_USER_ID) return [];

const KEYWORDS = ["link", "guide"];
const text = (value.text || "").toLowerCase();
const matched = KEYWORDS.some((k) => text.includes(k));
if (!matched) return [];

return [{
  json: {
    commentId: value.id,
    commenterId: value.from?.id,
    commenterUsername: value.from?.username,
    text: value.text,
  },
}];
Enter fullscreen mode Exit fullscreen mode

Blip: Somewhere a spec writer decided a webhook that fires on your own reply, right after you reply, was a feature and not a trap. Check the sender id, always.

I verified this filtering logic (case-insensitive keyword match, and skipping the account's own comments) as a standalone script against a sample payload shaped like Meta's documented comments webhook, and a match returns the parsed fields while a non-match and a self-authored comment both correctly return nothing. Wiring the exact same logic into an n8n Code node is a direct port, not a rewrite, but the live n8n execution itself isn't something I ran here.

Step 3: Reply publicly first

A public reply keeps the conversation visible under the post, a bit of social proof for the next person scrolling by, and it's the same endpoint whether or not you go on to DM anyone. Add an HTTP Request node:

POST https://graph.instagram.com/v21.0/{{ $json.commentId }}/replies
Authorization: Bearer <your Instagram access token>
Content-Type: application/json

{ "message": "Sent you a DM with the link! 📩" }
Enter fullscreen mode Exit fullscreen mode

Step 4: Send the private DM

This is the actual comment-to-DM automation piece, turning that public comment into a private message without the commenter ever having messaged you first. Instagram's Messaging API supports exactly one flow for this: a private reply, sent from your own IG account's /messages endpoint but addressed by comment ID rather than by the commenter's user ID, in another HTTP Request node:

POST https://graph.instagram.com/v21.0/{{ $json.myIgUserId }}/messages
Authorization: Bearer <your Instagram access token>
Content-Type: application/json

{
  "recipient": { "comment_id": "{{ $json.commentId }}" },
  "message": { "text": "Here's the link: https://solutionscraft.com/newsletter" }
}
Enter fullscreen mode Exit fullscreen mode

{{ $json.myIgUserId }} here is your own Instagram business account's user ID (the same value you'd hardcode as OWN_IG_USER_ID in Step 2), not the commenter's, since the endpoint is scoped to your account and the recipient is identified through the comment_id in the body instead.

Two hard limits worth planning around, straight from Meta's docs: you can only private-reply to a comment within 7 days of it being posted, and you only get one private reply per comment, ever. Send it once and that comment's window is closed for good, so this isn't a node you want firing twice on a retry.

Step 5: Land the DM link on a page, not an API call

It's tempting to have the DM link straight into your own signup API to skip a step, but don't point it at an endpoint sitting behind bot protection. Ours, like most newsletter signup forms, is gated by Cloudflare Turnstile, which needs an actual browser solving a challenge, something no link click can do on its own. Send the DM to your normal newsletter signup page instead, and let the page's existing form (Turnstile check, Resend contact creation, automatic welcome email) do the rest exactly the way a human visitor triggers it. The automation's job ends at getting a warm click into that page, not at replacing the page.

What this doesn't solve

This catches new comments going forward, not a backlog of ones posted before the webhook was live, and it won't touch anything if you edit an existing comment. Instagram also caps API calls at 200 per user per hour (down from the 5,000 you'll see quoted in older tutorials), so a viral post with the keyword in a huge share of the comments can outrun this well before it outruns your patience. And the 7-day/one-reply limits from Step 4 mean this is a first-touch tool, so anything past the initial DM needs a real conversation, not another automated message.

Where to start

Wire up Steps 1 and 2 first and just watch the executions log for a day against your own comments, confirming the keyword filter catches the right ones and ignores your own replies before you let Step 4 actually message a stranger. Once that's solid, turn on the public reply and private DM, and treat App Review for instagram_manage_messages as the thing to submit in parallel, not after, since it's the step most likely to make you wait. If you'd rather point n8n itself at an AI assistant once this is running, connecting n8n's instance-level MCP to Claude Desktop covers asking it to inspect or adjust a workflow like this one in plain English.

Top comments (0)