Comment-to-DM

Send a direct message to people who comment on your Instagram posts, using the comment.received webhook and POST /v1/conversations.

Comment-to-DM is a flow that answers a comment in the commenter's direct messages. Someone comments "LINK" on a post, and your application sends them the link, a discount code or a sign-up form as a DM. When they reply, the thread carries on as a normal conversation in your inbox.

It takes two parts of the API: the comment.received webhook tells you about the comment, and Start a conversation (POST /v1/conversations) sends the DM.

Comment-to-DM works on Instagram only. Instagram calls it a private reply: a business cannot message someone first, but it can answer their comment privately.

How it works

  1. Someone comments on a post owned by a connected Instagram account. Posts published outside Outstand count too.
  2. Outstand sends comment.received to your webhook endpoint with the comment text and its author.
  3. Your application decides whether to answer, for example by matching a keyword.
  4. You call POST /v1/conversations with the comment. The commenter receives your message as a DM, and Outstand stores it as the first message of a conversation.
  5. When the commenter replies, message.received fires and you continue with Send a message.

Instagram's rules

  • One private reply per comment. A second request for the same comment returns 409. A later comment from the same person can get its own reply, which joins their existing conversation.
  • Within 7 days of the comment. After that, the request returns 422.
  • Nothing more until they reply. The private reply does not open the 24-hour messaging window; the commenter's answer does. Until then, the conversation's lastInboundAt is null and Send a message returns 422.
  • Text only. The first message takes content and no media. Once the contact replies, you can send media as usual.

1. Prepare the account

  • Managed Keys. Direct Messages are not available with your own Meta app (BYOK) yet. See Availability.
  • Messaging access. Connect, or reconnect, the account with instagram_business_manage_messages in scopes, keeping the default scopes in the list. instagram_business_manage_comments is needed for the comments. See Connect with messaging access.
  • Instagram settings. The account must be a Business or Creator account with Allow access to messages turned on. See Instagram account requirements.
  • Webhooks. Subscribe your webhook endpoint to comment.received, plus message.received to hear the commenter's answer. Add conversation.started, message.sent and message.failed if your inbox tracks them.

2. Receive the comment

{
  "event": "comment.received",
  "timestamp": "2026-10-04T10:30:00.000Z",
  "data": {
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "network": "instagram",
    "accountId": "x8Kp2",
    "postId": "a1B2c",
    "platformPostId": "17912345678901234",
    "commentId": "17865799348089039",
    "parentCommentId": null,
    "authorId": "17841400000000000",
    "authorUsername": "jane.doe",
    "text": "LINK please! Is it available in blue?"
  }
}

Three of its fields address the DM:

comment.received fieldPOST /v1/conversations field
accountIdsocial_account_id
commentIdinstagram.comment_id
authorIdinstagram.author_id

Use text to decide whether to answer, and parentCommentId to tell a top-level comment (null) from a reply to another comment. Comments written by the connected account itself do not trigger the event.

Verify the signature and acknowledge with a 2xx before doing the work: the send calls Instagram, so run it after responding, for example from a job queue.

3. Send the DM

curl -X POST https://api.outstand.so/v1/conversations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "social_account_id": "x8Kp2",
    "content": "Thanks Jane! Here is the blue one: https://shop.example.com/blue",
    "instagram": {
      "comment_id": "17865799348089039",
      "author_id": "17841400000000000"
    }
  }'

A successful request returns 201 Created with the conversation and its first message (abridged):

{
  "success": true,
  "conversation": {
    "id": "9dyJS",
    "socialAccountId": "x8Kp2",
    "network": "instagram",
    "participantId": "17841400000000000",
    "participantDisplayName": "jane.doe",
    "lastInboundAt": null,
    "unreadCount": 0,
    "status": "active"
  },
  "message": {
    "id": "3kPqR",
    "conversationId": "9dyJS",
    "direction": "outbound",
    "source": "api",
    "content": "Thanks Jane! Here is the blue one: https://shop.example.com/blue",
    "status": "sent"
  }
}

The message's status is sent once Instagram has delivered it, or pending until then, and message.sent fires on delivery. conversation.started fires when this is the first conversation with the commenter. If they have messaged the account before, the DM is added to their existing conversation instead.

Store the conversation id against the comment so you can match the commenter's answer to it later.

Example: keyword auto-reply

A Node.js handler that answers top-level comments containing "link":

const KEYWORD = /\blink\b/i;

async function handleCommentReceived({ data }) {
  if (data.network !== 'instagram' || data.parentCommentId !== null || !KEYWORD.test(data.text)) {
    return;
  }

  const res = await fetch('https://api.outstand.so/v1/conversations', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.OUTSTAND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      social_account_id: data.accountId,
      content: `Thanks @${data.authorUsername}! Here is the link: https://shop.example.com/blue`,
      instagram: { comment_id: data.commentId, author_id: data.authorId },
    }),
  });

  if (res.status === 201 || res.status === 409) {
    return;
  }

  const { error } = await res.json();
  if (res.status === 502) {
    throw new Error(`Instagram rejected the DM, retry later: ${error}`);
  }
  console.warn(`Not sending a DM for comment ${data.commentId}: ${res.status} ${error}`);
}

Webhooks can be delivered more than once. Because a comment can receive one private reply, a duplicate delivery returns 409 rather than sending a second DM, so treat 409 as done.

4. Continue the conversation

When the commenter replies, Outstand stores their message and fires message.received with the same conversationId. Their reply sets lastInboundAt and opens the 24-hour window, so you can answer with Send a message, including media:

curl -X POST https://api.outstand.so/v1/conversations/9dyJS/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Great choice! Want me to reserve one in your size?"}'

From here the thread is an ordinary conversation: see Message lifecycle and webhooks.

Errors

Errors return success: false and an error message.

StatusMeaningWhat to do
400The body is invalid, or instagram is missing for an Instagram account.Fix the request.
404The social account was not found, or the comment is not on one of its posts, for example because it was deleted.Do not retry.
409The comment already has a private reply.Treat as done.
422More than 7 days have passed since the comment.Do not retry.
502Instagram rejected the DM; error carries its response. Nothing was stored.Retry once the cause is fixed. If error mentions permissions, reconnect the account with messaging access.