WebSockets
Subscribe to mail and delivery events over one authenticated real-time connection.
Use WebSockets when an agent or console is already running and should react without polling. For a sleeping server or Cloudflare Worker, use a webhook instead—the delivery wakes it and retries without requiring a process to hold a connection.
Connect and subscribe
WebSocket authentication uses the same API key as REST, passed as api_key
because browser WebSocket handshakes cannot set an Authorization header. Scope
the key to one inbox or pod whenever the listener does not need the whole
organization.
const apiKey = process.env.CARLYEMAIL_API_KEY;
const socket = new WebSocket(
`wss://ws.carlyemail.com/v0?api_key=${encodeURIComponent(apiKey)}`,
);
socket.addEventListener("open", () => {
socket.send(JSON.stringify({
type: "subscribe",
event_types: ["message.received", "message.delivered", "message.bounced"],
inbox_ids: ["agent@carlyemail.com"],
}));
});
socket.addEventListener("message", ({ data }) => {
const frame = JSON.parse(data);
if (frame.type === "event" && frame.event_type === "message.received") {
console.log(frame.message, frame.thread);
}
});
The acknowledgement contains the filters CarlyEmail actually applied:
{
"type": "subscribed",
"event_types": ["message.received"],
"inbox_ids": ["agent@carlyemail.com"],
"pod_ids": []
}
An inbox- or pod-scoped key cannot widen itself. If it requests another inbox,
the acknowledgement shows its own scope and only matching events arrive.
The socket also honors the key's permission map: message events require
message_read, and domain verification events require domain_read.
Event frames
Every event has a stable event_id and an event_type. The entity key follows
the event: received mail has message and thread, sent mail has send, and
delivery feedback has delivery, bounce, complaint, or reject.
{
"type": "event",
"event_type": "message.received",
"event_id": "evt_00n...",
"message": {
"inbox_id": "agent@carlyemail.com",
"message_id": "<example@sender.test>",
"subject": "Can you help?"
},
"thread": {
"thread_id": "thread_00n...",
"message_count": 1
}
}
Supported event types are the same as webhooks: received, sent, delivery, bounce, complaint, rejection, and domain verification events.
Reconnect without losing mail
WebSocket delivery is at least once. Deduplicate side effects with event_id.
Connections receive keepalive {"type":"ping"} frames, but the platform may
still close a connection for maintenance or after its maximum lifetime. Clients
must reconnect with exponential backoff, send the subscription again, and
backfill inbox events from the REST event endpoint using the last durable
cursor.
let delay = 250;
function connect() {
const socket = new WebSocket(
`wss://ws.carlyemail.com/v0?api_key=${encodeURIComponent(apiKey)}`,
);
socket.addEventListener("open", () => {
delay = 250;
socket.send(JSON.stringify({ type: "subscribe" }));
});
socket.addEventListener("close", () => {
setTimeout(connect, delay + Math.random() * delay);
delay = Math.min(delay * 2, 30_000);
});
}
connect();
Warning
Do not put the full WebSocket URL in application logs—the query string contains the API key. CarlyEmail's own access logs deliberately omit query strings.