Messenger API

Endpoints for the end-to-end encrypted PQC messenger.

All messages are encrypted client-side using ML-KEM-768 key encapsulation and AES-GCM. The server only stores encrypted blobs — it cannot read message contents.

v2 API (March 2026): All write operations now use /api/v2/ endpoints that require ML-DSA-65 signed requests with timestamp validation and nonce-based anti-replay protection. Legacy unsigned endpoints return HTTP 410 (Gone) in production.

Signed Request Format

All v2 write endpoints accept a signed request body:

{
  "payload": {
    "action": "register_wallet",
    "from": "ml-dsa65-public-key-hex",
    "timestamp": 1710100000000,
    "nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "...": "operation-specific fields"
  },
  "signature": "ml-dsa65-signature-hex",
  "public_key": "ml-dsa65-public-key-hex"
}

The server verifies: (1) the ML-DSA-65 signature, (2) the timestamp is within a 5-minute window, (3) the from field matches the signing key, and (4) the nonce has not been used before.

Register Messenger Wallet

POST /api/v2/messenger/wallets/register
Content-Type: application/json

Register your wallet's encryption public key so others can send you encrypted messages and mail. This is required before receiving mail from other apps. Requires a signed request.

Request Body (signed)

{
  "payload": {
    "id": "wallet-uuid-or-public-key",
    "displayName": "Alice",
    "signingPublicKey": "ml-dsa65-signing-public-key-hex",
    "encryptionPublicKey": "ml-kem768-encryption-public-key-hex",
    "from": "ml-dsa65-signing-public-key-hex",
    "timestamp": 1710100000000,
    "nonce": "random-hex-nonce"
  },
  "signature": "ml-dsa65-signature-hex",
  "public_key": "ml-dsa65-signing-public-key-hex"
}
FieldTypeDescription
idstringUnique wallet identifier
displayNamestringHuman-readable name (unique, case-insensitive)
signingPublicKeystringML-DSA-65 public key (hex)
encryptionPublicKeystringML-KEM-768 public key (hex) — needed for E2E encryption

Response

{
  "success": true
}

List Messenger Wallets

GET /api/messenger/wallets

Returns all registered messenger wallets.

Response

{
  "success": true,
  "wallets": [
    {
      "id": "wallet-uuid",
      "display_name": "Alice",
      "signing_public_key": "abc123...",
      "encryption_public_key": "def456..."
    }
  ]
}

Send Message

POST /api/v2/messenger/messages
Content-Type: application/json

Requires a signed request. The sender is authenticated via the ML-DSA-65 signature.

Request Body (signed)

The payload includes the message fields plus from, timestamp, and nonce:

{
  "payload": {
    "conversationId": "conv-uuid",
    "senderPublicKey": "sender-pub-hex",
    "recipientPublicKey": "recipient-pub-hex",
    "senderEncrypted": "base64-encrypted-for-sender",
    "recipientEncrypted": "base64-encrypted-for-recipient",
    "senderDisplayName": "Alice",
    "recipientDisplayName": "Bob",
    "selfDestruct": false,
    "destructSeconds": 0,
    "from": "sender-pub-hex",
    "timestamp": 1710100000000,
    "nonce": "random-hex-nonce"
  },
  "signature": "ml-dsa65-signature-hex",
  "public_key": "sender-pub-hex"
}

Messages are encrypted twice — once for the sender (so they can read their sent messages) and once for the recipient. The server stores both ciphertexts.

Self-Destruct Messages

Set selfDestruct: true and destructSeconds to a value. After the recipient reads the message, it is marked as read and hidden from the UI.

Response

{
  "success": true,
  "messageId": "msg-uuid"
}

Get Messages

POST /api/v2/messenger/messages/list
Content-Type: application/json

Requires a signed request. The server verifies the caller is a participant in the conversation.

Request Body (signed)

