Engagement
Inbox
Read and send direct messages on Instagram, Facebook and Bluesky, including quick replies, buttons and cards.
Read and answer an account's direct messages, start conversations, and send buttons, quick replies and cards. Messages are read live from the platform.
Support by Platform
| Feature | Bluesky | ||
|---|---|---|---|
| List and read | |||
| Start a conversation with | Username or user id | Page-scoped id | Handle or DID |
| Message length | 1,000 characters | 2,000 characters | 1,000 graphemes |
| Sending window | 24 hours | 24 hours | None |
| Rich messages | No | ||
| Received attachments | No | ||
| Ice breakers and menu | No | No | |
| Inbox webhooks | No | ||
| Private replies, automations | No |
Other platforms return 400 UNSUPPORTED_PLATFORM.
The account needs messaging access, approved at connect. Without it you get 422 INBOX_PERMISSION, so reconnect and approve every permission. On Bluesky, the App Password needs direct message access.
List Conversations
GET /v1/accounts/{id}/conversations lists conversations, newest first. Each page holds 50 by default, or up to 100 with limit. Pass nextCursor as cursor until hasMore is false to read them all.
import { Configuration, InboxApi } from "@onepostly/sdk";
const inboxApi = new InboxApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await inboxApi.listConversations({
id: "acc_YOUR_INSTAGRAM_ACCOUNT_ID",
});
console.log(response);{
"data": [
{
"id": "cnv_u1rTltEvz7pEbzpQOLjQAlMVsqWCdakS9U2wjAFB6CV6SYgY3Bx8GTrVhAn9njOg3eB2YWHX",
"accountId": "acc_YOUR_INSTAGRAM_ACCOUNT_ID",
"members": [
{
"id": "17841401234567890",
"username": "yourbrand",
"displayName": "yourbrand"
},
{
"id": "6873452198765432",
"username": "jane_doe",
"displayName": "jane_doe"
}
],
"muted": false,
"unreadCount": 0,
"lastMessage": {
"id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDAxMjM0NTY3ODk6MzQwMjgyMzY2ODQxNzEwMzAwOTQ5MTI4MTM2NDEyOTg3NjU0MzIxMDk4NzY1NDMy",
"text": "Do you ship to Canada?",
"sentAt": "2027-03-15T11:42:08.000Z"
}
},
{
"id": "cnv_UYeVH71CtvWwPasNAhEA9WXmVPAMJhQLmnGMapF5MxmPu59aNUBxoHt0T0YnAq98AujCL19c",
"accountId": "acc_YOUR_INSTAGRAM_ACCOUNT_ID",
"members": [
{
"id": "17841401234567890",
"username": "yourbrand",
"displayName": "yourbrand"
},
{
"id": "7120984456321987",
"username": "mark.builds",
"displayName": "mark.builds"
}
],
"muted": false,
"unreadCount": 0,
"lastMessage": {
"id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDAxMjM0NTY3ODk6MzQwMjgyMzY2ODQxNzEwMzAwOTQ5MTI4MTM1MzQ1Njc4OTAxMjM0NTY3ODkwMTIz",
"text": "Thanks, it arrived today!",
"sentAt": "2027-03-15T10:17:45.000Z"
}
}
],
"nextCursor": null,
"hasMore": false
}| Field | Contains |
|---|---|
id | The cnv_ id |
accountId | The conversation's account |
members | Each person's platform id, username and displayName, the last two null when hidden |
muted | true when muted. Bluesky only |
unreadCount | Unread messages, or 0 when not reported |
lastMessage | The newest message's id, text and sentAt, or null |
GET /v1/accounts/{id}/conversations/{conversationId} returns a single conversation, with the same fields as the list.
Conversation Ids
A cnv_ id works only in the workspace that received it. On Instagram and Facebook, one conversation can have two ids. List Conversations returns one, and Start a Conversation and webhooks return the other. Both work.
Read Messages
GET /v1/accounts/{id}/conversations/{conversationId}/messages lists messages, newest first. Paging works the same as for conversations.
import { Configuration, InboxApi } from "@onepostly/sdk";
const inboxApi = new InboxApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await inboxApi.listMessages({
id: "acc_YOUR_INSTAGRAM_ACCOUNT_ID",
conversationId: "cnv_YOUR_CONVERSATION_ID",
});
console.log(response);{
"data": [
{
"id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDAxMjM0NTY3ODk6MzQwMjgyMzY2ODQxNzEwMzAwOTQ5MTI4MTM2NDEyOTg3NjU0MzIxMDk4NzY1NDMy",
"conversationId": "cnv_YOUR_CONVERSATION_ID",
"text": "Do you ship to Canada?",
"senderId": "6873452198765432",
"sentAt": "2027-03-15T11:42:08.000Z",
"attachments": []
},
{
"id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDAxMjM0NTY3ODk6MzQwMjgyMzY2ODQxNzEwMzAwOTQ5MTI4MTM2NDEyOTg3NjU0MzIxMDk4NzY1NDE5",
"conversationId": "cnv_YOUR_CONVERSATION_ID",
"text": "Yes, it is in stock in every size.",
"senderId": "17841401234567890",
"sentAt": "2027-03-15T11:40:02.000Z",
"attachments": []
},
{
"id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDAxMjM0NTY3ODk6MzQwMjgyMzY2ODQxNzEwMzAwOTQ5MTI4MTM2NDEyOTg3NjU0MzIxMDk4NzY1NDA3",
"conversationId": "cnv_YOUR_CONVERSATION_ID",
"text": "Hi! Is this jacket still available?",
"senderId": "6873452198765432",
"sentAt": "2027-03-15T11:38:40.000Z",
"attachments": []
},
{
"id": "aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjE3ODQxNDAxMjM0NTY3ODk6MzQwMjgyMzY2ODQxNzEwMzAwOTQ5MTI4MTM2NDEyOTg3NjU0MzIxMDk4NzY1Mzk1",
"conversationId": "cnv_YOUR_CONVERSATION_ID",
"text": null,
"senderId": "6873452198765432",
"sentAt": "2027-03-15T11:38:15.000Z",
"attachments": [
{
"type": "image",
"url": "https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=18043219876543210&signature=AbxQ3kLm9pRtYv2WcZ8sN1"
}
]
}
],
"nextCursor": null,
"hasMore": false
}| Field | Contains |
|---|---|
id | The message's platform id |
conversationId | The message's conversation |
text | The text, or null for attachments only or a deleted Bluesky message |
senderId | An Instagram-scoped id, a Page-scoped id or a Bluesky DID |
sentAt | When it was sent, or null |
attachments | Each with a type and a direct url. Empty on Bluesky |
An attachment type is image, video, audio, file or other.
Start a Conversation
POST /v1/accounts/{id}/conversations with a recipient starts a one-to-one conversation with that person and returns 201.
import { Configuration, InboxApi } from "@onepostly/sdk";
const inboxApi = new InboxApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await inboxApi.createConversation({
id: "acc_YOUR_BLUESKY_ACCOUNT_ID",
createConversationBody: {
"recipient": "alice.bsky.social"
},
});
console.log(response);{
"id": "cnv_xZWwUFsb6q2nrlD7GUiqQxNHfiqbgdNAjqlcvDLTpafW1UHpqYkJAIRnqf5uJ71zS3gigSZp",
"accountId": "acc_YOUR_BLUESKY_ACCOUNT_ID",
"members": [
{
"id": "did:plc:4y7hqd2kz3rbe6mfxw5tnc2a",
"username": "yourbrand.bsky.social",
"displayName": "Your Brand"
},
{
"id": "did:plc:z72i7hdynmk6r22z27h6tvur",
"username": "alice.bsky.social",
"displayName": "Alice"
}
],
"muted": false,
"unreadCount": 0,
"lastMessage": null
}| Platform | recipient | Notes |
|---|---|---|
A username, @ optional, or a numeric user id | Usernames work for public Business and Creator accounts. Otherwise use a senderId | |
| A numeric Page-scoped id | Find it in members or a senderId | |
| Bluesky | A handle, @ optional, or a DID | An existing conversation comes back as is |
On Instagram and Facebook, a new conversation is readable once it has a message. Until then you get 404 CONVO_NOT_FOUND and no messages.
The person must have written in the last 24 hours, or sending fails with 409 MESSAGE_WINDOW_EXPIRED. On Bluesky, the recipient's settings decide, and follow-only settings or a block return 409 RECIPIENT_UNREACHABLE.
Send a Message
POST /v1/accounts/{id}/conversations/{conversationId}/messages sends a message and returns 201. text is required and must fit the message length.
import { Configuration, InboxApi } from "@onepostly/sdk";
const inboxApi = new InboxApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await inboxApi.sendMessage({
id: "acc_YOUR_FACEBOOK_ACCOUNT_ID",
conversationId: "cnv_YOUR_CONVERSATION_ID",
sendMessageBody: {
"text": "Yes, we deliver on Sundays from 10:00 to 14:00."
},
});
console.log(response);{
"id": "m_Xq2b7KfRz1nT8vLp3sWcYd9GhJ0aMeU4oQiN5kB6xZtVrHlF2gDsAy7wEcP1uOjI3mK8nL",
"conversationId": "cnv_YOUR_CONVERSATION_ID",
"text": "Yes, we deliver on Sundays from 10:00 to 14:00.",
"senderId": "104987654321098",
"sentAt": "2027-03-15T12:05:31.000Z",
"attachments": []
}Instagram and Facebook accept messages only within 24 hours of the person's last one, and message tags are not available. After the window closes, you get 409 MESSAGE_WINDOW_EXPIRED until they write again.
Messages count toward action limits. Over one, you get 429 QUOTA_EXCEEDED, and error says when to retry. A platform slowdown returns 429 RATE_LIMITED.
Rich Messages
On Instagram and Facebook, these fields add something to open or tap. Bluesky returns 400 ATTACHMENT_UNSUPPORTED or 400 TEMPLATE_UNSUPPORTED.
| Field | Adds | Limits |
|---|---|---|
attachmentUrl | An image, video, audio or PDF | Public HTTPS, up to 2,048 characters |
quickReplies | Tappable replies | Up to 13 |
buttons | Buttons under the text | Up to 3 |
cards | A swipeable carousel | Up to 10, each with up to 3 buttons |
| Item | Fields |
|---|---|
| Quick reply | title up to 20 characters, payload up to 1,000, optional imageUrl |
| Button | type, title up to 20 characters, and url for web_url or payload for postback |
| Card | title and optional subtitle up to 80 characters, optional imageUrl and buttons |
import { Configuration, InboxApi } from "@onepostly/sdk";
const inboxApi = new InboxApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await inboxApi.sendMessage({
id: "acc_YOUR_FACEBOOK_ACCOUNT_ID",
conversationId: "cnv_YOUR_CONVERSATION_ID",
sendMessageBody: {
"text": "Which pickup time works for you?",
"quickReplies": [
{
"title": "Morning",
"payload": "PICKUP_AM"
},
{
"title": "Afternoon",
"payload": "PICKUP_PM"
}
]
},
});
console.log(response);{
"id": "m_Rk5nW8cTq2vYb7LzP4sXdA1fGh9JmE3oUiK6tN0aBwVyQ2rHlC8gDsMx5eFpZ1uOjI7kL4",
"conversationId": "cnv_YOUR_CONVERSATION_ID",
"text": "Which pickup time works for you?",
"senderId": "104987654321098",
"sentAt": "2027-03-15T12:06:10.000Z",
"attachments": []
}A message has one main part, and text is always required.
| You send | The person sees |
|---|---|
attachmentUrl | The attachment only |
cards | The carousel only, without outside buttons |
buttons | The text, cut at 640 characters, with buttons |
Only text | The text |
quickReplies go with any of these, but attachmentUrl together with buttons or cards in one message returns 400 INVALID_TEMPLATE.
The attachmentUrl extension sets how it is sent.
| Extension | Sent as |
|---|---|
.mp4, .mov, .m4v, .webm, .avi | Video |
.mp3, .m4a, .aac, .wav | Audio |
.pdf | File |
| Anything else | Image |
| Tapped | You receive |
|---|---|
| A quick reply | message.received with the title as text. Match on it, since payload is not sent |
A postback button | message.postback with its payload |
A web_url button | Nothing. The link opens |
Messenger Profile
A Facebook Page can greet people with up to 4 ice breaker questions in a new chat, and keep a menu of up to 3 items in every chat.
GET /v1/accounts/{id}/messenger-profile returns the setup.
import { Configuration, AccountsApi } from "@onepostly/sdk";
const accountsApi = new AccountsApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await accountsApi.getMessengerProfile({
id: "acc_YOUR_FACEBOOK_ACCOUNT_ID",
});
console.log(response);{
"accountId": "acc_YOUR_FACEBOOK_ACCOUNT_ID",
"iceBreakers": [
{
"question": "What are your opening hours?",
"payload": "HOURS"
},
{
"question": "Do you deliver?",
"payload": "DELIVERY"
}
],
"persistentMenu": [
{
"type": "web_url",
"title": "Order online",
"url": "https://example.com/order",
"payload": null
}
]
}PUT on the same path changes it. Send iceBreakers, persistentMenu or both. Fields you leave out stay. The response is the setup after the change.
| Field | Each item | Limits |
|---|---|---|
iceBreakers | question and payload | Up to 4. question up to 80 characters, payload up to 1,000 |
persistentMenu | type, title, and url or payload | Up to 3. title up to 20 characters |
A web_url item needs url, and a postback item needs payload.
import { Configuration, AccountsApi } from "@onepostly/sdk";
const accountsApi = new AccountsApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await accountsApi.setMessengerProfile({
id: "acc_YOUR_FACEBOOK_ACCOUNT_ID",
setMessengerProfileBody: {
"iceBreakers": [
{
"question": "What are your opening hours?",
"payload": "HOURS"
},
{
"question": "Do you deliver?",
"payload": "DELIVERY"
}
],
"persistentMenu": [
{
"type": "web_url",
"title": "Order online",
"url": "https://example.com/order"
}
]
},
});
console.log(response);{
"accountId": "acc_YOUR_FACEBOOK_ACCOUNT_ID",
"iceBreakers": [
{
"question": "What are your opening hours?",
"payload": "HOURS"
},
{
"question": "Do you deliver?",
"payload": "DELIVERY"
}
],
"persistentMenu": [
{
"type": "web_url",
"title": "Order online",
"url": "https://example.com/order",
"payload": null
}
]
}Tapping an ice breaker or postback item sends message.postback with its payload. Other account types return 400 UNSUPPORTED_PLATFORM.
Webhooks
Instagram and Facebook inbox events keep your copy current without polling.
| Event | Fires when |
|---|---|
message.received | Someone messages the account |
message.sent | The account sends, here or on the platform |
message.delivered, message.read | Your messages arrive, or are read |
message.reaction | The person adds or removes a reaction |
message.postback | The person taps a button, menu item or ice breaker |
message.deleted | The person unsends a message, Instagram only |
Each event's conversationId works with the routes here. See Inbox Webhooks. Bluesky sends no events, so list its conversations instead.
Errors
| Code | HTTP | Cause | Fix |
|---|---|---|---|
UNSUPPORTED_PLATFORM | 400 | The platform has no inbox | See Support by Platform |
TEXT_REQUIRED, TEXT_TOO_LONG | 400 | text is empty or too long | Fit the message length |
INVALID_TEMPLATE, INVALID_ATTACHMENT | 400 | A bad button, a mixed attachment, or a non-HTTPS URL | Fix the fields in error |
CONVO_NOT_FOUND, RECIPIENT_NOT_FOUND | 404 | No such conversation or recipient | Check the id |
MESSAGE_WINDOW_EXPIRED | 409 | The 24-hour window closed | Wait for the person to write |
RECIPIENT_UNREACHABLE, CONVO_LOCKED | 409 | The person refuses messages, or the Bluesky chat is locked | Reach them another way |
TOKEN_EXPIRED, TOKEN_INVALID, CONNECTION_INACTIVE, CONNECTION_BROKEN | 409 | The account needs a reconnect | Reconnect it |
INBOX_PERMISSION | 422 | No messaging access | Reconnect and approve every permission |
INBOX_FAILED | 502 | The platform did not complete it | Retry. Reconnect if it keeps failing |
Other codes are in Error Codes.