Search the complete documentation

Try idempotency, proposal lifecycle, authentication, or ordering.

Navigate Open
繁體中文
Browse documentation

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

ConditionResult
Token is missing or invalid401 with invalid_or_missing_token
User is not a channel member403 with forbidden
Channel is closed422 validation response
body or nonce is missing or invalid422 validation response
An attachment is missing, belongs to another sender, or was not uploadedThe 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.

Next: receive realtime events and recover after reconnect.