Skip to content

Direct Messages API Reference

The Direct Messages API lets you read and send 1:1 messages on profiles that support DMs. Outbound sends are processed asynchronously.

A conversation between your profile and a participant is represented by a Chat. Each chat has zero or more Messages (inbound or outbound).

Direct messages are supported on Facebook Messenger (facebook), Instagram (instagram), Telegram (telegram), and Bluesky (bluesky). Attempting to use the DM API on any other platform returns 422 Unprocessable Entity.

CapabilityFacebookInstagramTelegramBluesky
Send / receive textYesYesYesYes
AttachmentsYesYesYesNo
ReactionsYesYesNoNo
Edit outbound messageNoNoYesNo
Delivery / read receiptsYesYesNoNo
Archive (mute) chatNoNoNoYes
Private reply to commentYesYesNoNo
Inbound deliveryWebhookWebhookWebhookPoller (~5 min)

Telegram and Bluesky differ from the Meta networks in several ways — see Telegram notes and Bluesky notes.

MethodEndpointDescription
GET/api/profiles/:profile_id/chatsList chats for a profile
POST/api/profiles/:profile_id/chatsFind or create a chat by participant
GET/api/chats/:idGet a single chat
GET/api/chats/:chat_id/messagesList messages in a chat
POST/api/chats/:chat_id/messagesSend an outbound message
GET/api/messages/:idGet a single message
PATCH/api/messages/:idEdit an outbound message (Telegram only)
POST/api/messages/:id/reactReact to a message (Facebook & Instagram)
DELETE/api/messages/:id/unreactRemove this account’s reaction
POST/api/chats/:id/archiveArchive a chat (Bluesky only)
DELETE/api/chats/:id/archiveUnarchive a chat (Bluesky only)
POST/api/posts/:post_id/comments/:id/private_replyDM the author of a comment
  • A chat :id accepts the Postproxy hashid OR the platform’s external_conversation_id.
  • A message :id accepts the Postproxy hashid OR the platform’s external_id.

Meta only permits free-form messages to a participant within 24 hours of their last inbound message. Send freely inside that window. Sending outside the 24-hour window is not currently supported through the API.

Telegram has no messaging window — once the user has DM’d the bot, it can reply at any time. The bot can only message a user after that user has DM’d the bot at least once (see Telegram notes).

Bluesky has no messaging window either. The recipient must have DMs enabled and satisfy their own “who can message me” preference, otherwise the send fails with a Bluesky-side error.


GET /api/profiles/:profile_id/chats

Returns a paginated list of chats for a profile, ordered by last_message_at descending (most recent first).

ParameterTypeRequiredDefaultDescription
pageintegerNo0Page number (zero-indexed)
per_pageintegerNo20Items per page
beforestringNo-ISO 8601 timestamp — only chats with last_message_at before this
afterstringNo-ISO 8601 timestamp — only chats with last_message_at after this
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/PROFILE_ID/chats?per_page=20" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"total": 12,
"page": 0,
"per_page": 20,
"data": [
{
"id": "chat_xyz789",
"profile_id": "prof_abc123",
"platform": "instagram",
"participant_external_id": "igsid_8675309",
"participant_username": "jane_doe",
"participant_name": "Jane Doe",
"participant_avatar_url": "https://storage.postproxy.dev/.../chat_avatar_42.jpg",
"external_conversation_id": null,
"last_inbound_at": "2026-05-31T14:02:00.000Z",
"last_outbound_at": "2026-05-31T15:10:00.000Z",
"last_message_at": "2026-05-31T15:10:00.000Z",
"metadata": {
"is_verified_user": false,
"is_user_follow_business": true,
"is_business_follow_user": false,
"follower_count": 482,
"participant_fetched_at": "2026-05-31T15:10:05Z"
},
"created_at": "2026-04-12T08:00:00.000Z"
}
]
}

POST /api/profiles/:profile_id/chats

Idempotently creates a chat for a (profile, participant_external_id) pair. If a chat with that participant already exists, the existing chat is returned (and any provided participant fields are updated). Use this before sending an outbound message to a participant the profile has not yet messaged.

