Entwickler, Bot-API & Webhooks

Automatisiere deine Community mit Bots, oder schicke Nachrichten per eingehendem Webhook. Die SpeakSpeak-API ist Discord-kompatibel: verbreitete Bot-Bibliotheken lassen sich mit wenigen Anpassungen portieren.

Basis-URL: https://api.speakspeak.net/api/v1 · Echtzeit-Gateway: wss://api.speakspeak.net/api/v1/gateway · Öffentliche Beta.

Erste Schritte

  1. Anwendung erstellen. In den Entwickler-Einstellungen deines SpeakSpeak-Kontos legst du eine Anwendung an. Zu jeder Anwendung gehört automatisch ein Bot-Benutzer.
  2. Bot-Token erzeugen. Die Anwendung stellt ein Token der Form ssbot_<app>.<secret> aus. Das Token wird nur einmal angezeigt, bewahre es sicher auf, es ist so mächtig wie ein Passwort.
  3. Bot installieren. Installiere die Anwendung auf einem Server, auf dem du Server verwalten darfst, und wähle die Berechtigungen. Erst danach kann der Bot dort handeln.

Danach spricht dein Bot direkt mit der API:

# Wer bin ich?, bestätigt, dass das Token gültig ist
curl https://api.speakspeak.net/api/v1/users/@me \
  -H "Authorization: Bot ssbot_AZ-wUWG0…​.ovB1whFr…​"

Authentifizierung

Jede REST-Anfrage trägt den Authorization-Header mit dem Präfix Bot:

Authorization: Bot ssbot_<app>.<secret>
  • Das Token ist eine Obergrenze, keine Verleihung. Was ein Bot tatsächlich darf, ist immer Token-Rechte ∩ Rolle des Bots auf dem Server, auf dem Zielserver bei jedem Aufruf neu geprüft. Ein Token kann nie mehr, als die Bot-Rolle erlaubt.
  • Pro Server installiert. Ein Bot handelt nur auf Servern, auf denen die Anwendung installiert ist.
  • Widerrufbar. Ein geleaktes Token widerrufst du jederzeit in den Entwickler-Einstellungen; Aufrufe damit scheitern sofort.

REST-API

Discord-kompatible Endpunkte unter https://api.speakspeak.net/api/v1. IDs sind UUIDs (statt Discord-Snowflakes), sonst folgen Pfade und JSON-Formen dem gewohnten Schema.

BereichBeispiele
IdentitätGET /users/@me, GET /users/{id}
ServerGET /guilds/{id}, GET /guilds/{id}/members, GET /guilds/{id}/audit-logs
KanäleGET|PATCH|DELETE /channels/{id}, POST /guilds/{id}/channels
NachrichtenGET|POST /channels/{id}/messages, GET|PATCH|DELETE /channels/{id}/messages/{mid}
ReaktionenPUT|DELETE /channels/{id}/messages/{mid}/reactions/{emoji}/@me
PinsGET /channels/{id}/pins, PUT|DELETE /channels/{id}/pins/{mid}
RollenGET|POST /guilds/{id}/roles, PATCH|DELETE /guilds/{id}/roles/{rid}, PUT|DELETE /guilds/{id}/members/{uid}/roles/{rid}
MitgliederPATCH /guilds/{id}/members/{uid} (Nick/Rollen), DELETE … (Kick)
BansGET /guilds/{id}/bans, GET|PUT|DELETE /guilds/{id}/bans/{uid}
EinladungenPOST /channels/{id}/invites, DELETE /invites/{code}
TippenPOST /channels/{id}/typing

Eine Nachricht senden, und die Antwort:

curl -X POST https://api.speakspeak.net/api/v1/channels/{channel_id}/messages \
  -H "Authorization: Bot ssbot_…​" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hallo aus meinem Bot 👋"}'
{
  "id": "019fb0a1-2c3d-7e4f-…",
  "channel_id": "019fb0…",
  "content": "Hallo aus meinem Bot 👋",
  "author": { "id": "019fb0…", "username": "meinbot#bot", "bot": true },
  "timestamp": "2026-07-30T13:12:42.000Z"
}

Grenzen wie bei Discord: content max. 2000 Zeichen, Rollennamen max. 100, Kanal-Thema max. 1024. Über der Grenze, oder ein Null-Byte im Text, gibt es ein sauberes 400.

Eingehende Webhooks

Ein Webhook postet in genau einen Kanal, ohne Bot und ohne Login, die Zugangsdaten stecken in der URL. Ideal für CI-, Monitoring- oder Deploy-Meldungen.

  1. Erstellen: in den Kanal-Einstellungen (oder per API). Du bekommst webhook_id und token, das Token nur einmal.
  2. Auslösen: ein einfacher POST auf die Webhook-URL.
curl -X POST "https://api.speakspeak.net/api/v1/webhooks/{webhook_id}/{token}?wait=true" \
  -H "Content-Type: application/json" \
  -d '{"content": "✅ Build 2016 deployed", "username": "CI"}'

