Inbox
Read your conversations, reply, and triage — programmatically. Replies are asynchronous: you get a receipt, then poll (or let a webhook tell you) whether it landed.
Everything here needs the inbox plan feature (402 without it) and the
inbox:read / inbox:write scopes. Conversations are nested under a brand,
like the rest of the API.
List conversations
curl "https://vm.viraldashboard.io/api/public/v1/brands/412/conversations?status=open&type=comment" \
-H "Authorization: Bearer $VD_API_KEY"
{
"data": [
{
"id": 5521,
"platform": "facebook",
"type": "comment",
"status": "open",
"priority": "normal",
"contact": { "id": 88, "name": "Sam Rivera", "avatar": null },
"message_count": 3,
"last_message_at": "2026-07-13T12:41:09+00:00"
}
],
"meta": { "current_page": 1, "per_page": 30, "total": 1 }
}
Filter with status, type (dm · comment · mention), platform, and search. Fetch one with its full thread:
curl "https://vm.viraldashboard.io/api/public/v1/brands/412/conversations/5521" \
-H "Authorization: Bearer $VD_API_KEY"
Reply — asynchronous
A reply goes to a third-party platform, which can be slow or rate-limited, so the API does not hold
the request open. POST …/reply returns 202 with the message in
delivery_status: pending:
curl -X POST "https://vm.viraldashboard.io/api/public/v1/brands/412/conversations/5521/reply" \
-H "Authorization: Bearer $VD_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "body": "Thanks for reaching out! We ship worldwide." }'
HTTP/1.1 202 Accepted
{
"data": {
"id": 9012,
"direction": "outbound",
"body": "Thanks for reaching out! We ship worldwide.",
"delivery_status": "pending"
}
}
Then learn the outcome one of two ways.
Poll the message
Re-fetch the conversation and watch your outbound message's delivery_status go
pending → sent (or failed, with a delivery_error):
async function waitForReply(brand, conversationId, messageId) {
for (;;) {
const res = await fetch(
`${process.env.VD_API}/brands/${brand}/conversations/${conversationId}`,
{ headers: { Authorization: `Bearer ${process.env.VD_API_KEY}` } },
)
const { data } = await res.json()
const msg = data.messages.find(m => m.id === messageId)
if (msg.delivery_status === 'sent') return msg
if (msg.delivery_status === 'failed') throw new Error(msg.delivery_error)
await new Promise(r => setTimeout(r, 3000))
}
}
Or subscribe to the webhook (recommended)
Don't poll — subscribe to inbox.reply_failed and we'll call you only when a reply
doesn't make it. No news is good news (it sent):
{
"event": "inbox.reply_failed",
"data": {
"conversation_id": 5521,
"message_id": 9012,
"brand_id": 412,
"platform": "facebook",
"error": "The linked social account needs reconnecting."
}
}
And subscribe to inbox.message_received to be told about new inbound messages
as they're ingested — see Webhooks.
Instagram DMs can't be replied to over the API yet. A reply to an Instagram
conversation of type: dm returns 422 platform_capability_unavailable
— checked before we enqueue anything, so no send is attempted. This is a pending Meta
permission (instagram_manage_messages); Instagram comments and mentions
reply normally, as do all other platforms' DMs.
Triage
Move a conversation through your workflow — each returns the updated conversation:
| Call | Body |
|---|---|
PUT …/status | { "status": "resolved" } — open · assigned · snoozed · resolved · spam (snoozed needs snoozed_until) |
PUT …/assign | { "user_id": 7 } — or null to unassign |
PUT …/tags | { "tags": ["vip","urgent"] } — replaces the set |
PUT …/priority | { "priority": "urgent" } — urgent · high · normal · low |
Like every conversation route, these are tied to the brand in the URL: a conversation that
isn't in {brand} returns 403, never another brand's data. Writes
take an Idempotency-Key, and a vd_test_ key can read but not reply or
triage (403).