ParameterTypeRequiredDescription
participant_external_idstringYesPlatform participant ID (Instagram-scoped user ID for IG, PSID for Facebook, DID for Bluesky)
participant_usernamestringNoDisplay username for the participant
participant_namestringNoDisplay name for the participant
Terminal window
curl -X POST "https://api.postproxy.dev/api/profiles/PROFILE_ID/chats" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"participant_external_id": "PARTICIPANT_ID",
"participant_username": "jane_doe"
}'

Response — 201 Created for a new chat, 200 OK if it already existed (idempotent):

{
"id": "chat_xyz789",
"profile_id": "prof_abc123",
"platform": "instagram",
"participant_external_id": "igsid_8675309",
"participant_username": "jane_doe",
"participant_name": null,
"participant_avatar_url": null,
"external_conversation_id": null,
"last_inbound_at": null,
"last_outbound_at": null,
"last_message_at": null,
"metadata": null,
"created_at": "2026-05-31T15:30:00.000Z"
}

GET /api/chats/:id

Fetch a single chat by Postproxy hashid or by external_conversation_id.

Terminal window
curl -X GET "https://api.postproxy.dev/api/chats/CHAT_ID" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"id": "chat_xyz789",
"profile_id": "prof_abc123",
"platform": "instagram",
"participant_external_id": "igsid_8675309",
"participant_username": "jane_doe",
"participant_name": "Jane Doe",
"participant_avatar_url": "https://storage.postproxy.dev/.../chat_avatar_42.jpg",
"external_conversation_id": null,
"last_inbound_at": "2026-05-31T14:02:00.000Z",
"last_outbound_at": "2026-05-31T15:10:00.000Z",
"last_message_at": "2026-05-31T15:10:00.000Z",
"metadata": {
"is_verified_user": false,
"is_user_follow_business": true,
"is_business_follow_user": false,
"follower_count": 482,
"participant_fetched_at": "2026-05-31T15:10:05Z"
},
"created_at": "2026-04-12T08:00:00.000Z"
}

GET /api/chats/:chat_id/messages

Returns a paginated list of messages in a chat, ordered by external_posted_at descending (pending outbound messages without external_posted_at are ordered by created_at).

ParameterTypeRequiredDefaultDescription
pageintegerNo0Page number (zero-indexed)
per_pageintegerNo20Items per page
directionstringNo-Filter by direction: inbound or outbound
statusstringNo-Filter by status
Terminal window
curl -X GET "https://api.postproxy.dev/api/chats/CHAT_ID/messages?direction=inbound" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"total": 8,
"page": 0,
"per_page": 20,
"data": [
{
"id": "msg_111",
"chat_id": "chat_xyz789",
"external_id": "mid.abc123",
"direction": "inbound",
"body": "Hey, do you ship internationally?",
"status": "received",
"external_comment_id": null,
"error_message": null,
"platform_data": null,
"external_posted_at": "2026-05-31T14:02:00.000Z",
"external_delivered_at": "2026-05-31T14:02:00.500Z",
"external_read_at": "2026-05-31T14:03:12.000Z",
"external_edited_at": null,
"reply_to_external_id": null,
"external_deleted_at": null,
"reactions": [
{ "sender_external_id": "psid_123", "emoji": "❤️", "reaction": "love", "at": "2026-05-31T14:04:00.000Z" }
],
"attachments": [],
"is_unsupported": false,
"created_at": "2026-05-31T14:02:01.000Z"
}
]
}

POST /api/chats/:chat_id/messages

Creates an outbound message and queues it for delivery. The message is created immediately with status: "pending" and published asynchronously. Once published, the status updates to "published" and external_id is populated. On failure it becomes "failed_waiting_for_retry" (retried with backoff) or "failed".

ParameterTypeRequiredDescription
bodystringConditionalMessage text. Required when media is empty.
mediaarrayConditionalA single media attachment. See Send media.
reply_to_external_idstringNoTelegram only. Platform message_id to thread the reply under.
reply_markupobjectNoTelegram only. Inline keyboard, reply keyboard, force-reply, or remove-keyboard payload. See Telegram notes.

Each send accepts either text or a single attachment, never both.

Terminal window
curl -X POST "https://api.postproxy.dev/api/chats/CHAT_ID/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "body": "Yes, we ship worldwide!" }'

Response (202 Accepted):

{
"id": "msg_222",
"chat_id": "chat_xyz789",
"external_id": null,
"direction": "outbound",
"body": "Yes, we ship worldwide!",
"status": "pending",
"external_comment_id": null,
"error_message": null,
"platform_data": null,
"external_posted_at": null,
"attachments": [],
"created_at": "2026-05-31T15:30:05.000Z"
}

