A user is a person using Relay. An agent appears to that person as a contact. The Agent Token authenticates external code as that contact; it does not create a user session or run the agent inside Relay.
Conversations and participants
A conversation has a stable conversation_id and participants. A direct conversation normally contains one user and one contact. An agent can send only to a conversation in which its contact is an active participant and installed for the user.
Use the conversation_id supplied in message.received. Agents can read a conversation’s history with GET /v1/conversations/{id}/messages.
Messages and ordered parts
A message carries an ordered parts[] array. Relay assigns each stored part a zero-based part_index. Keep the array order: it is presentation order and provides a stable target for replies and future interactions.
| Type | v0 shape | Meaning |
|---|
text | { "type": "text", "text": "Hello" } | Text content. |
link | { "type": "link", "text": "https://example.com" } | An HTTP or HTTPS URL. |
data | { "type": "data", "payload": { ... } } | Structured JSON content. |
media | { "type": "media", "attachment_id": "att_..." } | An existing media attachment reference. |
voice | { "type": "voice", "attachment_id": "att_..." } | An existing voice-note attachment reference. |
The v0 send route accepts 1–32 parts. Text is limited to 8 KB per part and data payloads to 16 KB. Public attachment upload is coming soon, so a new developer integration can send text, links, and data today but cannot create a new attachment_id through the developer API yet.
Stored messages include:
{
"id": "msg_01JZM3T8AH",
"conversation_id": "cnv_01JZC7K4RQ",
"sequence": 7,
"sender": { "kind": "contact", "id": "agt_01JZRELAY" },
"parts": [
{ "part_index": 0, "type": "text", "text": "Here is the link:" },
{ "part_index": 1, "type": "link", "text": "https://example.com" }
],
"reply_to": null,
"fallback_text": "Here is the link:",
"status": "sent",
"created_at": "2026-07-12T01:21:08.000Z"
}
fallback_text is the plain-language representation used by notifications, search, and clients that cannot render a part. In v0 Relay derives it from the first text part, then the first link, or finally [attachment]. The developer send route does not accept a fallback_text override.
Sequence and cursor
message.sequence is the message’s order inside one conversation. It is not the polling cursor.
next_cursor is an opaque position in an agent’s event log. Persist it exactly and return it as cursor; do not decode it or calculate a replacement. Passing it acknowledges the earlier events.
At-least-once delivery
Polling and webhooks are at-least-once transports. The same envelope may arrive again, and webhook attempts may arrive out of order. Process each event_id once:
- Verify the webhook signature when applicable.
- Atomically record
event_id if it is new.
- Return success or advance the polling cursor promptly.
- Process the event asynchronously.
Use the incoming event_id to derive the reply’s Idempotency-Key. Reusing the same key with the same request returns the original message. Reusing it with a different request returns idempotency_conflict.
Attachments and uploads, typing and read state, edits, unsends, socket mode, store distribution, and calls are specified for Relay but are not implemented in the v0 developer API.