Skip to main content
GET
/
api
/
conversations
/
{id}
Get a conversation
curl --request GET \
  --url https://app.puffle.ai/api/conversations/{id} \
  --header 'Authorization: Bearer <token>'
{ "conversation": { "id": "f11e8a2c-1234-4567-89ab-cdef01234567", "user_id": "11111111-1111-1111-1111-111111111111", "account_id": "aaaa1111-1111-1111-1111-111111111111", "channel": "linkedin", "status": "active", "external_id": "unipile_chat_abc123", "participant_name": "Jane Doe", "participant_email": null, "participant_linkedin_url": "https://linkedin.com/in/jane", "last_message_at": "2026-04-22T09:12:00Z", "unread_count": 0, "created_at": "2026-04-20T15:00:00Z", "updated_at": "2026-04-22T09:12:00Z" }, "messages": [ { "id": "11111111-2222-3333-4444-555555555555", "user_id": "11111111-1111-1111-1111-111111111111", "conversation_id": "f11e8a2c-1234-4567-89ab-cdef01234567", "campaign_id": "8c2b2d4e-e29b-41d4-a716-446655440000", "direction": "outbound", "message_type": "message", "subject": null, "body_text": "Hi Jane — quick question about Acme's ops stack.", "body_html": null, "sender_name": "Sid", "sender_identifier": "unipile_user_sid", "external_id": "unipile_msg_1", "sent_at": "2026-04-20T15:02:00Z", "created_at": "2026-04-20T15:02:00Z" }, { "id": "22222222-3333-4444-5555-666666666666", "user_id": "11111111-1111-1111-1111-111111111111", "conversation_id": "f11e8a2c-1234-4567-89ab-cdef01234567", "campaign_id": null, "direction": "inbound", "message_type": "message", "subject": null, "body_text": "Thanks for reaching out — happy to chat next week.", "body_html": null, "sender_name": "Jane Doe", "sender_identifier": "unipile_user_jane", "external_id": "unipile_msg_2", "sent_at": "2026-04-22T09:12:00Z", "created_at": "2026-04-22T09:12:05Z" } ], "participantDetails": { "company": "Acme Inc", "position": "VP Operations", "headline": "VP Ops @ Acme", "profile_picture": "https://media.licdn.com/.../jane.jpg" }, "senderDisconnected": false, "senderAccount": { "display_name": "Sid's LinkedIn", "status": "active" } }

Overview

Two operations live on this path:
  • GET /api/conversations/{id} — returns the conversation, every message (ordered sent_at ASC), enriched participant details (company, position, headline, profile picture), and sender-account health. Side effect: resets unread_count to 0 before returning.
  • PATCH /api/conversations/{id} — patches status (archive/un-archive) and/or unread_count.
A conversation is a single inbox thread — one sender account × one prospect × one channel. Threads are created when a prospect replies to a campaign (the Unipile or AgentMail webhook inserts the conversations row) or when the agent sends the first outbound reply via sendConversationMessage.

Sender-disconnected signal

GET returns senderDisconnected: true when the sender account backing this thread is missing or its status isn’t one of connected, active, or warming. The inbox UI renders an amber banner and disables the reply composer in that state — agents should refuse to call sendConversationMessage and prompt the user to reconnect first.

AI agent notes

Opening = marking read. Every successful GET resets unread_count to 0. Don’t use this endpoint just to peek at a thread from a polling loop — use listConversationMessages instead, which does not mutate state.Ownership errors mask existence. 404 is returned both when the id is unknown and when it’s owned by another user — the API never reveals whether a foreign conversation exists.PATCH accepts two fields.
  • status: "active" | "archived" — archive/un-archive the thread. Other string values are silently dropped.
  • unread_count: number — raw counter override. Most agents can ignore this; the server already resets to 0 on GET.
A PATCH with no recognized keys returns 400 "No valid fields to update".Preconditions before replying.
  1. GET /api/conversations/{id} — confirm senderDisconnected: false.
  2. If the sender is disconnected, stop and tell the user to reconnect the underlying account (email: reconnect inbox; LinkedIn: OAuth via Unipile).
  3. Otherwise chain into sendConversationMessage.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path Parameters

id
string<uuid>
required

Conversation UUID. Must be owned by the authenticated user — other callers get 404.

Pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$

Response

Conversation, all messages, participant metadata, and sender health.

conversation
object
required

Canonical conversation row. unread_count is always 0 here — the server resets it as part of this call.

messages
object[]
required

Every message in the thread, ordered sent_at ASC (oldest first). Rendered top-to-bottom in the UI.

participantDetails
object
required

Best-effort metadata about the prospect on the other end of the thread. Populated by joining the conversation's participant identifier against campaign_prospects and (for LinkedIn) linkedin_conversations. Any field can be missing.

senderDisconnected
boolean
required

true when the sender account is missing or its status isn't connected/active/warming. The UI renders an amber warning banner and the reply composer should refuse to send until the user reconnects.

senderAccount
object
required

Minimal snapshot of the sender account tied to this conversation. null when the account no longer exists.