media accepts the same shapes as the Posts API — a multipart upload, a public URL, a base64 data URI, or a base64 hash. Currently one attachment per send (Meta Send API limit); passing more returns 422. Attachments are downloaded to durable storage before dispatch, so the returned url is stable and does not expire. Bluesky does not support attachments — media returns 422 on Bluesky chats.

Terminal window
# Send an image by hosted URL
curl -X POST "https://api.postproxy.dev/api/chats/CHAT_ID/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "media": ["https://cdn.example.com/photo.png"] }'
# Or upload a local file (multipart)
curl -X POST "https://api.postproxy.dev/api/chats/CHAT_ID/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "media[]=@./photo.png"

Response (202 Accepted):

{
"id": "msg_223",
"chat_id": "chat_xyz789",
"external_id": null,
"direction": "outbound",
"body": null,
"status": "pending",
"external_comment_id": null,
"error_message": null,
"platform_data": null,
"external_posted_at": null,
"attachments": [
{
"id": "att_zxc987",
"type": "image",
"url": "https://cdn.example.com/photo.png",
"status": "pending",
"external_id": null
}
],
"created_at": "2026-05-31T15:31:00.000Z"
}

Neither body nor media supplied (400):

{ "error": "body or media is required" }

Both body and media supplied (422):

{ "error": "Direct messages support either text or attachment per send, not both" }

More than one attachment (422):

{ "error": "Direct messages support one attachment per send" }

GET /api/messages/:id

Fetch a single message by Postproxy hashid or by platform external_id.

Terminal window
curl -X GET "https://api.postproxy.dev/api/messages/MESSAGE_ID" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"id": "msg_111",
"chat_id": "chat_xyz789",
"external_id": "mid.abc123",
"direction": "inbound",
"body": "Hey, do you ship internationally?",
"status": "received",
"external_comment_id": null,
"error_message": null,
"platform_data": null,
"external_posted_at": "2026-05-31T14:02:00.000Z",
"external_delivered_at": "2026-05-31T14:02:00.500Z",
"external_read_at": "2026-05-31T14:03:12.000Z",
"external_edited_at": null,
"reply_to_external_id": null,
"external_deleted_at": null,
"reactions": [],
"attachments": [],
"is_unsupported": false,
"created_at": "2026-05-31T14:02:01.000Z"
}

PATCH /api/messages/:id

Edit a previously-sent outbound message on the platform. Telegram only — Facebook and Instagram do not expose outbound edits to bots, so calling this on a non-Telegram message returns 422. Edits update both the platform-side message and the Postproxy record; external_edited_at is set and a message.edited webhook fires.

ParameterTypeRequiredDescription
bodystringConditionalNew text. For media messages this becomes the new caption. At least one of body / reply_markup is required.
reply_markupobjectConditionalNew inline keyboard (or other reply_markup). Pass an empty {} to remove the existing keyboard.
Terminal window
curl -X PATCH "https://api.postproxy.dev/api/messages/MESSAGE_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "body": "Updated answer: we ship to 90+ countries." }'

Response:

{
"id": "msg_222",
"chat_id": "chat_tg456",
"external_id": "5821",
"direction": "outbound",
"body": "Updated answer: we ship to 90+ countries.",
"status": "published",
"external_comment_id": null,
"error_message": null,
"platform_data": null,
"external_posted_at": "2026-05-31T15:30:10.000Z",
"external_delivered_at": null,
"external_read_at": null,
"external_edited_at": "2026-05-31T15:45:00.000Z",
"reply_to_external_id": null,
"reply_markup": null,
"external_deleted_at": null,
"reactions": [],
"attachments": [],
"is_unsupported": false,
"created_at": "2026-05-31T15:30:05.000Z"
}
  • 422Editing messages is only supported on Telegram
  • 422Only published outbound messages can be edited
  • 400body or reply_markup is required
  • 422 — Telegram-side errors are passed through verbatim (e.g. Bad Request: message is not modified).

POST /api/messages/:id/react

Add a reaction from your business account to a message. The reaction appears in the recipient’s thread, is persisted on the message under your profile.external_id, and fires a reaction.received webhook. Supported on Facebook Messenger and Instagram Direct only — other networks (including Telegram) return 422.

