Skip to content

Message Routing API

Message routing is the core function of Cryptid servers. All messages are encrypted with MLS before reaching the server, making the server a “dumb pipe” that routes opaque ciphertext without understanding content, relationships, or cryptographic meaning.

The server’s only responsibilities are:

  • Basic format validation (message IDs, addresses)

  • Temporary queuing for offline recipients

  • Federation routing to remote servers

  • Rate limiting to prevent abuse

Routes a single encrypted MLS message to a recipient. If the recipient is on the same server, the message is queued locally. If the recipient is on a different server (federated), the message is forwarded via the federation protocol.

POST /v1/messages
POST /v1/messages HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"message_id": "a1b2c3d4e5f61728394a5b6c7d8e9f10123456789abcdef",
"recipient_address": "bob@server2.example.com",
"group_id": "f1e2d3c4b5a6978869504132e1f2d3c4b5a6978869504132",
"mls_ciphertext": "base64_mls_encrypted_payload_here",
"sender_signature": "ed25519_signature_over_ciphertext",
"timestamp": 1759697179,
"message_type": "Text"
}

Parameters:

  • message_id: 32-byte hex message identifier
  • recipient_address: Target device address
  • group_id: 32-byte hex group identifier
  • mls_ciphertext: Base64 encoded MLS encrypted payload (server cannot decrypt)
  • sender_signature: Ed25519 signature over ciphertext
  • message_type: Text, Media, or SystemOperation
{
"message_id": "a1b2c3d4e5f61728394a5b6c7d8e9f10123456789abcdef",
"status": "queued",
"delivery_estimate": "immediate"
}

Response Fields:

  • status:
    • queued: Message stored locally for retrieval by recipient
    • federated: Message forwarded to recipient’s home server
  • delivery_estimate:
    • immediate: Recipient is online or will retrieve the message soon
    • delayed: Recipient may be offline (message expires per retention policy)

Sends multiple messages in a single request for efficiency.

This is particularly useful for:

  • MLS group operations (commit + welcome messages)

  • Multi-device message fanout (same content to multiple recipients)

  • Delivery receipt batching

Batch requests are atomic at the transport level but individual messages may have different statuses (some queued locally, others federated).

MLS group operations often require sending multiple messages simultaneously (e.g., adding a member requires sending a commit to existing members and a welcome to the new number). Batching ensures these related messages are sent together and reduces HTTP overhead.

Max Batch Size: 100 messages per request (configurable by server)

POST /v1/messages/batch
POST /v1/messages/batch HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"messages": [
{
"message_id": "message_1_id_hex",
"recipient_address": "alice@server1.com",
"group_id": "group_id_hex",
"mls_ciphertext": "base64_ciphertext_1",
"sender_signature": "signature_1",
"timestamp": 1759697179,
"message_type": "SystemOperation"
},
{
"message_id": "message_2_id_hex",
"recipient_address": "bob@server2.com",
"group_id": "group_id_hex",
"mls_ciphertext": "base64_ciphertext_2",
"sender_signature": "signature_2",
"timestamp": 1759697179,
"message_type": "SystemOperation"
}
]
}
{
"accepted_count": 2,
"rejected_count": 0,
"message_statuses": [
{
"message_id": "message_1_id_hex",
"status": "queued"
},
{
"message_id": "message_2_id_hex",
"status": "federated"
}
]
}

Retrieves encrypted messages queued for the authenticated device. This endpoint is used for polling-based message delivery. For real-time delivery, use the WebSocket stream endpoint instead.

Messages remain in the queue until explicitly acknowledged. Unacknowledged messages are redelivered on subsequent polls until they expire (default: 30 days).

GET /v1/messages

Query Parameters:

  • since (optional): Unix timestamp for incremental sync
  • limit (optional): Max messages to return (default: 100, max: 1000)
GET /v1/messages?since=1759697179&limit=50 HTTP/1.1
Authorization: Bearer {access_token}
{
"messages": [
{
"message_id": "32_byte_message_id_hex",
"group_id": "32_byte_group_id_hex",
"mls_ciphertext": "base64_encrypted_content",
"sender_signature": "ed25519_signature",
"timestamp": 1758999498,
"message_type": "Text",
"received_at": 1758999500
}
],
"has_more": false,
"next_poll_interval": 30,
"server_timestamp": 1758999520
}

Response Fields:

  • messages[]: Array of queued messages (oldest first)

  • has_more: Boolean indicating if more messages exceed the limit

  • next_poll_interval: Server’s recommended polling interval in seconds (typically 30-60s)

  • server_timestamp: Current server time for client synchronization

Polling Best Practices:

  1. Use the since parameter for incremental sync (reduces bandwidth)

  2. Respect next_poll_interval to avoid rate limiting

  3. Consider WebSocket streaming for real-time applications

  4. Acknowledge messages promptly to free server storage

Permanently removes a message from the server queue after successful client-side decryption and storage. Once acknowledged, the message is deleted from the server and cannot be retrieved again.

When to Acknowledge:

  • After successful MLS decryption

  • After storing message in local database

  • Before displaying to user

When NOT to Acknowledge:

  • MLS decryption failed (may be retried later)

  • Message is invalid or corrupted

  • Local storage failed

DELETE /v1/messages/{message_id}
DELETE /v1/messages/a1b2c3d4e5f61728394a5b6c7d8e9f10123456789abcdef HTTP/1.1
Authorization: Bearer {access_token}
{
"acknowledged": true
}
Server receives a message ───────► Message is Queued ───────► Client retrieves the message
│ │
│ │
│ │
▼ ▼
Expires after max_retention_days Client processes the message
Client acknowledges the message
Message is deleted from the server

Acknowledge multiple messages in a single request.

POST /v1/messages/ack
POST /v1/messages/ack HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"message_ids": [
"message_1_id_hex",
"message_2_id_hex",
"message_3_id_hex"
]
}
{
"acknowledged_count": 3,
"failed_count": 0
}
  • After successfully processing multiple retrieved messages

  • When syncing a device after extended offline period

  • For efficiency when processing message batches

  • Text: Regular chat messages

  • Media: Messages with file attachments (files are referenced, not included in the MLS ciphertext)

  • SystemOperation: Internal protocol messages (MLS commits, welcomes, delivery receipts, typing indicators, etc)

Message IDs must be:

  • Unique per sender (prevent duplicate delivery)

  • 32 bytes (64 hex characters)

  • Generated using cryptographically secure random (UUIDv4 or similar)

The server uses message IDs only for deduplication and tracking within queues. It makes ne assumptions about their format beyond length.

  1. POST /v1/messages -> Server queues the message

  2. GET /v1/messages -> Client retrieves the message

  3. Message stays in queue until acknowledged

  4. DELETE /v1/messages/{id} -> Server deletes message

Messages remain retrievable multiple times until acknowledged, allowing clients to retry decryption or handle transient errors.

When sending to a federated recipient (bob@other-server.com), the server:

  1. Discovers the recipient’s home server

  2. Authenticates via the federation protocol

  3. Forwards the encrypted message

  4. Returns "status": "federated"

The sending client experiences no difference between local and federated delivery. The complexity is hidden by the server.