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
- Someone comments on a post owned by a connected Instagram account. Posts published outside Outstand count too.
- Outstand sends
comment.receivedto your webhook endpoint with the comment text and its author. - Your application decides whether to answer, for example by matching a keyword.
- You call
POST /v1/conversationswith the comment. The commenter receives your message as a DM, and Outstand stores it as the first message of a conversation. - When the commenter replies,
message.receivedfires 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
lastInboundAtisnulland Send a message returns422. - Text only. The first message takes
contentand 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_messagesinscopes, keeping the default scopes in the list.instagram_business_manage_commentsis 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, plusmessage.receivedto hear the commenter's answer. Addconversation.started,message.sentandmessage.failedif 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 field | POST /v1/conversations field |
|---|---|
accountId | social_account_id |
commentId | instagram.comment_id |
authorId | instagram.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.
| Status | Meaning | What to do |
|---|---|---|
400 | The body is invalid, or instagram is missing for an Instagram account. | Fix the request. |
404 | The social account was not found, or the comment is not on one of its posts, for example because it was deleted. | Do not retry. |
409 | The comment already has a private reply. | Treat as done. |
422 | More than 7 days have passed since the comment. | Do not retry. |
502 | Instagram 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. |