> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eventory.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connecting

> Open the WebSocket, handle the handshake errors, and reconnect safely.

## Endpoint

```
wss://api.eventory.ai/eventory/notifications/subscribe
```

TLS is required; `ws://` is not accepted. Send your key in the `apikey` header on the
upgrade request. It is checked once, at connection time. After the socket is open no
further authentication happens.

<Warning>
  The browser `WebSocket` constructor cannot set custom headers, so the stream cannot be
  opened directly from browser code. Use a native client (Node.js, Python, Go, …) or
  proxy the connection through your own backend, which also keeps your key off the
  client.
</Warning>

## Lifecycle

1. The gateway authenticates the request and checks that the key is granted the stream.
2. The server checks that your watchlist is not empty. An empty watchlist is rejected
   with `400` before the upgrade.
3. The request upgrades with the standard `101 Switching Protocols` handshake.
4. The server pushes JSON text frames. Anything you send over the socket is read and
   discarded; there is no client-to-server protocol.

**No heartbeat is sent.** To detect a half-open connection (NAT timeout, an
intermediary silently dropping the socket), track the time since the last message and
reconnect once it exceeds your threshold. Five minutes is a reasonable default.

On disconnect, from either side, no close-frame payload is sent. Treat any unexpected
close as "reconnect with backoff".

## Errors

These are HTTP responses returned **before** the upgrade completes.

| Status | Cause                                                   | What to do                                                                            |
| ------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `400`  | Your watchlist is empty.                                | Add at least one item with `POST /watchlist`, then reconnect.                         |
| `401`  | No key sent, or the key was rejected.                   | See [Authentication](/authentication).                                                |
| `403`  | The key is not granted the stream.                      | Contact Eventory.                                                                     |
| `429`  | Too many connection attempts this minute.               | Back off. Connection attempts count against the 30 per minute limit; messages do not. |
| `502`  | The gateway could not reach its authentication service. | Retry with backoff.                                                                   |

After the upgrade, a message that fails to parse on your side should be logged and
skipped; the server keeps streaming.

## Reconnection strategy

Use exponential backoff starting at one second, capped at 30 seconds, reset on a
successful open. A tight reconnect loop exhausts the rate limit in seconds and then
locks you out for the rest of the minute.

<CodeGroup>
  ```javascript Node.js theme={null}
  import WebSocket from "ws";

  const URL = "wss://api.eventory.ai/eventory/notifications/subscribe";
  const API_KEY = process.env.EVENTORY_API_KEY;
  const MAX_DELAY = 30_000;
  const STALE_AFTER = 5 * 60_000;

  let delay = 1_000;

  function connect() {
    const ws = new WebSocket(URL, { headers: { apikey: API_KEY } });
    let lastMessage = Date.now();

    const watchdog = setInterval(() => {
      if (Date.now() - lastMessage > STALE_AFTER) ws.terminate();
    }, 30_000);

    ws.on("open", () => { delay = 1_000; });

    ws.on("message", (data) => {
      lastMessage = Date.now();
      try {
        handle(JSON.parse(data.toString()));
      } catch (err) {
        console.error("bad frame", err);
      }
    });

    ws.on("close", () => {
      clearInterval(watchdog);
      setTimeout(connect, delay);
      delay = Math.min(delay * 2, MAX_DELAY);
    });

    ws.on("error", (err) => console.error("ws error", err));
  }

  function handle(msg) {
    // Dispatch on msg.sections_type; see "Message format".
  }

  connect();
  ```

  ```python Python theme={null}
  import asyncio, json, os
  import websockets  # pip install websockets

  URL = "wss://api.eventory.ai/eventory/notifications/subscribe"
  HEADERS = {"apikey": os.environ["EVENTORY_API_KEY"]}
  MAX_DELAY = 30
  STALE_AFTER = 5 * 60


  async def run():
      delay = 1
      while True:
          try:
              async with websockets.connect(URL, additional_headers=HEADERS) as ws:
                  delay = 1
                  while True:
                      raw = await asyncio.wait_for(ws.recv(), timeout=STALE_AFTER)
                      try:
                          handle(json.loads(raw))
                      except ValueError as e:
                          print("bad frame", e)
          except Exception as e:
              print("disconnected:", e)
          await asyncio.sleep(delay)
          delay = min(delay * 2, MAX_DELAY)


  def handle(msg: dict) -> None:
      # Dispatch on msg["sections_type"]; see "Message format".
      ...


  asyncio.run(run())
  ```

  ```go Go theme={null}
  package main

  import (
  	"encoding/json"
  	"log"
  	"net/http"
  	"os"
  	"time"

  	"github.com/gorilla/websocket"
  )

  const url = "wss://api.eventory.ai/eventory/notifications/subscribe"

  func main() {
  	delay := time.Second
  	for {
  		if err := run(); err != nil {
  			log.Println("disconnected:", err)
  		}
  		time.Sleep(delay)
  		if delay *= 2; delay > 30*time.Second {
  			delay = 30 * time.Second
  		}
  	}
  }

  func run() error {
  	h := http.Header{"apikey": {os.Getenv("EVENTORY_API_KEY")}}
  	c, _, err := websocket.DefaultDialer.Dial(url, h)
  	if err != nil {
  		return err
  	}
  	defer c.Close()
  	for {
  		c.SetReadDeadline(time.Now().Add(5 * time.Minute))
  		_, raw, err := c.ReadMessage()
  		if err != nil {
  			return err
  		}
  		var msg map[string]any
  		if err := json.Unmarshal(raw, &msg); err != nil {
  			log.Println("bad frame:", err)
  			continue
  		}
  		handle(msg)
  	}
  }

  func handle(msg map[string]any) {
  	// Dispatch on msg["sections_type"]; see "Message format".
  }
  ```
</CodeGroup>

## Best practices

1. **One connection per process.** A single socket covers your whole watchlist. Do not
   open one per event.
2. **Reconnect with backoff.** See above.
3. **Filter client-side** for everything except `event_id` and `change_type`.
4. **Treat `event_infos` as optional.** It is omitted entirely when the platform does
   not expose metadata.
5. **Do not block in your message handler.** Process notifications asynchronously so
   back-pressure never stalls the socket.
6. **Track time since the last message** and reconnect after about five minutes of
   silence.
