Message lifecycle and webhooks
Handle asynchronous delivery, scheduled replies, Instagram messaging windows, and conversation webhook events.
Message states
| Direction | State | Meaning |
|---|---|---|
| Inbound | received | A message from the contact has arrived. |
| Inbound | read | Your application has marked the conversation as read. |
| Outbound | pending | The reply is queued, including replies scheduled for later. |
| Outbound | sent | The reply was sent through the underlying platform. |
| Outbound | read | The platform reported that the contact read the reply. |
| Outbound | failed | Delivery failed; inspect the message's error field. |
Message source
Every stored message has a source:
| Source | Meaning |
|---|---|
api | Sent through the Outstand API with Send a message or Start a conversation. |
contact | Received from the other participant. |
native_app | Sent by the account owner outside Outstand, for example from the Instagram app. |
Messages the account owner sends from the Instagram app are relayed to your webhook as message.sent with source: "native_app", but they are not stored, so they do not appear in List messages and do not start a stored conversation. native_app only appears on messages stored before this behaviour was introduced. To keep your own inbox in sync, record these messages from the webhook.
Send a message returns 202 Accepted with a pending message. Use its id to correlate message.sent or message.failed events, or find it in List messages. Do not treat acceptance as successful delivery or automatically send another copy while the first message is pending.
Text and media
Send non-blank content, at least one URL in media_urls, or both. Media URLs must be accessible to the delivery service. To prepare hosted media, see the Media API.
{
"content": "Here is the image you requested.",
"media_urls": ["https://your-cdn.example.com/product.jpg"]
}Request bodies use media_urls and scheduled_at; message response objects expose mediaUrls and scheduledAt.
Messaging windows and scheduling
Instagram's standard reply window ends 24 hours after the contact's last inbound message (lastInboundAt). Your own outbound replies do not extend this window.
For an eligible human support reply, pass:
{
"content": "I have checked your support request.",
"instagram": { "tag": "HUMAN_AGENT" }
}HUMAN_AGENT extends the window to seven days after the contact's last message. It is for a human replying to a support issue and requires App Review plus business verification on the Meta app that owns the connection. It is not a general-purpose extension for automated replies.
The tag is enabled per organization on request - contact support to request access. Until it is enabled for your organization, a send with instagram.tag is rejected with 403, and a scheduled message that carries the tag fails at delivery if access is removed in the meantime.
To schedule a reply, add scheduled_at with a future ISO 8601 timestamp. Calculate it from your intended delivery time and the contact's current messaging window. The API checks the window against that scheduled time immediately: a reply scheduled beyond the window is rejected with 422, even if the request is made while the window is still open. An invalid or non-future timestamp returns 400.
Scheduled messages remain pending until delivery. The delivery outcome arrives through the same webhooks as an immediate reply.
Starting a conversation
The first message of a conversation you start follows each network's own rules. On Instagram, it must be a private reply to the contact's comment, sent within 7 days of the comment, and each comment can receive one. It does not open the 24-hour window. That opens when the contact answers, so until then the conversation's lastInboundAt is null and Send a message is rejected with 422. See Comment-to-DM for the full flow.
Cancel a scheduled reply
Call Cancel a scheduled message with the conversation ID and message ID. Only a scheduled message that is still pending can be cancelled. Successful cancellation removes the message from the queue and deletes it.
A reply that was not scheduled, or is no longer pending, returns 409. A message not found in the conversation returns 404. Refresh the message list if cancellation races with delivery.
Pagination and read state
Both list endpoints return data and pagination. When hasMore is true, pass the returned nextCursor as the next request's cursor, retaining your filters. Treat the cursor as opaque.
Conversations are ordered by most recent activity; messages are ordered newest first. Conversation pages default to 25 items (maximum 100); message pages default to 50 items (maximum 200). The message list supports an inbound or outbound direction filter.
Mark conversation as read updates inbound messages and clears the unread count.
Webhook events
Configure subscriptions using the shared Webhooks guide. All events have the standard event, timestamp, and data envelope.
| Event | When it occurs | Fields in data |
|---|---|---|
conversation.started | A new conversation is stored, from a contact's message or because you started it. | conversationId, orgId, network, participantId |
message.received | An inbound message is stored. | conversationId, messageId, orgId, network, content, senderId, sentAt |
message.sent | A message sent through the API was delivered (source: "api"), or the account owner sent a message from the Instagram app (source: "native_app"). | conversationId, messageId, orgId, network, platformMessageId, source; for native_app also participantId, senderId, content, mediaUrls, sentAt |
message.failed | An accepted outbound message fails delivery. | conversationId, messageId, orgId, network, error |
message.read | The contact read an outbound message. | conversationId, messageId, orgId, network |
For example, an inbound message event:
{
"event": "message.received",
"timestamp": "2026-09-21T10:00:01.000Z",
"data": {
"conversationId": "9dyJS",
"messageId": "3kPqR",
"orgId": "org_example",
"network": "instagram",
"content": "Can you help with my order?",
"senderId": "17841400000000000",
"sentAt": "2026-09-21T10:00:00.000Z"
}
}An asynchronous delivery failure uses the same envelope:
{
"event": "message.failed",
"timestamp": "2026-09-21T10:01:00.000Z",
"data": {
"conversationId": "9dyJS",
"messageId": "4rStU",
"orgId": "org_example",
"network": "instagram",
"error": "Example delivery error from the platform"
}
}A message sent from the Instagram app is not stored, so its message.sent event has messageId: null and carries the message itself. conversationId is set when the conversation is already stored and null otherwise; use participantId (the contact's platform user ID) to match it to a thread. The mediaUrls are platform CDN links that expire, so copy any media you need to keep. Use platformMessageId to deduplicate these events.
{
"event": "message.sent",
"timestamp": "2026-09-21T10:05:01.000Z",
"data": {
"conversationId": "9dyJS",
"messageId": null,
"orgId": "org_example",
"network": "instagram",
"platformMessageId": "aWdfZAG1faXRlbToxOk...",
"source": "native_app",
"participantId": "17841400000000001",
"senderId": "17841400000000000",
"content": "Thanks, I will check and get back to you.",
"mediaUrls": [],
"sentAt": "2026-09-21T10:05:00.000Z"
}
}Webhook payloads are event notifications, not complete message objects. In particular, message.received does not include mediaUrls; fetch the messages endpoint for current message details. Media-only messages can have null content.
Verify signatures against the raw request body, acknowledge promptly with a 2xx response, and handle duplicate notifications idempotently. Use the message ID and event type to correlate updates, and fetch the latest message state when reconciling delayed events. See Delivery and retries.
Handling request failures
Handle validation errors (400), missing conversations or messages (404), cancellation conflicts (409), and expired messaging windows (422) separately from asynchronous message.failed events. Requests rejected before acceptance do not represent a pending message to track. Correct the request or wait for a new inbound message as appropriate, instead of retrying an expired-window reply unchanged.