How to Send Facebook Messenger Messages via API
Send and receive Facebook Messenger messages programmatically — Page chats, PSIDs, the 24-hour window, attachments, reactions, and read receipts via the Postproxy API.
What the Messenger API covers
Messenger messaging runs through Meta’s Send API, scoped to a Facebook Page: a Page can reply to people who message it, react to their messages, send attachments, and read delivery and read receipts. It cannot start a conversation with someone who has never contacted the Page, and it cannot message personal profiles — Page-to-person only.
Doing this natively means a Meta app with pages_messaging, App Review for that permission, a Page access token per Page, a verified webhook endpoint, and PSID handling. Through Postproxy those are already in place; what you work with is a chat ID and a message body.
The model: chats and messages
A Chat is a conversation between your Facebook Page and one person; Messages inside it are inbound or outbound. Postproxy holds the Page token and the Meta webhook subscription — no Meta app review, no PSID plumbing on your side.
Sends are asynchronous: 202 Accepted with status: "pending", then "published" with external_id once Meta confirms. Failures land in failed_waiting_for_retry (retried with backoff) or failed, with the platform error passed through in error_details.
Send a message
Find the chat, send into it:
# Chats for the Page, most recent activity firstcurl "https://api.postproxy.dev/api/profiles/PROFILE_ID/chats" \ -H "Authorization: Bearer YOUR_API_KEY"
# Sendcurl -X POST "https://api.postproxy.dev/api/chats/CHAT_ID/messages" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "body": "Your order shipped today — tracking inside the app." }'If the Page hasn’t messaged this person yet but you know their PSID (Page-scoped ID, e.g. from an earlier webhook), create the chat first — the call is idempotent and returns the existing chat if there is one:
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": "PSID", "participant_name": "Jane Doe" }'The 24-hour window
Meta permits free-form Messenger sends only within 24 hours of the person’s last inbound message. The chat’s last_inbound_at tells you where you stand before sending. The exception is a private reply to a Page post comment, which opens a chat up to 7 days after the comment — it works on Facebook exactly as on Instagram.
What you can send, by how much time has passed since that last inbound message:
| Since last inbound | Free-form reply | How |
|---|---|---|
| Under 24 hours | Yes | Normal send |
| 24 hours to 7 days | Human replies only | tag: "HUMAN_AGENT" |
| Over 7 days | No | Wait for the person to write again |
| Any time, after a comment | Yes, once per comment | Private reply |
Replying after the window closes
HUMAN_AGENT is Meta’s message tag for a human following up on someone’s own inquiry after the 24-hour window has shut. It extends the reply window to 7 days from the last inbound message:
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": "Following up on your order.", "tag": "HUMAN_AGENT" }'The tag is for a human resolving the person’s own question — nothing else. Marketing, offers, re-engagement blasts and automated follow-ups sent under it are policy violations, and the penalty is suspension of that Page’s messaging capability. It applies on Facebook and Instagram, and is ignored on Telegram. Details: the HUMAN_AGENT tag.
Receive messages with webhooks
Inbound messages arrive as message.received events on your webhook endpoint — payload includes the text, attachments (already mirrored to durable storage), and the chat ID to reply into. Messenger also reports lifecycle: message.sent, message.delivered, and message.read map to external_delivered_at / external_read_at on the message.
A reply bot is the webhook handler plus the send call above — match on event.type === "message.received", generate a response, POST it to event.data.object.chat_id.
Attachments
One attachment per send (a Meta Send API limit), as a URL, multipart upload, or base64 — and a send is either text or media, never both:
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://yourcdn.com/receipt-4821.pdf"] }'Files are copied to durable storage before dispatch, so the stored url never expires.
Reactions
React to a customer’s message from the Page:
curl -X POST "https://api.postproxy.dev/api/messages/MESSAGE_ID/react" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reaction": "like" }'Messenger expects a unicode emoji; named reactions (love, like, dislike, smile, wow, sad, angry) are translated for you, or pass emoji directly to override. A second react replaces the first — Meta doesn’t stack them. Remove with DELETE /api/messages/MESSAGE_ID/unreact.
Routing signals
Chat metadata carries the owning page_id, and each chat gets the participant’s name and avatar fetched from Meta. Follow/verified signals (is_verified_user, is_user_follow_business, follower count) are an Instagram-only feature — Messenger’s PSID lookup doesn’t return them.
What fails, and why
| Symptom | Cause |
|---|---|
| Send rejected outside 24 hours | Window closed — use HUMAN_AGENT if a human is replying, otherwise wait |
failed_waiting_for_retry | Transient Meta error; retried with backoff, no action needed |
failed with a Meta error in error_details | Permanent — the platform’s own message says which rule was hit |
| Text and media in one send | Not supported; send them as two messages |
| Second reaction doesn’t appear | Meta replaces rather than stacks reactions |
| No inbound events arriving | Page not subscribed, or the webhook endpoint is not returning 2xx |
Messenger vs the other DM platforms
| Capability | Telegram | Bluesky | ||
|---|---|---|---|---|
| Text | Yes | Yes | Yes | Yes |
| Attachments | Yes | Yes | Yes | No |
| Reactions | Yes | Yes | No | No |
| Delivery / read receipts | Yes | Yes | No | No |
| Inbound delivery | Webhook | Webhook | Webhook | Poller (~5 min) |
Same endpoints across all four — which is what makes a unified inbox a rendering exercise instead of four integrations. To hand the person tappable options instead of free text, add quick replies and buttons to a send. Full reference: Direct Messages API.