Envelopes
The core path. A server accepts CryptidEnvelopes, queues them against the address they name, and hands them to whoever proves ownership of that mailbox. It never learns what any of them contain, who sent them, or which belong together.
struct CryptidEnvelope { // Routing string only, "prefix@server". The server needs nothing more. // Mailbox public key is verified once at registration, not per message. recipient_address: String,
// HPKE Base mode (ephemeral-static) to the recipient's mailbox public key, // with recipient_address bound as AAD so a captured envelope cannot be // re-injected at a different mailbox. No sender key is involved, so the // sender stays anonymous even if the mailbox key is compromised. encrypted_blob: Vec<u8>,}SubmitEnvelope
Section titled “SubmitEnvelope”Queues one envelope for delivery.
- Input:
CryptidEnvelope - Output:
SubmitReceipt { message_id, accepted_at } - Auth: PrivacyPass
- Idempotent: No
- Errors:
TOKEN_INVALID,TOKEN_REPLAYED,TOKEN_MISSING,MAILBOX_UNKNOWN,MAILBOX_OVERLOADED,PAYLOAD_TOO_LARGE
Requirements
Section titled “Requirements”- Submission MUST spend one token
- Servers MUST NOT inspect, parse, or validate
encrypted_blob - Servers MUST reject an envelope naming an unregistered mailbox
- Servers MUST assign
accepted_atthemselves and MUST NOT accept a sender-supplied timestamp - Servers MUST assign
message_idas a UUIDv7 - Servers MUST forward an envelope naming a remote address to that server
- Servers MUST NOT record anything about the submitter
One envelope per delivery attempt
Section titled “One envelope per delivery attempt”Fan-out happens on the client: MLS encrypts once for the group, then the outer layer is sealed once per recipient device. A sender submits one envelope per recipient, each sealed to that recipient’s mailbox key with its address as AAD.
Failover is not fan-out. A device holding several mailboxes for one group publishes them in preference order, and a sender attempts the primary first - but a secondary is a fallback, not an additional recipient.
- Senders MUST attempt the primary address first
- Senders SHOULD fail over on
MAILBOX_UNKNOWN, a connection failure, or a timeout - Senders MUST NOT fail over on
MAILBOX_OVERLOADEDorSERVICE_UNAVAILABLE, and SHOULD retry the primary - Each attempt requires re-sealing, since both the AAD and the mailbox key differ per address
Tokens are spent at redemption, so an attempt that never reached a server costs nothing. Consult token_consumed on the error rather than assuming.
HTTP Binding
Section titled “HTTP Binding”POST /api/v1/envelopesAuthorization: PrivateToken token="<base64url>"Content-Type: application/cryptid+tlsFetchEnvelopes
Section titled “FetchEnvelopes”Retrieves queued envelopes for one mailbox.
- Input:
FetchRequest { since, limit } - Output:
Vec<QueuedEnvelope> - Auth: Mailbox ownership session
- Idempotent: Yes
- Errors:
SESSION_INVALID,MAILBOX_UNKNOWN
Requirements
Section titled “Requirements”- The session MUST authorize exactly the mailbox being fetched
- Servers MUST return envelopes in queue order
- Servers MUST NOT delete on read; deletion is by acknowledgement or expiry
- Servers MUST NOT log the source address of a fetch
- Fetching spends no token
Each returned envelope carries its message_id, its encrypted_blob, and the server’s received_at. It does not carry recipient_address or expires_at - the session already fixes the mailbox, and retention is the server’s bookkeeping.
The Cursor
Section titled “The Cursor”since is the last message_id the client received. Because message_id is a UUIDv7, it sorts by creation time, so the cursor needs no separate sequence number and no server-side per-client state.
sinceMUST be amessage_idthe server previously issued for this mailbox- Omitting
sinceMUST return from the head of the queue - Servers MUST treat an unknown
sinceasMAILBOX_UNKNOWNrather than silently returning from the head, which would replay the whole queue - Servers MUST NOT store a per-client cursor; the client holds it
HTTP Binding
Section titled “HTTP Binding”GET /api/v1/envelopes?since={cursor}&limit={n}Authorization: MailboxSession <token>AcknowledgeEnvelopes
Section titled “AcknowledgeEnvelopes”Confirms envelopes have been durably received, so the server can discard them.
- Input:
AckRequest { message_ids } - Output:
AckReceipt { discarded } - Auth: Mailbox ownership session
- Idempotent: Yes
- Errors:
SESSION_INVALID
Requirements
Section titled “Requirements”- Servers MUST discard an acknowledged envelope
- Servers MUST treat an unknown or already-acknowledged
message_idas success - Servers MUST NOT require acknowledgement for correctness; unacknowledged envelopes expire at
MAX_MESSAGE_RETENTIONregardless - Clients SHOULD acknowledge only after the envelope is durably stored locally, not on receipt
HTTP Binding
Section titled “HTTP Binding”POST /api/v1/envelopes/ackAuthorization: MailboxSession <token>SubscribeEnvelopes
Section titled “SubscribeEnvelopes”Opens a live stream of envelopes for one mailbox.
- Input:
SubscribeRequest { }- the session names the mailbox - Output: A stream of
QueuedEnvelope - Auth: Mailbox ownership session
- Idempotent: N/A
- Errors:
SESSION_INVALID,MAILBOX_UNKNOWN
Requirements
Section titled “Requirements”- A subscription MUST cover exactly one mailbox
- Servers MUST NOT accept a subscription list, and MUST NOT accept one session as authorization for another mailbox
- Servers MUST deliver the backlog before live envelopes, in queue order
- Servers MUST accept acknowledgements over the same stream
- Subscribing spends no token
This operation is defined over a session, not a connection. It says nothing about sockets, streams, or framing - a binding decides how many connections carry how many subscriptions. See Transports.
Bindings
Section titled “Bindings”WebTransport is the primary binding, WebSocket the fallback where UDP is blocked, and FetchEnvelopes on a timer the last resort. All three carry the same operation.