ParameterTypeRequiredDescription
reactionstringNoNamed reaction, defaults to love.
emojistringNoUnicode emoji. Forwarded to Instagram; on Messenger it overrides reaction as the literal value sent to Meta.

Instagram’s outbound reactions API accepts only love (you may pass an emoji alongside). Facebook Messenger expects a unicode emoji — Postproxy translates the common named reactions before calling Meta:

reactionSent to Messenger as
love
like👍
dislike👎
smile😆
wow😮
sad😢
angry😡

A supplied emoji always takes precedence over the named reaction for Messenger. A second react from the same account replaces the previous one — Meta does not stack reactions.

Terminal window
# Instagram — react with a named reaction (love only)
curl -X POST "https://api.postproxy.dev/api/messages/MESSAGE_ID/react" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d 'reaction=love' \
-d 'emoji=❤️'
# Facebook Messenger — react by name (auto-translated to the emoji)
# curl -X POST "https://api.postproxy.dev/api/messages/MESSAGE_ID/react" \
# -H "Authorization: Bearer YOUR_API_KEY" \
# -d 'reaction=smile'

Response:

{
"id": "msg_111",
"chat_id": "chat_xyz789",
"external_id": "mid.abc123",
"direction": "inbound",
"body": "Hey, do you ship internationally?",
"status": "received",
"external_comment_id": null,
"error_message": null,
"platform_data": null,
"external_posted_at": "2026-05-31T14:02:00.000Z",
"external_delivered_at": "2026-05-31T14:02:00.500Z",
"external_read_at": "2026-05-31T14:03:12.000Z",
"external_edited_at": null,
"reply_to_external_id": null,
"external_deleted_at": null,
"reactions": [
{ "sender_external_id": "prof_external_42", "emoji": "❤️", "reaction": "love", "at": "2026-05-31T15:50:00.000Z" }
],
"attachments": [],
"is_unsupported": false,
"created_at": "2026-05-31T14:02:01.000Z"
}

DELETE /api/messages/:id/unreact

Remove the business account’s reaction from a message. Acts on whichever reaction the current account previously set; does not affect reactions from other senders. Returns the message with the account’s entry stripped from reactions, and fires a reaction.received webhook with action: "unreact".

Terminal window
curl -X DELETE "https://api.postproxy.dev/api/messages/MESSAGE_ID/unreact" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"id": "msg_111",
"chat_id": "chat_xyz789",
"external_id": "mid.abc123",
"direction": "inbound",
"body": "Hey, do you ship internationally?",
"status": "received",
"external_comment_id": null,
"error_message": null,
"platform_data": null,
"external_posted_at": "2026-05-31T14:02:00.000Z",
"external_delivered_at": "2026-05-31T14:02:00.500Z",
"external_read_at": "2026-05-31T14:03:12.000Z",
"external_edited_at": null,
"reply_to_external_id": null,
"external_deleted_at": null,
"reactions": [],
"attachments": [],
"is_unsupported": false,
"created_at": "2026-05-31T14:02:01.000Z"
}

POST /api/chats/:id/archive

Bluesky only. Archives (mutes) a conversation via chat.bsky.convo.muteConvo. The new state is reflected in the chat’s metadata.muted and the top-level archived boolean.

Terminal window
curl -X POST "https://api.postproxy.dev/api/chats/CHAT_ID/archive" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"id": "chat_bsky01",
"profile_id": "prof_bsky99",
"platform": "bluesky",
"participant_external_id": "did:plc:examplez7y6x5w4v3u2",
"participant_username": "jane.bsky.social",
"participant_name": "Jane",
"participant_avatar_url": null,
"external_conversation_id": "3labcconvoid",
"last_inbound_at": "2026-05-31T14:02:00.000Z",
"last_outbound_at": "2026-05-31T15:10:00.000Z",
"last_message_at": "2026-05-31T15:10:00.000Z",
"metadata": {
"muted": true
},
"archived": true,
"created_at": "2026-04-12T08:00:00.000Z"
}

DELETE /api/chats/:id/archive

Bluesky only. Unarchives (unmutes) a conversation via chat.bsky.convo.unmuteConvo.

