# HTTP-API (https://docs.pixl.lennox.zip/sdk/api)

> REST- und Stream-Endpunkte ohne SDK

Das SDK ist nur eine dünne Schicht über diese Endpunkte. Wenn du kein JavaScript im Browser nutzt (z.B. ein Bot oder ein Script auf deinem Rechner), sprichst du sie direkt an.

## Grundlagen [#grundlagen]

- Basis-URL: `https://pixl.lennox.zip/api/v1`
- Authentifizierung: Header `Authorization: Bearer pixl_DEIN_SCHLÜSSEL`. Einen Query-Parameter für den Schlüssel gibt es bewusst nicht, URLs landen zu leicht in Logs.
- Alle Endpunkte sind lesend (`GET`), außer [`POST /tts`](#post-tts). Alle erlauben Aufrufe von fremden Seiten (CORS `*`, ohne Cookies).
- Die API liefert nur Daten, sie rechnet nichts aus. Summen o.ä. bildest du selbst aus den Events.
- Antworten sind JSON. Fehler haben die Form `{ "error": "..." }`, bei TTS zusätzlich mit `problem`.
- Limit: 120 Anfragen pro Minute und Schlüssel.

| Status | Bedeutung |
|---|---|
| `400` | Ungültiger Parameter, z.B. kaputtes Datum |
| `401` | Schlüssel fehlt, ist falsch oder gelöscht |
| `403` | Dem Schlüssel fehlt das nötige [Recht](/sdk/api-keys#rechte), oder das Modul ist im Kanal aus (`"code": "module_disabled"`, siehe unten) |
| `404` | `since=stream`, aber es gab noch keinen Stream, oder ein TTS-Audio ist abgelaufen |
| `422` | TTS: Text gesperrt, leer oder unbekannte Stimme |
| `429` | Zu viele Anfragen, bei TTS auch: Credits aufgebraucht |

**Module:** Owner und Admins können ganze Funktionen eines Kanals ausschalten (aktuell TTS). Die Endpunkte des Moduls antworten dann mit `403` und `{ "error": "...", "code": "module_disabled", "problem": "module_disabled", "module": "tts" }`. Nach dem Einschalten geht alles wie vorher, der Schlüssel bleibt gültig.

Zeitangaben (`since`, `until`, `cursor`) gehen als ISO-Datum (`2026-10-01T18:00:00Z`) oder Unix-Millisekunden. `since` kennt zusätzlich `stream` für den Start des letzten Streams.

## `GET /org` [#get-org]

Kanal des Schlüssels und seine Scopes. Braucht kein besonderes Recht.

```json
{ "id": "…", "name": "laGameTV", "logo": "https://…", "twitchChannelLogin": "lagametv", "scopes": ["events:read"] }
```

## `GET /events` [#get-events]

Eventlog, neueste zuerst. Braucht `events:read`.

| Parameter | Bedeutung |
|---|---|
| `since` | Ab wann (optional) |
| `until` | Bis wann, exklusiv (optional) |
| `types` | Kommagetrennte Event-Typen, z.B. `twitch.sub,twitch.cheer` |
| `limit` | 1 bis 200, Standard 50 |
| `cursor` | `nextCursor` der vorherigen Antwort |

```json
{
  "events": [
    { "id": "b24f…", "event": "twitch.cheer", "data": { "username": "Kekskruemel698", "bits": 100 }, "at": "2026-10-08T21:48:56.984Z" }
  ],
  "nextCursor": "2026-10-08T21:48:56.984Z"
}
```

Die Payloads stehen in der [Event-Referenz](/sdk/events#event-referenz). Erneut abgespielte Events stehen als eigene Zeile drin, erkennbar an `data.replay: true`.

## `GET /subs` [#get-subs]

Aktuelle Sub-Zahl direkt von Twitch, nicht aus dem Eventlog. Braucht `subs:read` und einen verbundenen Twitch-Kanal. pixl fragt Twitch höchstens einmal pro Minute, die Zahl ist also bis zu 60 Sekunden alt.

```json
{ "total": 312, "points": 340, "at": "2026-10-20T08:15:00.000Z" }
```

`total` zählt wie bei Twitch den Streamer selbst mit. `points` sind die Sub-Punkte (Stufe 1 = 1, Stufe 2 = 2, Stufe 3 = 6). Ändert sich die Zahl, kommt im Stream `subs.updated` mit denselben Feldern, meist wenige Sekunden nach dem Sub. Ohne verbundenen Kanal oder mit abgelaufener Twitch-Verbindung antwortet der Endpunkt mit `409`.

## `GET /spotify/now-playing` [#get-spotifynow-playing]

Was gerade auf der verbundenen Spotify-Wiedergabe des Kanals läuft. Braucht `spotify:read` und das Modul Musik (sonst `403` mit `problem: "module_disabled"`).

```json
{
  "playing": true,
  "progressMs": 83000,
  "at": "2026-10-20T18:02:11.000Z",
  "track": {
    "uri": "spotify:track:4cOdK2wGLETKBW3PvgPWqT",
    "name": "Never Gonna Give You Up",
    "artists": ["Rick Astley"],
    "album": "Whenever You Need Somebody",
    "image": "https://i.scdn.co/image/…",
    "durationMs": 213573,
    "explicit": false,
    "url": "https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT"
  },
  "addedBy": { "name": "lagametv" }
}
```

`progressMs` gilt zum Zeitpunkt `at`. Läuft der Song (`playing`), rechnest du selbst weiter, das SDK hat dafür `Pixl.progress()`. Läuft Spotify auf keinem Gerät, ist `track` null. `addedBy` ist gesetzt, wenn jemand den Song über pixl in die Queue gelegt hat. Ändert sich etwas (anderer Song, Play/Pause, Sprung im Song), kommt im Stream `spotify.now_playing` mit denselben Feldern.

pixl fragt Spotify nur ab, solange ein Widget mit `spotify:read` am Stream hängt: während des Streams alle 5 Sekunden und direkt zum Songende, offline alle 30 Sekunden. Ohne Spotify-Verbindung oder mit abgelaufener Verbindung antwortet der Endpunkt mit `409`, bremst Spotify gerade, mit `429`.

## `GET /emotes` [#get-emotes]

Alle Emotes von 7TV, BTTV und FFZ im Kanal, Kanal- und globale Sets. Braucht `events:read` und einen verbundenen Twitch-Kanal. Gibt es einen Namen mehrfach, steht nur der Gewinner drin (7TV vor BTTV vor FFZ, Kanal vor global). Twitch-Emotes fehlen, die kommen direkt mit jeder Nachricht.

```json
{ "emotes": [{ "provider": "7tv", "id": "01G3WEGZN0000ET2J0MQP5YJ0G", "name": "GAMBA", "animated": true }] }
```

## `GET /7tv/paints` [#get-7tvpaints]

Definitionen von 7TV-Paints (farbige Namen), z.B. `?ids=01JHXJFACX42RV996VE9933TB8`. Braucht `events:read`, höchstens 100 IDs pro Abfrage. Welchen Paint ein Chatter trägt, steht als `paint` in `twitch.chat_message`.

```json
{
  "paints": [{
    "id": "01JHXJFACX42RV996VE9933TB8",
    "name": "Emerald Doppler",
    "layers": [{ "type": "image", "url": "https://cdn.7tv.app/paint/…/1x.webp", "animated": true, "opacity": 1 }],
    "shadows": [{ "color": "#39D21EFF", "x": 0, "y": 0, "blur": 0.1 }]
  }]
}
```

Ebenen gibt es als `linear-gradient` (`angle`, `repeating`, `stops`), `radial-gradient` (`shape`, `repeating`, `stops`), `color` und `image`. Farben als `#RRGGBBAA`, `stops[].at` von 0 bis 1, Schatten in px. Das SDK macht daraus CSS (`pixl.chat.paintStyle()`).

## `GET /streamelements/{pfad}` [#get-streamelementspfad]

Lesender Zugriff auf den StreamElements-Kanal, der mit deinem Kanal verbunden ist. Braucht `streamelements:read` und einen in den Einstellungen hinterlegten StreamElements Overlay Token. pixl reicht die Anfrage mit diesem Token an StreamElements weiter, der Token selbst bleibt bei pixl. Query-Parameter gehen unverändert mit, die Antwort ist die von StreamElements.

| Pfad | StreamElements-Endpunkt |
|---|---|
| `activities` | Activity-Historie (Parameter `after`, `before`, `limit`, `types` usw. wie bei StreamElements) |
| `sessions` | Session-Daten: letzter Tip, Top-Spender, Goals, Zähler |
| `tips/leaderboard` | Spender-Rangliste |
| `points/top`, `points/alltime` | Loyalty-Punkte-Ranglisten |
| `store/items` | Artikel im Loyalty-Store |
| `contests` | Contests |
| `songrequest/queue`, `songrequest/playing` | Songrequest-Warteschlange und aktueller Song |

Andere Pfade antworten mit `404`. Ist StreamElements nicht verbunden, kommt `409`. Zusätzlich zum Limit pro Schlüssel gilt hier eins pro Kanal (60 Anfragen pro Minute), weil sich alle Schlüssel denselben StreamElements-Zugang teilen.

```bash
curl -H "Authorization: Bearer pixl_…" "https://pixl.lennox.zip/api/v1/streamelements/tips/leaderboard"
```

## `GET /stream` [#get-stream]

Live-Verbindung als [Server-Sent Events](https://developer.mozilla.org/de/docs/Web/API/Server-sent_events). Jede Nachricht ist eine `data:`-Zeile mit JSON:

```
data: {"orgId":"…","event":"twitch.cheer","data":{"username":"…","bits":100},"id":"…","at":"2026-10-11T19:00:00.000Z"}
```

- Was ankommt, hängt von den Scopes ab: `twitch.*`-, `stream.*`-, `streamelements.*`- und `seventv.*`-Events mit `events:read`, `tts.ready`, `tts.skip` und `tts.clear` mit `tts:read`, `subs.updated` mit `subs:read` und `spotify.now_playing` mit `spotify:read`.
- Werden die Scopes geändert, endet die Verbindung ohne weitere Nachricht. Einfach neu verbinden, dann gelten die neuen Rechte.
- `replay: true` markiert erneut abgespielte Events, `id` und `at` gibt es bei Events aus dem Eventlog. Über die `id` erkennst du Events, die du schon per `/events` geladen hast.
- Alle 15 Sekunden kommt ein Kommentar (`: ping`), damit die Verbindung offen bleibt. Kommt länger als 45 Sekunden gar nichts, ist die Verbindung tot, auch wenn sie noch offen aussieht. Dann einfach neu verbinden.
- Bei einem Update von pixl endet die Verbindung, und der Server ist ein paar Sekunden nicht erreichbar. Verbinde bei jeder Antwort außer `401` und `403` mit kurzer Wartezeit neu, auch bei `502` oder `503`.
- Wird der Schlüssel gelöscht, kommt `api_key.revoked` und die Verbindung endet. Danach nicht neu verbinden.

Weil der Schlüssel als Header mitgeht, funktioniert der Browser-eigene `EventSource` hier nicht. Nutz das SDK, `fetch()` mit Streaming oder eine SSE-Bibliothek, die Header unterstützt.

```bash
curl -N -H "Authorization: Bearer pixl_DEIN_SCHLÜSSEL" https://pixl.lennox.zip/api/v1/stream
```

## TTS [#tts]

Wie das Ganze zusammenspielt, steht unter [TTS](/sdk/tts).

### `POST /tts` [#post-tts]

Text vorlesen lassen. Braucht `tts:write`, Body als JSON (`Content-Type: application/json`):

```bash
curl -X POST -H "Authorization: Bearer pixl_…" -H "Content-Type: application/json"   -d '{"text": "Danke fürs Einlösen!", "voice": "polly:hans"}'   "https://pixl.lennox.zip/api/v1/tts"
```

Optional `filters`: `false` filtert weder Links noch Wiederholungen noch gesperrte Wörter, oder einzeln als Objekt, z.B. `{"links": false}` (Schlüssel `links`, `repeats`, `blockedWords`, fehlend = an). Unsichtbare Zeichen und die Längengrenze gelten immer.

Antwort (`201`): `{ "id": "…", "text": "Danke fürs Einlösen!", "voice": "polly:hans" }`. `text` ist der bereinigte Text. Danach geht `tts.ready` an alle Verbindungen mit `tts:read`. Fehler kommen mit `problem`, die Werte stehen unter [TTS](/sdk/tts#selbst-vorlesen-lassen).

### `GET /tts/voices` [#get-ttsvoices]

Verfügbare Stimmen und die Standardstimme des Kanals. Braucht `tts:read` oder `tts:write`.

```json
{ "voices": [{ "id": "polly:hans", "name": "Hans", "language": "de-DE", "gender": "male", "description": "…" }], "default": "polly:hans" }
```

### `GET /tts/{id}/audio` [#get-ttsidaudio]

Das Audio zu einem `tts.ready` als MP3. Braucht `tts:read`. Nach etwa einem Tag gibt es `404`.

### `GET /tts/{id}/marks` [#get-ttsidmarks]

Zeitmarken pro Wort zum Audio, für Untertitel, die mitlaufen. Braucht `tts:read`. Entstehen beim ersten Abruf und kosten dann beim Anbieter extra, danach kommen sie aus dem Zwischenspeicher. Nach etwa einem Tag gibt es `404`.

```json
{ "marks": [{ "word": "Lisa", "start": 6, "end": 286 }, { "word": "hat", "start": 286, "end": 473 }, { "word": "Stream", "start": 2654, "end": null }] }
```

Zeiten in Millisekunden ab Start des Audios. `end` ist `null`, wenn es nicht bekannt ist (beim letzten Wort: Ende des Audios).

### Events im Stream [#events-im-stream]

```
data: {"orgId":"…","event":"tts.ready","data":{"id":"…","voice":"polly:hans","text":"Danke fürs Einlösen!","source":"api"},"at":"…"}

data: {"orgId":"…","event":"tts.skip","data":{"id":"…"}}

data: {"orgId":"…","event":"tts.clear","data":{}}
```

`tts.skip` ohne `id` heißt: überspring, was gerade läuft. `tts.clear` heißt: brich ab, was läuft, und verwirf alles, was wartet. `replay: true` steht an erneut gesendeten Einträgen.
