---
title: "Send Message"
method: POST
path: "/conversations/{conversation_id}/messages"
tags: ["Conversations"]
---

# Send Message

`POST /conversations/{conversation_id}/messages`

## 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}/messages`
- Auth: HTTP bearer

Reply in a thread.

Runs the SAME three consent gates as notify-owner, with the same arguments.
A thread with no resolved contact (a parent-level thread) answers 409:
consent is per (number, contact) and cannot be checked without one.

`attach_snapshot` sends the event's alert-frame JPEG as MMS. It requires
`event_id` (422 at the body level without one) and answers 409
SNAPSHOT_UNAVAILABLE when that event has no snapshot — nothing is written
and nothing is sent in that case.

## Path parameters

- `conversation_id` string, required

## Request body

- SendMessageRequest
  - `body` string, required: Message body. Links are permitted — the 10DLC campaign declares them and link shortening is disabled per message, so a URL goes out exactly as typed (see NotifyOwnerRequest.message).
  - `event_id` string, nullable: Optional incident this message is about. When given, the message ALSO appears on that event's communications timeline; the event must be in your scope AND belong to this thread's site.
  - `attach_snapshot` boolean: Attach the event's alert-frame JPEG as MMS. REQUIRES `event_id` — there is no snapshot without an incident to take it from, and `attach_snapshot` without one is a 422 at the body level. 409 SNAPSHOT_UNAVAILABLE when the event has no snapshot (no clip key, a clip key that derives none, or the object is not readable in S3); nothing is written and nothing is sent in that case. Absent or false behaves exactly as before.
  - `snapshot_offset_ms` integer, nullable: WHICH moment of the clip to attach, in milliseconds from the start of the event's clip, as picked by the operator and cut earlier by `POST /events/{id}/snapshot-frame`. Rounded to a tenth of a second. Only meaningful with `attach_snapshot` (422 at the body level otherwise). Absent, `attach_snapshot` sends the ingestor's alert-frame poster as before. 409 SNAPSHOT_UNAVAILABLE when no frame exists at that offset.
  - `include_clip_link` boolean: Append a login-free, expiring link to this event's clip. The URL is added by the SERVER to the end of the body; reserve share_link_reserved_chars (GET /telephony/config) characters for it. REQUIRES event_id — a clip link with no incident behind it is a 422.

## Response `200`

Successful Response

- ConversationMessage: One message in a thread. `provider_sid` IS DELIBERATELY NOT EXPOSED: it is provider-internal, the UI has no use for it, and it is the correlation key for our own webhook handling.
  - `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.

## 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)
