---
title: "Get Conversation"
method: GET
path: "/conversations/{conversation_id}"
tags: ["Conversations"]
---

# Get Conversation

`GET /conversations/{conversation_id}`

## Request

- Base URL: `https://api.verdun.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.verdun.ai/conversations/{conversation_id}`
- Auth: HTTP bearer

One thread and a keyset page of its messages, newest first.

## Path parameters

- `conversation_id` string, required

## Query parameters

- `before` string, nullable
- `limit` integer

## Response `200`

Successful Response

- ConversationThreadResponse
  - `conversation` ConversationSummary, required
    - `id` string, required: The thread id.
    - `verdun_number` string, required: Our line, E.164.
    - `customer_number` string, required: The customer's number, E.164.
    - `contact_name` string, nullable: Snapshot of the contact at resolution time. Null when the number could not be pinned to one person.
    - `site_contact_id` string, nullable: The resolved contact, if exactly one matched. Null makes the thread read-only (consent cannot be checked without it).
    - `parent_id` string, nullable: Client. Null only for a thread orphaned by a hard client delete.
    - `parent_name` string, nullable: Client name, for the list row.
    - `site_id` string, nullable: Site. **Null means a PARENT-LEVEL thread** — the number belongs to contacts at several sites of one client — not a missing field. It is visible only to a scope holding the whole client.
    - `site_name` string, nullable: Site name, for the list row.
    - `account_id` string, nullable: The SITE'S account, resolved at read time. Null for a client-wide thread and for a site directly under the client.
    - `account_name` string, nullable: Account name, for the list row.
    - `status` 'open' | 'archived', required: An inbound or an outbound reopens an archived thread.
    - `unread_count` integer, required: Conversation-level and SHARED across operators in v1, not per viewer.
    - `last_message_at` string, date-time, required: Last activity. The list's sort key, never null.
    - `last_inbound_at` string, date-time, nullable: Last message from the customer.
    - `last_outbound_at` string, date-time, nullable: Last message from us.
    - `last_message_preview` string, nullable: First 160 characters of the newest body. Staff-only; it never reaches an activity-log entry.
    - `last_message_status` string, nullable: Delivery state of the LAST message, provider vocabulary (queued / sent / delivered / failed / undelivered; `received` on an inbound). Null with no messages. For the list row's badge.
    - `last_message_operator_id` string, nullable: Who sent the last message. Null on an inbound.
    - `last_message_operator_name` string, nullable: Their name, snapshotted when the message was sent. Null on an inbound.
    - `last_message_direction` 'outbound' | 'inbound', nullable: Direction of the previewed message.
    - `claimed_by_operator_id` string, nullable: Who is answering this thread.
    - `claimed_by_name` string, nullable: Their name, for the list row.
    - `claimed_at` string, date-time, nullable: When they claimed it.
    - `last_event` LastEventSummary: The newest incident this thread carries a message about. NO `closed_at` — `events_metadata` HAS NO SUCH COLUMN, and the two things that could stand in for one mean different things: `operator_evaluated` is "a human triaged or dismissed it", `expired_at` is "it aged out unreviewed". Both are exposed and the UI derives "open / closed" rather than the API inventing a single field that erases the distinction.
      - `id` string, required: The event id.
      - `occurred_at` string, date-time, nullable: Ingestor-reported time of the motion. Display only.
      - `created_at` string, date-time, nullable: When we recorded the event — the single DB clock.
      - `operator_evaluated` boolean, required: True once a human triaged or dismissed it.
      - `expired_at` string, date-time, nullable: Set when it aged out unreviewed. NOT the same as a dismissal.
    - `created_at` string, date-time, nullable: When the thread was opened.
    - `updated_at` string, date-time, nullable: Last write to the thread row.
  - `messages` ConversationMessage[], required: One page of messages, newest first.
    - `id` string, required: The message id. Also the `cid` echoed back by the provider.
    - `conversation_id` string, required: The thread it belongs to.
    - `direction` 'outbound' | 'inbound', required: Who sent it.
    - `body` string, required: The message text.
    - `status` string, required: Same vocabulary and the same monotonic rank as the incident timeline's SMS rows.
    - `error_code` string, nullable: Provider failure reason, bounded at 255 characters.
    - `event_metadata_id` string, nullable: The incident this message was about, if any. Cleared rather than cascaded if that event is deleted.
    - `notification_log_id` string, nullable: The mirror row on that incident's timeline. Null for a thread-only message.
    - `operator_id` string, nullable: Who sent it. Null on inbound.
    - `operator_name` string, nullable: Snapshot of their name.
    - `occurred_at` string, date-time, required: Provider timestamp inbound, send time outbound. The thread's sort key.
    - `created_at` string, date-time, nullable: When we wrote the row.
    - `attachments` AttachmentOut[], nullable: Media attached to this message, presigned at read time. NULL — never [] — when the message carried none, so there is one state on the wire for 'no attachments'.
      - `kind` string, required: What the file is. 'snapshot' is the event's alert-frame JPEG. An OPEN string, NOT an enum — inbound customer media arrives in a later phase as a new value with no migration, so never switch on it exhaustively.
      - `url` string, nullable: Presigned GET URL, minted at read time and valid 3600s. Null when presigning failed; the record is still returned. Presigned BLIND — a non-null URL does NOT prove the object still exists, so probe it.
      - `content_type` string, required: MIME type as sent. 'application/octet-stream' when unrecorded.
      - `offset_ms` integer, nullable: For an operator-picked snapshot: milliseconds into the event's clip the frame was cut from. Absent for the ingestor's alert-frame poster and for every other kind.
    - `share_link` ShareLinkBrief: The share link minted with ONE send, as it rides that send's timeline row. A deliberate SUBSET of `ShareLinkRecord`: enough to render "viewed" / "expires in 2 days" / "revoked" and offer a Revoke button, and nothing more. Absent (or null) on every row that carries no link -- voice, e-mail, manually-logged and inbound rows always do. NO TOKEN AND NO HASH, here or anywhere else on an operator surface. The plaintext URL is already in the message body this row carries; a second copy in a structured field would invite a client to render it as a clickable control, which is not what an operator's timeline is for.
      - `id` string, required: The share link id. Pass it to the revoke route.
      - `expires_at` string, date-time, nullable: When the link stops working, whatever else happens.
      - `revoked_at` string, date-time, nullable: Set once an operator killed it. Null means live (if unexpired).
      - `first_viewed_at` string, date-time, nullable: When the recipient first opened it. Null means nobody has yet -- which is the single most useful fact on the row.
      - `view_count` integer, required: How many times the page was served. No IP and no device is recorded.
  - `next_cursor` string, nullable: Keyset cursor for older messages.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/verdun/apis/verdun-api.md) - [All operations](https://skmtc.dev/verdun/apis/verdun-api/llms.txt) - [OpenAPI document](https://skmtc.dev/verdun/apis/verdun-api/revisions/35a2a6ebedda?raw)
