chat_id and an ordered parts[] array. Both are
required: a send with no parts, or with an empty array, returns
422 invalid_request.
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:
Part types
A message carries 1 to 32 parts.
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_typewhen the path has no useful file extension.
Mentions
Atext 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: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:
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-mintedmessage_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
- Message model for part ids and reply targets
- Attachments
- Voice memos
- Reactions
- Delivery model

