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

> ## Agent Instructions
> Base URL: https://api.eventory.ai. Send your API key in the apikey header. Market Data and Live Availability calls cost credits, pings included; GET /usage is free, so use it to test a key. Costs per endpoint: https://eventory.ai/docs/credits-and-access.md

# Connect to the notifications WebSocket

> 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. A browser can pass the
> key in the `api_key` query parameter instead
> (`wss://api.eventory.ai/eventory/notifications/subscribe?api_key=…`), but never ship
> your key in client-side code: anyone who loads the page can read it. Use a native
> client (Node.js, Python, Go, …) or proxy the connection through your own backend.

## 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. WebSocket pings are answered with
   pongs.

**The server pings every connection every 30 seconds**, so intermediaries do not close
an idle socket. Most WebSocket clients answer pings automatically; sending your own
pings is optional. A ping only proves the connection is up, not that notifications are
flowing: track the time since the last message as well, and reconnect once it exceeds
your threshold. Five minutes is a reasonable default.

When the server's own feed of notifications ends, it closes the socket with close code
`1011`: reconnect. A server restart drops the connection without a close frame. Treat
any unexpected close as "reconnect with backoff". The server restarts every few hours,
so expect routine disconnects.

Delivery is at-most-once. Each connection gets its own queue, created when it opens and
deleted when it closes, so nothing is kept for you while you are disconnected and nothing
is replayed when you reconnect.

## 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](/docs/authentication.md). |
| `403` | The key is not granted the Notifications API. Access is not part of any API plan. | Contact Eventory. |
| `500` / `503` | The stream server failed or is unreachable, for example while it restarts. The body is the [gateway envelope](/docs/errors.md#gateway-errors) with code `service_unavailable`. | Retry with backoff. |
| `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. The stream is not rate limited, but a tight reconnect loop hammers
the service while it recovers and reconnects no sooner.

```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".
}
```

## Best practices

1. **One connection per account.** A single socket covers your whole watchlist. Do not
   open one per event; extra sockets are for failover only, see
   [Multiple connections](/docs/stream/overview.md#multiple-connections).
2. **Reconnect with backoff.** See above.
3. **Filter client-side** for everything except the event, `task_active` 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.