Skip to content

Transports

Operations in this specification are defined without a reference to a transport. This page says how they travel.

Two concerns live here:

What it is Server state
Delivery transport How bytes reach a client that is connected right now none - “a connection exists” is not a record
Push target How a sleeping client is woken durable, registered against one mailbox

Conflating them is a mistake that should be avoided. A delivery transport leaves no residue, while a push target is the longest-lived state in the design and the only kind that involves a third party.

Every binding carries the same canonical serialization: TLS presentation language as implemented by tls_codec, the same codec MLS uses.

  • Protocol types MUST use application/cryptid+tls
  • Operational metadata MAY use application/json
  • No binding may re-encode an envelope; encryption_blob passes through unmodified

The baseline. Every operation has an HTTP binding, and a server MUST implement it.

  • Requests carry PrivacyPass tokens as Authorization: PrivateToken token="<base64url>" per RFC 9577
  • Requests carry retrieval sessions as Authorization: MailboxSession <token>
  • Errors arrive as an ErrorEnvelope in the response body under the mapped status code

The streaming bindings share one frame format: a one-byte type tag followed by a tls_codec payload. Envelope bytes pass through from the queue without re-encoding.

Tag Frame Direction Payload
0x01 Subscribe client -> server none; the session names the mailbox
0x02 Unsubscribe client -> server none
0x03 Ack client -> server message_ids
0x04 Ping client -> server timestamp
0x10 Envelope server -> client QueuedEnvelope
0x11 Status server -> client connection state
0x12 Error server -> client ErrorEnvelope
0x13 Pong server -> client echoed timestamp
  • Implementations MUST ignore unknown frame types rather than closing the connection
  • A frame carrying an ErrorEnvelope MUST use the same codes as the HTTP binding

The primary foreground binding.

https://{server}/api/v1/envelopes/subscribe

WebTransport connection setup is 1-RTT cold and 0-RTT on resumption against roughly three for WebSocket, and that connection survives a network change rather than dying on a WiFi-to-cellular handoff. This is important when we’re holding one connection per mailbox.

  • Clients MUST open one connection per mailbox
  • Clients SHOULD use 0-RTT resumption where available
  • Servers MUST authorize each connection with a retrieval session for the mailbox it covers

This is fallback for when QUIC is unreachable. QUIC runs over UDP and that gets blocked on a lot of corporate networks and some carriers.

GET /api/v1/envelopes/subscribe
Upgrade: websocket

Identical frame format and semantics. The only difference is the handshake cost and that a network change forces a reconnect.

  • Clients SHOULD attempt WebTransport first and fall back to WebSocket only on failure to establish
  • Clients MUST NOT interpret a WebSocket fallback as a signal about server capability beyond reachability

For clients that can hold no connection at all.

Polling is FetchEnvelopes on a timer. It needs no binding of its own.

  • Clients SHOULD poll no more often than the interval the server advertises
  • Clients SHOULD add jitter, so a population of pollers does not synchronize

Servers MAY limit concurrent delivery connections per source address, as resource protection.

A push target is registered against exactly one mailbox and stored until revoked. It is durable in a design where almost nothing else is.

  • Wakeups MUST be contentless
  • Wakeups MUST NOT name the mailbox that received a message
  • Clients SHOULD register distinct endpoints per mailbox where the distributor permits it
  • Servers MUST discard a push target when its mailbox is deregistered

Self-hostable, and the only push target a self-hosted server can use unaided. The endpoint is user-supplied: ntfy, gotify, or any other UnifiedPush distributor.

The server POSTs a contentless body to the endpoint. The client wakes, opens a retrieval session, fetches, and sleeps.

APNs and FCM cannot be used directly by a self-hosted server, because both require the credentials of whoever ships the client application rather than whoever runs the server. A gateway holds those credentials and forwards to the platform; the servre only ever sees a URL and a token.

This introduces a centralized dependency into a federated protocol. It is a compromise made for platform reach, not a design goal, and a deployment that can use UnifiedPush should.