Relay · Messages
Send a retry-safe message
Every logical send needs a client-generated nonce. Keep that nonce when retrying an uncertain request. Relay uses (channel_id, nonce) to return the first result instead of creating another message.
Before you send
You need:
- A valid host-signed bearer token
- A channel in the same tenant as the token
- A membership for the token’s
user_id - A new nonce for this logical message
In a browser or Node.js runtime, crypto.randomUUID() can generate the nonce.
First request
NONCE="client-generated-uuid"
curl -i \
-X POST "https://relay.example.com/api/channels/$CHANNEL_ID/messages" \
-H "Authorization: Bearer $RELAY_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"body\":\"hello\",\"nonce\":\"$NONCE\",\"parent_message_id\":null,\"mentions\":[],\"attachment_ids\":[]}" A new message returns 201 Created:
{
"data": {
"id": "message-uuid",
"channel_id": "channel-uuid",
"sender_id": "user-uuid",
"body": "hello",
"nonce": "client-generated-uuid",
"seq": 41,
"parent_message_id": null,
"mentions": [],
"attachments": []
}
} Relay assigns seq inside the database transaction. Use seq, not a client timestamp, to order messages in the channel.
Retry after an uncertain result
A timeout or disconnected client does not prove that the write failed. Retry the same logical message with the same channel and nonce:
curl -i \
-X POST "https://relay.example.com/api/channels/$CHANNEL_ID/messages" \
-H "Authorization: Bearer $RELAY_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"body\":\"hello\",\"nonce\":\"$NONCE\",\"parent_message_id\":null,\"mentions\":[],\"attachment_ids\":[]}" When the first request already created the message, the retry returns 200 OK with the original message. Its id and seq do not change. The duplicate attempt does not write a second message or a second post-commit handoff.
Generate a new nonce only when the user or agent intends to create a new logical message. A nonce is scoped to one channel; it is not a global message ID.
Failure behavior
| Condition | Result |
|---|---|
| Token is missing or invalid | 401 with invalid_or_missing_token |
| User is not a channel member | 403 with forbidden |
| Channel is closed | 422 validation response |
body or nonce is missing or invalid | 422 validation response |
| An attachment is missing, belongs to another sender, or was not uploaded | The entire message transaction fails |
The common response shape is { "data": ... } for success and { "errors": [...] } for failure.
What happens after commit
REST and WebSocket sends call the same Relay write action. Membership validation, nonce idempotency, sequence assignment, attachment binding, and message serialization therefore follow one contract.
Delivery work begins after the message transaction commits. A retry with the same nonce returns the stored message and does not create another delivery intent.
To catch up after a reconnect, request messages after the last sequence you stored:
GET /api/channels/{channel_id}/messages?after_seq=41&limit=100
Authorization: Bearer <jwt> Relay returns channel history in ascending seq order. The maximum limit for this endpoint is 100.