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 pendingsent (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:

CallBody
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).