?wait=true lässt den Server die erstellte Nachricht zurückgeben (praktisch zum Bestätigen). Wird der Kanal überlastet, antwortet der Webhook mit 429 und einem Retry-After-Header, dann kurz warten und erneut senden.

Gateway (Echtzeit-Events)

Für Live-Events (neue Nachrichten, Reaktionen, Beitritte) hält dein Bot eine WebSocket-Verbindung zum Gateway. Ablauf wie bei Discord: HELLO (op 10) → IDENTIFY (op 2) → READY, danach regelmäßige Heartbeats.

// Node.js, minimaler Handshake (ohne Bibliothek)
const ws = new WebSocket("wss://api.speakspeak.net/api/v1/gateway");
ws.onmessage = (ev) => {
  const m = JSON.parse(ev.data);
  if (m.op === 10) {                    // HELLO
    setInterval(() => ws.send(JSON.stringify({ op: 1, d: null })),
                m.d.heartbeat_interval);   // Heartbeat
    ws.send(JSON.stringify({ op: 2, d: {   // IDENTIFY
      token: "ssbot_…​", intents: 33281,  // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT
      properties: { os: "linux", browser: "meinbot", device: "meinbot" }
    }}));
  } else if (m.t === "MESSAGE_CREATE") {
    console.log("neue Nachricht:", m.d.content);
  }
};

Welche Events du bekommst, steuern die Intents (Bitfeld im IDENTIFY), genau wie bei Discord. Das Gateway kennt diese Bits:

IntentBitWertSchaltet frei
GUILDS1 << 01Kanal-, Thread-, Rollen- und Server-Metadaten-Events
GUILD_MEMBERS1 << 12GUILD_MEMBER_ADD/REMOVE/UPDATE
GUILD_VOICE_STATES1 << 7128VOICE_STATE_UPDATE
GUILD_PRESENCES1 << 8256wird akzeptiert, liefert aber keine Events (siehe „Nur schreibend“ oben)
GUILD_MESSAGES1 << 9512MESSAGE_CREATE/UPDATE/DELETE
GUILD_MESSAGE_REACTIONS1 << 101024MESSAGE_REACTION_ADD/REMOVE
GUILD_MESSAGE_TYPING1 << 112048TYPING_START
MESSAGE_CONTENT1 << 1532768den Inhalt von Nachrichten-Events, nicht die Events selbst (siehe unten)

Unbekannte Bits werden ignoriert. intents: 0 ist gültig, liefert aber null Events, ein Bot, der auf Nachrichten reagieren soll, braucht mindestens GUILD_MESSAGES.

MESSAGE_CONTENT: Event kommt an, Inhalt ist leer?

MESSAGE_CONTENT (1 << 15) entscheidet nicht, ob Nachrichten-Events ankommen, das tut GUILD_MESSAGES (1 << 9). MESSAGE_CONTENT entscheidet nur, ob die Inhaltsfelder gefüllt sind: Ohne das Bit kommt MESSAGE_CREATE trotzdem an, aber content ist leer und embeds und attachments sind leere Listen, exakt wie bei Discords privilegiertem Intent. Das Symptom: dein Handler feuert, m.d.content ist "". Die Lösung: MESSAGE_CONTENT zusätzlich zu GUILD_MESSAGES setzen, zusammen mit GUILDS ergibt das intents: 33281 wie im Beispiel oben. Anders als bei Discord gibt es keinen Freigabeprozess, Bit setzen genügt; was der Bot überhaupt sieht, begrenzen weiterhin seine Kanal-Berechtigungen.

Status deines Bots (op 3)

Mit PRESENCE_UPDATE (op 3) setzt du, wie dein Bot in der Mitgliederliste erscheint, in allen Servern gleichzeitig. Du kannst denselben Aufbau auch direkt im IDENTIFY unter presence mitschicken; ohne Angabe ist dein Bot ab READY online.

ws.send(JSON.stringify({ op: 3, d: {
  status: "dnd",                                  // online | idle | dnd | invisible | offline
  activities: [{ type: 0, name: "Celeste" }]   // → "Playing Celeste"
}}));
  • Nur activities[0] wird ausgewertet: Typ 0 → „Playing X“, 1 → „Streaming X“, 2 → „Listening to X“, 3 → „Watching X“, 5 → „Competing in X“. Typ 4 (Custom) übernimmt dein state unverändert ohne Präfix. Maximal 128 Zeichen inklusive Präfix.
  • Die Präfixe sind in jeder Sprache englisch, der Satz besteht zur Hälfte aus deinem eigenen Text, und nur unsere Hälfte zu übersetzen liest sich schlechter als eine einheitliche Zeile.
  • Ein unbekannter status wird zu online, nie ein Fehler, nie ein Verbindungsabbruch. since und afk werden angenommen und ignoriert.
  • Es gibt keine Antwort. Erfolg ist still, genau wie bei Discord.
  • Höchstens eine Änderung pro 5 Sekunden je Verbindung. Updates innerhalb des Fensters werden nicht abgelehnt, sondern überschreiben einander; angewendet wird der jeweils neueste Wert.
  • Nur schreibend: dein Bot empfängt niemals PRESENCE_UPDATE, unabhängig von den Intents. GUILD_PRESENCES (1 << 8) wird akzeptiert, liefert aber nichts, es existiert nur, damit Discord-kompatible Bibliotheken es anfordern können, ohne zu scheitern.
  • Dein Status wird automatisch alle 60 Sekunden erneuert und nach einer internen Neuverbindung wiederhergestellt, einmal gesetzt, bleibt er.