{
  "payload": {
    "conversationId": "conv-uuid",
    "from": "your-pub-hex",
    "timestamp": 1710100000000,
    "nonce": "random-hex-nonce"
  },
  "signature": "ml-dsa65-signature-hex",
  "public_key": "your-pub-hex"
}

Response

{
  "messages": [
    {
      "id": "msg-uuid",
      "conversationId": "conv-uuid",
      "senderPublicKey": "abc...",
      "senderDisplayName": "Alice",
      "encrypted": "base64-ciphertext",
      "timestamp": 1706745600000,
      "selfDestruct": false,
      "mediaType": null
    }
  ]
}

Mark Message as Read

POST /api/v2/messenger/messages/read
Content-Type: application/json

Used for self-destruct messages. Requires a signed request with messageId and conversationId in the payload.

Delete Message

POST /api/v2/messenger/messages/delete
Content-Type: application/json

Requires a signed request with messageId and conversationId in the payload. The caller must be a conversation participant.

Delete Conversation

POST /api/v2/messenger/conversations/delete
Content-Type: application/json

Requires a signed request with conversationId in the payload. The caller must be a conversation participant.


Get Conversations

POST /api/v2/messenger/conversations/list
Content-Type: application/json

Returns all conversations for a wallet. Requires a signed request.

Response

{
  "conversations": [
    {
      "conversationId": "conv-uuid",
      "participants": [
        {
          "publicKey": "abc...",
          "displayName": "Alice"
        },
        {
          "publicKey": "def...",
          "displayName": "Bob"
        }
      ],
      "lastMessage": "2024-01-31T12:00:00Z",
      "unreadCount": 2
    }
  ]
}

Media Messages

The messenger supports image and video attachments. Media is encrypted and sent as base64 within the message payload.

LimitValue
Max upload size50 MB (before compression)
Compressed target~1.5 MB
Image formatConverted to WebP client-side
Video formatConverted to WebM (VP9) client-side

The client automatically compresses large media before encryption and sending.


Encryption Flow

Sender                                    Recipient
  │                                          │
  │ 1. Generate shared secret via ML-KEM-768 │
  │ 2. Derive AES-256 key via HKDF           │
  │ 3. Encrypt message with AES-GCM          │
  │ 4. Encrypt for self (sender copy)        │
  │ 5. Encrypt for recipient                 │
  │                                          │
  │── POST /messages (both ciphertexts) ────►│
  │                                          │
  │                    6. Decapsulate shared secret
  │                    7. Derive same AES key
  │                    8. Decrypt message

Wallet Blocking

Users can block wallets client-side. Blocked wallets are filtered from conversations and contacts. The block list is stored in browser localStorage under pqc_blocked_wallets.

This is a client-side feature — the server is not involved. Blocked users can still send messages, but they won't appear in the blocking user's UI.


SDK Usage

The @rougechain/sdk provides a high-level API for messenger operations. All write operations now require a wallet parameter for ML-DSA-65 request signing:

import { RougeChain, Wallet } from "@rougechain/sdk";

const rc = new RougeChain("https://testnet.rougechain.io/api");
const wallet = Wallet.generate();

// Register wallet (signed request, required for receiving messages and mail)
await rc.messenger.registerWallet(wallet, {
  id: wallet.publicKey,
  displayName: "Alice",
  signingPublicKey: wallet.publicKey,
  encryptionPublicKey: encPubKey,
});

// Conversations (signed requests)
const convos = await rc.messenger.getConversations(wallet);
await rc.messenger.createConversation(wallet, [wallet.publicKey, recipientPubKey]);

// Messages (signed requests)
const msgs = await rc.messenger.getMessages(wallet, conversationId);
await rc.messenger.sendMessage(wallet, conversationId, encryptedContent, {
  selfDestruct: true,
  destructAfterSeconds: 30,
});
await rc.messenger.markRead(wallet, messageId);
await rc.messenger.deleteMessage(wallet, messageId);

TypeScript types: MessengerWallet, MessengerConversation, MessengerMessage