Announcements

Instagram DMs are now in the Outstand API

Outstand now reads and sends Instagram Direct Messages. Five endpoints, five webhook events, and one constraint that shapes every inbox you will build on top of it: Instagram's 24 hour reply window.

Outstand started as a publishing API. You integrated once and posted everywhere, and the traffic went one way: out.

That changes today. Instagram Direct Messages are now part of the API. You can list an account's conversations, read a thread, send a reply, schedule one for later, and mark a thread as read. Inbound messages arrive at your webhook endpoint the moment they land. The Conversations documentation covers the whole surface, and this post is the tour.

Update, October 2026: one thing in this post has changed since it went out. There is now a way to start a conversation - POST /v1/conversations answers an Instagram comment with a direct message to the person who wrote it, once per comment, within seven days. The sections below are corrected for it, and the comment to DM walkthrough has the whole loop in code.

The endpoints

Five calls, all under /v1/conversations:

Call

What it does

GET /v1/conversations

Lists threads for a connected account, most recent activity first

GET /v1/conversations/{id}/messages

Reads a thread, newest first, with an inbound or outbound filter

POST /v1/conversations/{id}/messages

Sends a reply, immediately or scheduled

POST /v1/conversations/{id}/read

Marks inbound messages read and sends a read receipt on Instagram

DELETE on a scheduled message

Cancels a queued reply that has not gone out yet

At launch there was no way to open a thread at all, and that was deliberate rather than missing: a conversation existed because a contact messaged the account. One narrow way in has shipped since. POST /v1/conversations takes a comment id and sends a direct message to its author - Meta calls it a private reply - and it is limited to one message per comment, within seven days of the comment, text only. What is still true is that you cannot open a thread with somebody who has neither written to the account nor commented on its posts. That is Instagram's rule, not ours.

Sending a reply looks like this:

bash
1curl -X POST https://api.outstand.so/v1/conversations/CONVERSATION_ID/messages \
2  -H "Authorization: Bearer YOUR_API_KEY" \
3  -H "Content-Type: application/json" \
4  -d '{"content": "Hello! How can I help you today?"}'

That returns 202 Accepted with a message whose status is pending. Acceptance is not delivery. The real outcome arrives on a webhook, or you poll the message list. Do not treat the 202 as a sent message and do not send a second copy while the first is pending.

The 24 hour window

This is the constraint that shapes every inbox built on the Instagram messaging API, so it is worth getting straight before you write any code.

You may send free-form text for 24 hours after the contact's last inbound message. Your own replies do not extend that window. Only the contact writing again does. Send after it closes and the request is rejected with 422.

There is one sanctioned extension. Passing the HUMAN_AGENT tag stretches the window to seven days:

json
1{
2  "content": "I have checked your support request.",
3  "instagram": { "tag": "HUMAN_AGENT" }
4}

It is exactly what the name says: a human replying to a support issue, not a way to keep an automated sequence alive. Two things have to be true before a tagged send works. Outstand enables the tag per organization on request, so ask support first - until then the request comes back 403 The HUMAN_AGENT tag is not enabled for this organization. And the Meta app that owns the connection has to be approved by Meta for Human Agent, which is App Review plus business verification; if it is not, the reply is accepted with a 202 and then fails at delivery with Instagram's own refusal on message.failed. Treat it as the support-desk escape hatch it is.

Webhooks

Five events, all carrying the standard event, timestamp and data envelope:

  • conversation.started when a new thread is stored
  • message.received for an inbound message
  • message.sent when an outbound message is delivered
  • message.failed when an accepted message fails delivery, with the platform error in error
  • message.read when the contact reads your reply
json
1{
2  "event": "message.received",
3  "timestamp": "2026-09-21T10:00:01.000Z",
4  "data": {
5    "conversationId": "9dyJS",
6    "messageId": "3kPqR",
7    "orgId": "org_example",
8    "network": "instagram",
9    "content": "Can you help with my order?",
10    "senderId": "17841400000000000",
11    "sentAt": "2026-09-21T10:00:00.000Z"
12  }
13}

Two details that save debugging time. Webhook payloads are notifications, not complete message objects, so message.received carries no mediaUrls; fetch the message if you need the media. And a message the account owner sends from the Instagram app on their phone reaches you as message.sent with source: "native_app" and messageId: null. Those are not stored, so record them from the webhook if your inbox needs to show them. Deduplicate on platformMessageId.

