Skip to main content
WebSockets provide a persistent, bidirectional connection to OpenMail for receiving email events in real-time. Unlike webhooks, WebSockets don’t require a public URL or external tools like ngrok.

Why use WebSockets?

FeatureWebhookWebSocket
SetupRequires public URL + ngrokNo external tools needed
ConnectionHTTP request per eventPersistent connection
DirectionOpenMail → Your serverBidirectional
FirewallMust expose portOutbound only
LatencyHTTP round-tripInstant streaming
WebSockets are the recommended delivery method for agent platforms. Agents are long-running processes that benefit from a persistent event stream rather than a callback endpoint.

Connecting

Authenticate with your API key via the Authorization header or token query parameter.
const WebSocket = require("ws");

const ws = new WebSocket("wss://api.openmail.sh/v1/ws", {
  headers: { Authorization: `Bearer ${process.env.OPENMAIL_API_KEY}` },
});
The ?token= query parameter is useful for browser clients and CLI tools that can’t set custom headers on WebSocket connections.

Subscribe

After connecting, send a subscribe message to start receiving events. You can subscribe to specific inboxes, specific event types, or both.
Subscribe to all inboxes and all events
{ "type": "subscribe" }
Subscribe to specific inboxes
{ "type": "subscribe", "inbox_ids": ["inb_8f3a1b2c", "inb_2d4e6f8a"] }
Subscribe to specific event types
{
  "type": "subscribe",
  "inbox_ids": ["inb_8f3a1b2c"],
  "event_types": ["message.received"]
}
The server responds with a subscribed confirmation:
{ "type": "subscribed", "inbox_ids": [] }
When inbox_ids is empty in the response, you are subscribed to all inboxes on your account. Otherwise it lists the specific inboxes you’re subscribed to. Subscriptions accumulate - sending multiple subscribe messages adds to your existing subscriptions.
When subscribing to specific inbox_ids, OpenMail verifies you own each inbox. Unowned inbox IDs return an error.

Receiving events

Events arrive as JSON messages on the WebSocket. The payload is identical to the webhook event payload.
{
  "event": "message.received",
  "event_id": "evt_7f2a3b4c",
  "occurred_at": "2026-02-24T10:05:00.000Z",
  "delivered_at": "2026-02-24T10:05:00.012Z",
  "attempt": 1,
  "inbox_id": "inb_8f3a1b2c",
  "external_id": "user_abc123",
  "thread_id": "thr_9d4e5f6a",
  "message": {
    "id": "msg_4c8d5e6f",
    "rfc_message_id": "<original@message.id>",
    "from": "customer@example.com",
    "to": "jane@yourco.openmail.sh",
    "subject": "Re: Your order",
    "body_text": "Thanks for following up...",
    "attachments": [],
    "received_at": "2026-02-24T10:05:00.000Z"
  }
}

Event types

EventDescription
message.receivedA new inbound email was delivered to an inbox

Full examples

const WebSocket = require("ws");

const OPENMAIL_API_KEY = process.env.OPENMAIL_API_KEY;

function connect() {
  const ws = new WebSocket("wss://api.openmail.sh/v1/ws", {
    headers: { Authorization: `Bearer ${OPENMAIL_API_KEY}` },
  });

  ws.on("open", () => {
    console.log("Connected to OpenMail");
    ws.send(JSON.stringify({ type: "subscribe" }));
  });

  ws.on("message", (data) => {
    const event = JSON.parse(data);

    if (event.type === "subscribed") {
      console.log("Subscribed to:", event.inbox_ids);
      return;
    }

    if (event.event === "message.received") {
      console.log(`New email from: ${event.message.from}`);
      console.log(`Subject: ${event.message.subject}`);

      // Route by external_id to the right agent/container
      const container = getContainerByUserId(event.external_id);
      container.deliverEmail({
        threadId: event.thread_id,
        message: event.message,
      });
    }
  });

  ws.on("close", () => {
    console.log("Disconnected, reconnecting in 5s...");
    setTimeout(connect, 5000);
  });

  ws.on("error", (err) => {
    console.error("WebSocket error:", err.message);
  });
}

connect();

Unsubscribe

Remove subscriptions without disconnecting.
Unsubscribe from specific inboxes
{ "type": "unsubscribe", "inbox_ids": ["inb_8f3a1b2c"] }
Unsubscribe from everything
{ "type": "unsubscribe" }
After unsubscribing from everything, you stop receiving events until you send a new subscribe message.

Protocol reference

Client → Server

MessageFieldsDescription
subscribeinbox_ids?, event_types?Subscribe to events. Omit both to get all events for all inboxes.
unsubscribeinbox_ids?Remove subscriptions. Empty = unsubscribe from all.
ackevent_idAcknowledge receipt of an event (reserved for future replay).
pingApplication-level keepalive.

Server → Client

MessageFieldsDescription
subscribedinbox_idsSubscription confirmed. Empty array = all inboxes.
unsubscribedinbox_idsUnsubscription confirmed.
errormessageError details (invalid JSON, unknown type, unauthorized inbox).
pongResponse to ping.
(event)Same as webhook payloadEmail event pushed in real-time.

Delivery semantics

When a customer has both an active WebSocket connection and a webhook URL configured, OpenMail prefers the WebSocket. If the WebSocket is down, events fall back to the webhook with the same retry policy.
ScenarioDelivery method
WebSocket connected + subscribedWebSocket (instant)
WebSocket disconnected, webhook URL setWebhook (with retries)
NeitherEvent stored, marked as failed after retries

Connection management

  • Heartbeat - The server sends WebSocket pings every 30 seconds. Connections that don’t respond with a pong within 10 seconds are terminated.
  • Reconnection - Implement client-side reconnection with exponential backoff. The Python websockets library handles this automatically with async for ws in websockets.connect(...).
  • Multiple connections - You can open multiple WebSocket connections per account. Events are delivered to all connections whose subscriptions match.

Comparison with webhooks

Use WebSockets when:
  • Your agent runs locally or behind a firewall
  • You want instant delivery with no round-trip latency
  • You don’t want to manage a public HTTPS endpoint
Use webhooks when:
  • Your server is already publicly accessible
  • You need guaranteed delivery with automatic retries
  • You prefer stateless, request-based integration
Both deliver the same event payload. You can switch between them without changing your event handling logic.