Inbox
List conversations, read their messages, reply with text, media, buttons and quick replies, start new DMs, and hand a thread to another app.
Direct messages live in conversations. Each conversation belongs to one connected account and holds the messages between that account and one person (or a group). This page covers reading and answering them. For the overall architecture, start with the Inbox overview.
#List conversations
curl "https://api.pozy.ai/v1/inbox/conversations?profileId=6650c1e2a3b4c5d6e7f80911&status=active&limit=50" \
-H "Authorization: Bearer $POZY_API_KEY"let cursor: string | undefined;
do {
const query = new URLSearchParams({ profileId: "6650c1e2a3b4c5d6e7f80911", status: "active", limit: "50" });
if (cursor) query.set("cursor", cursor);
const res = await fetch(`https://api.pozy.ai/v1/inbox/conversations?${query}`, {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const c of data.data) console.log(c.platform, c.participantName, c.lastMessage);
cursor = data.pagination.hasMore ? data.pagination.nextCursor ?? undefined : undefined;
} while (cursor);import os
import requests
params = {"profileId": "6650c1e2a3b4c5d6e7f80911", "status": "active", "limit": 50}
while True:
resp = requests.get(
"https://api.pozy.ai/v1/inbox/conversations",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
params=params,
timeout=30,
)
body = resp.json()
for c in body["data"]:
print(c["platform"], c["participantName"], c["lastMessage"])
if not body["pagination"]["hasMore"]:
break
params["cursor"] = body["pagination"]["nextCursor"]| Parameter | What it does |
|---|---|
profileId, accountId, platform |
Narrow to a customer, one account or one platform |
status |
active or archived |
folder |
inbox (default) or requests for message requests, where the platform separates them |
tags |
Conversations whose contact carries these tags |
sortOrder |
desc (default, newest activity first) or asc |
limit, cursor |
Up to 100 per page. Pass pagination.nextCursor back as cursor |
Each conversation carries the participant (participantName, participantPicture, participantVerifiedType), lastMessage, updatedTime, unreadCount and threadControl. Conversations that started from an ad carry adAttribution with the ad, ad set and campaign names. Instagram conversations can carry instagramProfile (isFollower, isFollowing, followerCount, isVerified); treat a missing value as unknown, never as "not a follower".
To find a conversation by what was said, use GET /v1/inbox/conversations/search with query (up to 200 characters) and an optional direction.
#Read messages
GET /v1/inbox/conversations/{conversationId}/messages needs the accountId and returns messages oldest first by default (sortOrder=desc flips it). Pages hold up to 100 messages.
{
"id": "6650c1e2a3b4c5d6e7f81201",
"conversationId": "6650c1e2a3b4c5d6e7f80b33",
"platform": "instagram",
"direction": "incoming",
"message": "Hi, is the blue one still in stock?",
"senderName": "Jane",
"createdAt": "2026-10-29T14:00:03.000Z",
"attachments": [],
"deliveryStatus": null,
"reactions": [],
"isEdited": false,
"isDeleted": false,
"sentVia": null
}directionisincomingoroutgoing.deliveryStatuson outgoing messages moves throughsent,delivered,read,played(voice notes) orfailed, where the platform reports it.deliveryErrorexplains a failure.attachments[]have atype(image,video,audio,file,sticker,share,template), aurland, for links that expire, arefreshUrl.GET .../attachments/{index}always resolves a fresh link (a redirect by default, or JSON withformat=json).- Edited and unsent messages keep their history in
editHistory,isDeletedanddeletedAt. sentVianames what sent an outgoing message:human,api,broadcast,sequence,workflow,comment_automationorbulk-api.nullmeans unknown, for example a message sent from the platform's own app.
#Reply
POST /v1/inbox/conversations/{conversationId}/messages sends a message into an existing conversation. accountId is required; everything else depends on what you send.
curl -X POST https://api.pozy.ai/v1/inbox/conversations/6650c1e2a3b4c5d6e7f80b33/messages \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9b6c0f4e-1f0b-4b38-9a1e-6f2e8d7f1c55" \
-d '{
"accountId": "6650c1e2a3b4c5d6e7f80a22",
"message": "Yes, the blue one is in stock. Want me to hold it?",
"quickReplies": [
{ "title": "Hold it", "payload": "HOLD_BLUE" },
{ "title": "No thanks", "payload": "NO_THANKS" }
]
}'import { randomUUID } from "node:crypto";
const res = await fetch("https://api.pozy.ai/v1/inbox/conversations/6650c1e2a3b4c5d6e7f80b33/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "6650c1e2a3b4c5d6e7f80a22",
message: "Yes, the blue one is in stock. Want me to hold it?",
quickReplies: [
{ title: "Hold it", payload: "HOLD_BLUE" },
{ title: "No thanks", payload: "NO_THANKS" },
],
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data);import uuid
resp = requests.post(
"https://api.pozy.ai/v1/inbox/conversations/6650c1e2a3b4c5d6e7f80b33/messages",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"accountId": "6650c1e2a3b4c5d6e7f80a22",
"message": "Yes, the blue one is in stock. Want me to hold it?",
"quickReplies": [{"title": "Hold it", "payload": "HOLD_BLUE"}, {"title": "No thanks", "payload": "NO_THANKS"}],
},
timeout=30,
)
print(resp.json())A 200 means Pozy accepted the message, not that it was delivered. Delivery arrives later as message.sent, then message.delivered and message.read (or message.failed) on platforms that report them.
#What you can send
| Field | What it sends | Where |
|---|---|---|
message |
Text | Everywhere |
attachmentUrl + attachmentType (image, video, audio, file) |
One attachment from a public HTTPS URL. attachmentName sets the file name |
Most DM platforms, not Bluesky or Reddit |
quickReplies |
Up to 13 tappable chips (title up to 20 characters) | Instagram, Facebook |
buttons |
1 to 3 url, postback or phone buttons under the text. Text is then capped at 640 characters. phone is Facebook only |
Instagram, Facebook |
template |
A generic card carousel, up to 10 cards with title, subtitle, image and buttons |
Instagram, Facebook |
interactive |
Lists, call-to-action URLs, flows, product messages, location and contact requests | |
replyTo |
The platform message id to quote. On Slack it threads the reply instead | WhatsApp, Telegram, Slack |
replyMarkup |
Inline or reply keyboards | Telegram |
voiceNote |
Send an OGG/Opus audio attachment as a voice note | |
location, contacts |
A location pin, or contact cards | WhatsApp (location also on iMessage) |
sharedMediaId, sticker |
Share one of the account's own posts, or send the heart sticker, on their own | |
subject, effect, contactCard |
A bold title line, a screen effect, or your contact card | iMessage |
channel |
Force one send over sms, rcs or whatsapp instead of letting the number pick |
iMessage numbers with those add-ons |
cc |
Extra recipients on the reply | Email threads from connected Google or Microsoft mailboxes |
A postback button with a payload of pozy:workflow:<workflowId> starts that workflow when tapped. See Workflows.
Upload a local file first with POST /v1/media/upload-direct (multipart) and send the returned url as attachmentUrl.
#Messaging windows
Platforms limit when a business can write to a person:
| Platform | Rule |
|---|---|
| Instagram, Facebook | Free-form replies within 24 hours of the person's last message. Up to 7 days with messagingType: "MESSAGE_TAG" and messageTag: "HUMAN_AGENT", for replies typed by a person only |
| Free-form messages within the 24-hour customer service window. Outside it, send an approved template. See WhatsApp | |
| TikTok | Reply only. Up to 10 messages within 48 hours of the person's last message |
| X, Bluesky, Telegram | No window, but the person must accept DMs from you |
HUMAN_AGENT is refused on automated sends (workflows, sequences, broadcasts). The other message tags were retired by Meta and fail.
#Start a conversation
POST /v1/inbox/conversations opens a new conversation, or appends to the existing one with that person. Name the recipient with participantId or participantUsername.
- X: Pozy checks that the person accepts DMs from your account first and answers
422with codeDM_NOT_ALLOWEDwhen they do not. PassskipDmCheck: trueif you already verified it. X DMs are billed per call (pricing). - WhatsApp: a first message must be a template: pass
templateName,templateLanguage,templateParamsand, where the template has them,headerMediaandtemplateButtonParams. - TikTok: businesses cannot start conversations.
#Manage conversations
| Task | Call |
|---|---|
| Archive or unarchive | PUT /v1/inbox/conversations/{id} with status: "archived" or "active" |
| Mark as read | POST /v1/inbox/conversations/{id}/read |
| Show "typing..." | POST /v1/inbox/conversations/{id}/typing |
| React to a message | POST /v1/inbox/conversations/{id}/messages/{messageId}/reactions with an emoji; DELETE to remove |
| Edit a sent message | PATCH /v1/inbox/conversations/{id}/messages/{messageId} (Telegram text and keyboards) |
| Delete a message | DELETE /v1/inbox/conversations/{id}/messages/{messageId} where the platform allows unsending |
| Label a conversation | Create Page labels with POST /v1/inbox/labels, then attach them with POST .../labels (Facebook) |
| Delete a conversation | DELETE /v1/inbox/conversations/{id} |
#Message requests
On Facebook, DMs from people the Page has not accepted wait in a requests folder. List them with folder=requests and accept one with POST .../accept, which sends your first message and moves the thread into the inbox. Instagram does not expose the folder (the list answers 400 with PLATFORM_LIMITATION), but accepting still works, and requests arrive as message.received once the person writes.
#Hand a conversation to another app
On WhatsApp, Facebook and Instagram, another app can own a thread, for example Meta's own inbox app or a business agent. While it does:
- Incoming messages still arrive as
message.received, flaggedmetadata.standby: true. Pozy runs no automation on them. - Your sends answer
409with codenot_thread_owner.
Move the thread with POST /v1/inbox/conversations/{id}/thread-control:
action |
Effect |
|---|---|
pass |
Give the thread to targetAppId (or target: "ai_agent"), with an optional metadata note |
take |
Take it back. Meta allows this only to the Page's primary receiver |
request |
Ask the current owner to pass it. Nothing changes until they do |
release |
Return it to the primary receiver |
conversation.control_changed fires whenever the owner changes. On WhatsApp, sending any message takes control back, so a bot that answers every message.received must skip standby messages.
#When a send fails
| Response | Meaning | What to do |
|---|---|---|
400 PLATFORM_LIMITATION |
The platform cannot do what you asked, for example an attachment on Bluesky | Send without it |
409 not_thread_owner |
Another app owns the thread | Take the thread back first |
422 DM_NOT_ALLOWED |
X recipient does not accept your DMs | Do not retry |
500 |
The platform failed the send. It may or may not have gone out | List the conversation's messages before retrying |
Send an Idempotency-Key with every reply. If the response is lost after a successful send, a retry with the same key returns the original response instead of a second message. A failed (non-2xx) request releases the key, so after an ambiguous 500, check the messages first; an empty result is inconclusive, not proof it was not sent. See Idempotency.
#Endpoints
#Webhook events
| Event | When it fires |
|---|---|
message.received |
Message received. |
conversation.control_changed |
Conversation control changed. |
conversation.started |
Conversation started. |
message.sent |
Message sent. |
message.edited |
Message edited. |
message.deleted |
Message deleted. |
message.delivered |
Message delivered. |
message.read |
Message read. |
message.played |
Message played. |
message.failed |
Message delivery failed. |
reaction.received |
Reaction received. |
referral.received |
Referral received. |