Terminal window
curl -X DELETE "https://api.postproxy.dev/api/chats/CHAT_ID/archive" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"id": "chat_bsky01",
"profile_id": "prof_bsky99",
"platform": "bluesky",
"participant_external_id": "did:plc:examplez7y6x5w4v3u2",
"participant_username": "jane.bsky.social",
"participant_name": "Jane",
"participant_avatar_url": null,
"external_conversation_id": "3labcconvoid",
"last_inbound_at": "2026-05-31T14:02:00.000Z",
"last_outbound_at": "2026-05-31T15:10:00.000Z",
"last_message_at": "2026-05-31T15:10:00.000Z",
"metadata": {
"muted": false
},
"archived": false,
"created_at": "2026-04-12T08:00:00.000Z"
}

POST /api/posts/:post_id/comments/:id/private_reply

Sends a DM to the author of a comment, in response to that specific comment, using Meta’s “Private Replies” mechanism. This:

  • Bypasses the 24h messaging window — you can private-reply to comments up to 7 days old (Meta-enforced).
  • Does not require an existing chat — one is created (or reused) automatically, keyed by the comment’s author.
  • Allows one private reply per comment, ever (Meta limit).
  • Is available on Instagram and Facebook Page comments only.
ParameterTypeRequiredDescription
profile_idstringYesProfile hashid (Instagram or Facebook)
ParameterTypeRequiredDescription
textstringYesDM text
Terminal window
curl -X POST "https://api.postproxy.dev/api/posts/POST_ID/comments/COMMENT_ID/private_reply?profile_id=PROFILE_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Thanks for your comment — DM-ing you the details." }'

Response (202 Accepted) — note the returned object is a Message, with chat_id pointing at the chat for the comment’s author:

{
"id": "msg_333",
"chat_id": "chat_xyz789",
"external_id": null,
"direction": "outbound",
"body": "Thanks for your comment — DM-ing you the details.",
"status": "pending",
"external_comment_id": "17858893269123456",
"error_message": null,
"platform_data": null,
"external_posted_at": null,
"created_at": "2026-05-31T15:35:00.000Z"
}

Telegram DMs use the Bot API and offer richer capabilities than Meta in some areas, but work differently in others.

  • Inbound DMs, edits, and media. Photos, videos, documents, animations (GIFs), audio, voice notes, and stickers all arrive via webhook and are mirrored into durable storage. Inbound edits fire message.edited.
  • Outbound edits. PATCH /api/messages/:id updates the platform-side message — see Edit message.
  • Reply threading. Pass reply_to_external_id on send (or reply_markup: { force_reply: true }) to thread under a specific message.
  • Keyboards. reply_markup is passed through to Telegram unchanged — inline keyboards (inline_keyboard), custom reply keyboards (keyboard, with one_time_keyboard / resize_keyboard), force_reply, and { "remove_keyboard": true }.
  • Callback queries. When a user taps an inline-keyboard button with callback_data, Postproxy auto-acks and creates an inbound message with body set to the callback data and platform_data.kind == "callback_query". Subscribe to message.received to react.

Differences from Meta:

  • Bot must be DM’d first. A bot can only message a user after that user has DM’d it at least once (typically via /start). Sending otherwise returns 422 with Telegram’s Forbidden: bot can't initiate conversation with a user.
  • No reactions, no private-reply-to-comment. Both return 422 on Telegram chats.
  • No delivery / read receipts and no deletion events. external_delivered_at / external_read_at stay null; message.delivered / message.read / message.deleted never fire for Telegram.

Bluesky DMs go through the AT Protocol chat service (chat.bsky.convo.*). They behave differently from the other platforms:

  • No webhooks. Inbound messages arrive via a per-profile poller that runs every 5 minutes and walks chat.bsky.convo.getLog. New inbound messages still fire message.received, with up to ~5 minutes of latency.
  • No attachments, reactions, or edits. media, the React / Unreact endpoints, and PATCH /api/messages/:id all return 422 on Bluesky chats.
  • Archive = mute. Archive / unarchive map to muteConvo / unmuteConvo; the chat response carries an archived boolean.
  • Starting a chat by DID. Pass the recipient’s DID as participant_external_id. The Bluesky convo id is resolved lazily on first send and stored as external_conversation_id.
  • No delivery / read receipts. Message deletions are best-effort (external_deleted_at is stamped when the participant deletes for self).

