Conversations

A conversation is a single thread with one contact on one channel (WhatsApp, SMS, or email). Outbound sends and inbound replies are grouped into the same conversation, so you can read the full back and forth in order.

List conversations

Cursor-paginated, newest activity first. Each summary carries the channel, status, and when the last message arrived:

list.ts
const page = await bt.conversations.list({ limit: 50 });

for (const conv of page.data) {
  console.log(conv.id, conv.channel, conv.status, conv.last_message_at);
}
// page.next_cursor / page.has_more drive pagination

Get one conversation

Fetch a single conversation to see the contact it belongs to and its assignment state:

get.ts
const conv = await bt.conversations.get("conv_123");
console.log(conv.id, conv.status); // "open" | "pending" | "resolved"

Read the messages

The conversation summary does not include message bodies — read those from the messages endpoint. Messages come back oldest-first and are cursor-paginated:

messages.ts
const messages = await bt.conversations.messages("conv_123", { limit: 100 });

for (const m of messages.data) {
  // m.direction: "inbound" | "outbound"
  // m.body, m.status, m.created_at
  console.log(m.direction, m.body);
}

React to inbound replies

Do not poll for new replies. When a customer messages you, Blueticked fires the message.received webhook, and the event already carries the reply text. Read it from event.data.body — no follow-up call:

app/api/blueticked/route.ts (Next.js)
interface InboundMessage {
  message_id: string;
  conversation_id: string;
  channel: "whatsapp" | "sms" | "email";
  from: string;
  body: string;
  media: { type: string; filename: string | null } | null;
  occurred_at: string;
}

const event = await bt.webhooks.constructEvent<InboundMessage>(
  rawBody,
  req.headers.get("x-blueticked-signature") ?? "",
  process.env.BLUETICKED_WEBHOOK_SECRET!,
);

if (event.event === "message.received") {
  const { conversation_id, from, body, media } = event.data;

  // A captionless photo arrives as body "" with media set. Branch on
  // media, never on an empty body, or you silently drop real content.
  if (media) {
    // media.type: image | video | audio | document | sticker.
    // The webhook carries no media URL — fetch it from the conversation
    // once the download finishes.
  }

  await routeToInbox({ conversationId: conversation_id, from, body });
}

Reach for bt.conversations.messages() only when you need the surrounding thread — prior turns for an AI reply, say, or the media record once its download completes. Note the ordering when you do: messages are oldest-first, so { limit: 1 } returns the first message ever sent on that thread, not the reply that just arrived. Page to the end with messagesAll(), or match a message's id against the event's message_id.

Replying and triage

Replies are sent with messages.send, not through the conversation object. You can assign, resolve, or reopen a conversation with bt.conversations.update(id, { status }) — useful when a human agent picks up a thread.