Scheduling a reply

Add scheduled_at with a future ISO 8601 timestamp and the reply is queued instead of sent:

bash
1curl -X POST https://api.outstand.so/v1/conversations/CONVERSATION_ID/messages \
2  -H "Authorization: Bearer YOUR_API_KEY" \
3  -H "Content-Type: application/json" \
4  -d '{
5    "content": "Following up on your question from this morning.",
6    "scheduled_at": "2026-10-04T09:00:00.000Z"
7  }'

The window check runs against scheduled_at, not against the moment you call. A reply scheduled past the window is rejected with 422 up front, while the window is still open. That is the behaviour you want: the failure arrives when you can still do something about it rather than silently at delivery time.

Connecting an account

One thing here will catch you. The scopes field replaces Instagram's default scopes rather than adding to them. Send only instagram_business_basic,instagram_business_manage_messages and you get a messaging-only connection with publishing, comments and insights switched off for that account. List all five:

bash
1curl -X POST https://api.outstand.so/v1/social-networks/instagram/auth-url \
2  -H "Authorization: Bearer YOUR_API_KEY" \
3  -H "Content-Type: application/json" \
4  -d '{
5    "redirect_uri": "https://your-app.example.com/social/callback",
6    "scopes": "instagram_business_basic,instagram_business_content_publish,instagram_business_manage_comments,instagram_business_manage_insights,instagram_business_manage_messages"
7  }'

Accounts already connected to Outstand do not have messaging permission and must be reconnected through that call once. Reconnecting updates the account in place, so its ID, scheduled posts and history survive. Until it is reconnected the account is simply silent: no conversations, no webhooks, and no error to tell you why. Reconnecting from the dashboard does not help, because the dashboard asks for the default scopes only, and doing that to an account that already has messaging will remove it.

The limits, and the one that lifted

Direct Messages are Managed Keys only for now. Inbound messages arrive through webhooks on Outstand's own Meta app, so a connection made through your own Meta app does not receive them. If you are on your own keys and need DMs, write to support@outstand.so.

Instagram's private replies - the mechanism that DMs someone who commented on a post without them messaging first - were not part of this release. They are now. The call takes the two ids a comment.received webhook already handed you:

bash
1curl -X POST https://api.outstand.so/v1/conversations \
2  -H "Authorization: Bearer YOUR_API_KEY" \
3  -H "Content-Type: application/json" \
4  -d '{
5    "social_account_id": "x8Kp2",
6    "comment_id": "17865799348089039",
7    "content": "Here is the link you asked for: https://example.com/guide"
8  }'

social_account_id is accountId from the event and comment_id is commentId. The response is 201 with the message already sent, not the 202 the rest of this post describes, because Instagram allows exactly one private reply per comment and there is nothing useful a queued retry could do. A second attempt on the same comment returns 409, and a comment more than seven days old returns 422.

The important limit is what it does not do: a private reply leaves lastInboundAt untouched, so it does not open the 24-hour window. Until the contact answers, a send on that thread is rejected with 422 instagram_no_inbound_message. One message, no follow-up, so it has to carry the whole payload.

The comment trigger is the other half. Subscribe a webhook endpoint to comment.received and you get an event the moment someone comments on a connected account's post, or replies to a comment on it, with the comment text, its author and its commentId. It covers posts published outside Outstand, it skips the account's own comments, and it needs no extra scope - instagram_business_manage_comments is already one of the defaults. Unlike DMs it is not Managed Keys only, because each delivery is verified against the signing secret of the app that owns the connection.

Worth knowing: sending a private reply needs only that comments scope, so an account connected for publishing can already answer its commenters privately. Hearing the answer needs instagram_business_manage_messages, the opt-in above. It is entirely possible to ship the send, see 201s all day, and have no idea nobody's replies are reaching you.

So a comment-triggered flow now closes end to end: answer the comment publicly with the comment publishing API, answer its author privately with POST /v1/conversations, and when they write back message.received fires and the full 24-hour window is yours. The ceiling is one private message per comment, which is lower than most comment to DM tools imply and is the rule all of them are working inside.

Start here

The getting started guide takes you from connecting an account with messaging access to sending a first reply. Message lifecycle and webhooks has the state table, the scheduling rules and every webhook payload. If you are wiring up the Instagram side, the Instagram configuration guide is the reference.

One integration, every network, and now the messages coming back.