A chat’s metadata object carries platform-specific information about the participant, fetched asynchronously when the chat is first created and refreshed at most once per hour on inbound messages. Keys are only present when the platform returns a value — treat missing keys as unknown, not false.

KeyPlatformTypeDescription
page_idfacebookstringPage ID this chat is owned by (required for sending)
participant_fetched_atallstringISO 8601 timestamp of the last recipient-info refresh
language_codetelegramstringIETF language tag reported by Telegram for the user
mutedblueskybooleanWhether the conversation is muted/archived
is_verified_userinstagram, facebookbooleanParticipant has a verified account
is_user_follow_businessinstagram, facebookbooleanParticipant follows the business profile
is_business_follow_userinstagram, facebookbooleanThe business profile follows the participant
follower_countinstagramintegerParticipant’s follower count
is_payment_enabledfacebookbooleanParticipant has Messenger payments enabled
is_subscribed_to_messengerfacebookbooleanParticipant is subscribed to the Page via Messenger

FieldTypeDescription
idstringChat hashid
profile_idstringProfile hashid this chat belongs to
platformstringNetwork ID (facebook, instagram, telegram, bluesky)
participant_external_idstringPlatform-native participant ID
participant_usernamestring|nullParticipant’s username (when known)
participant_namestring|nullParticipant’s display name (when known)
participant_avatar_urlstring|nullStable storage URL of the participant’s profile picture (null until fetched)
external_conversation_idstring|nullPlatform’s native conversation ID (when known)
last_inbound_atstring|nullISO 8601 timestamp of last inbound message
last_outbound_atstring|nullISO 8601 timestamp of last outbound message
last_message_atstring|nullISO 8601 timestamp of last message in either direction
archivedbooleanBluesky only — whether the chat is muted/archived
metadataobject|nullPlatform-specific metadata. See Participant metadata.
created_atstringISO 8601 timestamp of chat creation
FieldTypeDescription
idstringMessage hashid
chat_idstringChat hashid
external_idstring|nullPlatform-native message ID (null while pending)
directionstringinbound or outbound
bodystring|nullMessage text
statusstringSee Message statuses
external_comment_idstring|nullSet when the message was sent as a private reply to a comment
error_messagestring|nullLast error from the platform (when failed)
platform_dataobject|nullPlatform-specific payload
external_posted_atstring|nullISO 8601 timestamp from the platform
external_delivered_atstring|nullWhen the platform confirmed delivery (outbound; FB & IG only)
external_read_atstring|nullWhen the recipient read the message (outbound; FB & IG only)
external_edited_atstring|nullISO 8601 timestamp of the most recent edit; body reflects the latest edit
reply_to_external_idstring|nullPlatform message ID this message replies to (inbound replies, outbound reply_to_external_id, or a Telegram callback query’s source)
reply_markupobject|nullTelegram only — the reply_markup attached to the message
external_deleted_atstring|nullWhen the message was deleted/unsent on the platform (treat as redacted)
reactionsarrayReactions on the message — each entry: sender_external_id, emoji, reaction, at. Reflects the live state. See React to message.
attachmentsarrayMedia attached to the message. See Media attachments.
is_unsupportedbooleantrue when the platform marked the message as unsupported (e.g. an unprocessable voice clip)
created_atstringISO 8601 timestamp of record creation

Both inbound and outbound messages can carry attachments. Inbound media is mirrored from the platform CDN to durable storage shortly after the webhook arrives; outbound files supplied via media are uploaded before the send. In both cases the url is stable — it does not expire.

FieldTypeDescription
idstringAttachment hashid
typestringimage, video, audio, sticker, file
urlstring|nullStable storage URL. While status is pending this may temporarily fall back to the source URL.
statusstringpending, processed, or failed
external_idstring|nullPlatform-side identifier (e.g. a Messenger sticker ID), when provided
StatusDirectionDescription
pendingoutboundQueued, not yet delivered to the platform
publishedoutboundSuccessfully delivered to the platform
failed_waiting_for_retryoutboundSend failed; will be retried with backoff
failedoutboundSend failed permanently (retries exhausted)
receivedinboundReceived from the platform

Subscribe to message and reaction events with the Webhooks API. Relevant event types include message.received, message.sent, message.delivered, message.read, message.edited, message.deleted, message.failed_waiting_for_retry, message.failed, and reaction.received. See the Webhooks reference for payload shapes and per-platform firing rules.