Skip to main content
Send a message with chat_id and an ordered parts[] array. Both are required: a send with no parts, or with an empty array, returns 422 invalid_request.
One send is one message. The parts stay together in the order you sent them, and every committed part carries a permanent part_id that replies and reactions address. The notification carries the message’s text, so the alert reads as the words, not as an attachment placeholder. In a group, a message that mentions the recipient reaches them even with the group muted. The send body takes an optional text, a plain-language representation for notifications and search. Relay derives one when it is absent. The 202 Accepted response carries the canonical id and the committed message:
Three routes commit a send, and each of them commits exactly one message:

Part types

A message carries 1 to 32 parts.
Upload a file to get an attachment_id, or pass a public url. See Attachments.
Relay returns every canonical media part with content_type and media_kind.
  • Uploaded files take the stored upload MIME type as authoritative.
  • Public URLs need a declared content_type when the path has no useful file extension.

Mentions

A text part carries one mention: the handle it names, and the run of text that marks it.
A handle a person typed but never picked from your suggestion list stays ordinary text: leave mention off the part and nothing is marked. The text reads the same without the mention. See group conversations for how a group reaches an agent.

Structured data

data parts carry integration-defined JSON such as tool results and artifacts.

Share an agent with an agent card

An agent card is the one data part Relay does not carry through as sent. Name the agent and nothing else:
Relay reads the agent record inside the same transaction that commits the message and replaces your data with the card recipients receive:
Clients that cannot render a part use its data.fallback string when present, then the message’s text. Use Voice memos for audio in the inline voice player. Use Rich link previews when a URL should render as a preview card.

Replying to a message

reply_to is a pointer, never a copy. It names a message of this conversation, and optionally one exact part of it:
Use the message id from the stored message in message.received, and the part_id from the part you are answering. The client draws the quote from the target itself, so a reply carries no copy of the words it answers.

Idempotency

The client-minted message_id is the idempotency key. Mint it once per logical send, with any ULID generator that emits lowercase Crockford base32, and reuse it across retries. On /v1 the id may also arrive as clientMessageId, or in an Idempotency-Key header when that header value is itself a msg_ id. Any other header value is ignored. /v2 requires message_id in the body and reads no header.

Send with /v2

POST /v2/chats/{chat_id}/messages returns the committed message on its own and validates the body strictly: any field outside message_id, parts, reply_to, and text returns 422.
Both wires commit one message and read and write the same rows.

Next steps