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

# WebSocket-Stream (Abonnements)

> Abonnieren Sie die Sportarten, Ligen, Events oder Märkte, die Sie tatsächlich verfolgen — und erhalten Sie nur diese.

<Note>
  Echtzeit-Streams erfordern den **Sharp**-Tarif.
</Note>

`wss://api.apinn.io/ws` liefert Quotenbewegungen wie der [SSE-Stream](/de/concepts/streaming), mit einem entscheidenden Unterschied: die Verbindung ist **bidirektional**, Sie können dem Server also mitteilen, was Sie möchten. Ohne Abonnement erhalten Sie das gesamte Buch — genau das, was der SSE-Stream sendet.

Übergeben Sie Ihren Schlüssel im Header `X-API-Key` oder als `?key=IHR_SCHLUESSEL` in der URL (ein Browser kann bei einer WebSocket-Verbindung keinen Header setzen).

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

const sock = new WebSocket("wss://api.apinn.io/ws?key=IHR_SCHLUESSEL");

sock.on("open", () => {
  sock.send(JSON.stringify({ op: "subscribe", sports: [29], markets: ["moneyline"] }));
});

sock.on("message", (buf) => {
  const m = JSON.parse(buf);
  if (m.t === "snapshot") {
    // vollständiger aktueller Stand Ihres Abonnements
    console.log(m.count, "Zeilen");
  } else if (m.t === "delta") {
    // nur die geänderten Zeilen innerhalb Ihres Bereichs
    for (const row of m.rows) console.log(row.event_id, row.market, row.odds1, row.odds2);
  }
});
```

## Abonnieren

Senden Sie `{"op": "subscribe", ...}` mit einer beliebigen Kombination der folgenden Filter. Sie wirken kumulativ: eine Zeile muss alle gesetzten Filter erfüllen.

| Filter    | Typ             | Beispiel                                    |
| --------- | --------------- | ------------------------------------------- |
| `sports`  | Sport-IDs       | `{"sports": [29, 33]}`                      |
| `leagues` | Liga-IDs        | `{"leagues": [2436]}`                       |
| `events`  | Event-IDs       | `{"events": [1632682788]}`                  |
| `markets` | Marktnamen      | `{"markets": ["moneyline", "totals"]}`      |
| `periods` | Periodennummern | `{"periods": [0]}` — nur reguläre Spielzeit |

Ein neues Abonnement ersetzt das vorherige und liefert einen aktuellen Snapshot. `{"op": "unsubscribe"}` bringt Sie zurück zum vollständigen Buch, und `{"op": "ping"}` antwortet mit `{"t": "pong"}`.

## Server-Nachrichten

| `t`        | Inhalt                                          | Bedeutung                                                                       |
| ---------- | ----------------------------------------------- | ------------------------------------------------------------------------------- |
| `hello`    | `{ subscription }`                              | Beim Verbinden und nach jeder Abonnementänderung; spiegelt den aktiven Stand.   |
| `snapshot` | `{ count, rows }`                               | Vollständiger Stand **Ihres Abonnements**, direkt nach dem Abonnieren.          |
| `delta`    | `{ rows, specials, removed, removed_specials }` | Was sich seit der vorherigen Nachricht geändert hat — und was verschwunden ist. |
| `pong`     | `{ ts }`                                        | Antwort auf `ping`.                                                             |

Jedes Delta trägt zusätzlich ein `seq`, das bei jeder Übertragung hochgezählt wird. Ein Client ohne Abonnement erhält alle Nummern lückenlos: eine Lücke bedeutet eine verlorene Nachricht. Ein Client mit Abonnement sieht normalerweise Lücken — die Übertragungen, die ihn nicht betrafen.

## Entfernungen

`removed` und `removed_specials` führen die Serien auf, die nicht mehr existieren: ein beendetes Spiel, ein geschlossener Markt. **Löschen Sie sie aus Ihrem Buch.** Wer sie ignoriert, liefert weiterhin Quoten zu verschwundenen Märkten: über neun Stunden gemessen sind das rund 2,5 % des Buchs, mit steigender Tendenz.

Ein Entfernungseintrag enthält nur die Kennung der Serie, keine Preise: `event_id`, `period`, `market`, `line` bei Quoten; `event_id`, `special_id`, `period`, `contestant_name`, `handicap` bei Specials. Sie werden wie alles andere nach Ihrem Abonnement gefiltert.

Die Zeilen haben dieselbe Form wie bei `/api/odds`: `event_id`, `period`, `market`, `line`, die Quoten (`odds1`/`odds0`/`odds2`), die fairen Quoten (`todds*`), `status` und `timestamp` — der Zeitpunkt der **letzten Preisänderung**, nicht des Versands.

## Warum abonnieren

Filtern ist nicht bloß Komfort. Gemessen am laufenden Buch entfallen auf Fußball allein rund 40 % aller Bewegungen, auf Moneyline in regulärer Spielzeit rund 49 %, und ein einzelnes Spiel fällt kaum ins Gewicht. Wer eine Handvoll Spiele verfolgt, geht von Zehntausenden Zeilen pro Stunde auf einige Dutzend zurück.

<Tip>
  Wie beim SSE-Stream zählt die Verbindung als **1 Anfrage**, im Moment des Verbindens. Was Sie abonnieren, ändert das empfangene Volumen, nicht den Preis.
</Tip>

<Note>
  Quotenzeilen tragen `event_id`, `period` und `market`, aber weder Sportart noch Liga — diese werden über die Fixtures aufgelöst. Ein Event, das noch nicht im Fixtures-Index steht, erfüllt keinen `sports`- oder `leagues`-Filter; filtern Sie über `events`, wenn Sie es sofort brauchen.
</Note>