Verbindungsabbruch: RESUME (op 6)

Das READY enthält eine session_id und eine resume_gateway_url. Merke dir beides und zusätzlich die höchste empfangene Sequenznummer s aus den Dispatch-Frames. Reißt die Verbindung ab, öffnest du einen neuen WebSocket zur resume_gateway_url und schickst als erstes Frame statt eines IDENTIFY:

ws.send(JSON.stringify({ op: 6, d: {   // RESUME
  token: "ssbot_…​",
  session_id: "…aus dem READY…",
  seq: lastSeq                            // höchstes empfangenes s
}}));

Gelingt der Resume, spielt das Gateway alle verpassten Events in Reihenfolge nach und schließt mit einem RESUMED-Dispatch ab. Ein zweites READY kommt nicht. Das Fenster dafür beträgt 90 Sekunden nach dem Abbruch, und der Puffer lebt im Speicher der Gateway-Instanz: Nach einem Neustart des Gateways (z. B. einem Deploy) ist jede Session weg.

Ist die Session nicht mehr fortsetzbar (Fenster abgelaufen, unbekannte session_id, zu alte seq, Gateway-Neustart, fehlerhaftes RESUME-Frame), antwortet das Gateway mit op 9 Invalid Session (d: false). Dann gilt: neue Verbindung, frisches IDENTIFY, weiter wie beim ersten Start. Discord-Bibliotheken machen genau das automatisch. Das Token wird bei jedem Resume vollständig neu geprüft, ein widerrufenes Token bekommt Close-Code 4004 und niemals ein Replay.

Close-Codes

CodeBedeutungWas tun
4004Authentifizierung fehlgeschlagen, Token ungültig oder widerrufen (auch mitten in der Session)Nicht automatisch neu verbinden, erst das Token prüfen
4008Rate-Limit: zu viele IDENTIFYs oder zu viele gleichzeitige SessionsWarten, dann erneut verbinden
4009Session-Timeout: Heartbeat blieb aus (getrennt bei 1,5× dem angesagten Intervall)Neu verbinden, ein RESUME innerhalb des 90-Sekunden-Fensters funktioniert

Alle anderen Abbrüche (Netz weg, Gateway-Shutdown) kommen ohne eigenen Code als gewöhnlicher Socket-Close, auch dann zuerst RESUME versuchen und bei Invalid Session neu identifizieren.

Rate-Limits

Limits sind pro Bot und pro Server gebündelt. Jede Antwort trägt die Zähler; ein 429 nennt die Wartezeit.

HeaderBedeutung
X-RateLimit-LimitAnfragen pro Fenster
X-RateLimit-Remainingim aktuellen Fenster noch frei
X-RateLimit-Reset-AfterSekunden bis zum Zurücksetzen
Retry-Afterbei 429: so lange warten

Ein 429-Body ist Discord-förmig: { "message": "…", "retry_after": 7.457, "global": false }. Respektiere Retry-After, verbreitete Bibliotheken tun das automatisch.

Fehler

Fehler kommen als JSON mit sprechendem Code, kompatibel zu Discords { message, code }-Schema.

HTTPBedeutung
401Token fehlt, ungültig oder widerrufen
403Bot fehlt die Berechtigung (Rolle/Rechte)
404Ressource nicht gefunden, oder außerhalb der Reichweite des Bots (bewusst ununterscheidbar, damit keine fremden IDs erraten werden)
400Eingabe ungültig (z. B. zu lang, Null-Byte, falsches Feld)
429Rate-Limit, siehe Retry-After

Bibliotheken portieren

Weil die API dem verbreiteten Bot-Standard folgt, laufen bestehende Bibliotheken oft mit drei Anpassungen:

  • Basis-URL auf https://api.speakspeak.net/api/v1 und das Gateway auf wss://api.speakspeak.net/api/v1/gateway setzen.
  • ID-Format: SpeakSpeak nutzt UUIDs statt 64-Bit-Snowflakes, Code, der IDs als Zahl behandelt, muss sie als String führen.
  • Berechtigungs-Bits gegen SpeakSpeaks Rechte-Modell prüfen.

Die vollständige API-Referenz – alle Endpunkte, Objekte, Fehlercodes, Rate-Limits und Gateway-Intents – findest du unter speakspeak.net/api. Porting-Fragen (etwa zu einzelnen Berechtigungs-Bits) beantworten wir direkt: contact@speakspeak.net.

Öffentliche Beta. Die Bot-API ist verfügbar und wird erweitert. Fragen, Feedback oder ein Bibliotheks-Wunsch? Schreib an contact@speakspeak.net.