Eine Zeile Code, und dein Spielserver spricht mit
NITE hat eine offene Schnittstelle ohne Freigabeverfahren. Für Benachrichtigungen reicht ein Webhook, für alles andere baust du einen Bot mit eigenem Token. Spielserver verbindest du mit einem Serverschlüssel.
Grundlagen
| REST-Basis | https://nite-portal.com/api/v1 |
|---|---|
| Gateway | wss://nite-portal.com/gateway |
| Format | JSON in UTF-8, Zeiten als Unix-Sekunden, IDs als Zeichenketten |
| Anmeldung | Authorization: Bot TOKEN für Bots, Authorization: Server SCHLÜSSEL für Spielserver. Webhooks tragen ihr Geheimnis in der Adresse. |
Tokens, Schlüssel und Webhook-Adressen sind wie Passwörter: Sie gehören nicht in öffentlichen Code, auf Bildschirmfotos oder in Client-Skripte. Wer einen Schlüssel neu erstellt, macht den alten sofort ungültig.
Webhooks
Ein Webhook schreibt Nachrichten in genau einen Kanal. Anlegen kannst du ihn in den Servereinstellungen → Integrationen, dafür brauchst du das Recht Kanäle verwalten. Pro Kanal sind zehn Webhooks möglich.
Nachricht senden
POST /webhooks/{webhook_id}/{webhook_token}
| content | Pflicht. Text der Nachricht, 1 bis 2000 Zeichen, Markdown wie im Chat |
|---|---|
| username | Optional. Anzeigename für diese Nachricht, höchstens 32 Zeichen. Ohne Angabe gilt der Name des Webhooks. |
curl -X POST "https://nite-portal.com/api/v1/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "Server online — 842 Spieler", "username": "Retro Roleplay"}'PerformHttpRequest('https://nite-portal.com/api/v1/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN', function() end, 'POST',
json.encode({ content = 'Server online — ' .. #GetPlayers() .. ' Spieler', username = 'Retro Roleplay' }),
{ ['Content-Type'] = 'application/json' })await fetch('https://nite-portal.com/api/v1/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: 'Neues Update ist live', username: 'Changelog' })
})Die Antwort ist die erstellte Nachricht. Ein Webhook darf 30 Nachrichten pro Minute senden.
Bots
Ein Bot ist ein eigenes Konto mit Token. Er liest über das Gateway mit, was auf seinen Servern passiert, und handelt über die Schnittstelle: Nachrichten schreiben, Rollen vergeben, Slash-Befehle beantworten. Was er auf einem Server darf, bestimmen dort wie bei allen anderen die Rollen.
Schritt für Schritt zum ersten Bot
- Anwendung anlegen. In NITE unter Einstellungen → Entwickler einen Namen eingeben und Bot erstellen wählen. Name und Bild des Bots kannst du dort jederzeit ändern.
- Token sichern. Der Token erscheint nur einmal. Leg ihn in eine Umgebungsvariable oder eine
.env-Datei, nie direkt in den Code, nie in ein öffentliches Repository und nie in eine Website oder App, die andere laden. - Projekt anlegen. Du brauchst Node.js 18 oder neuer. Dann:
mkdir mein-bot && cd mein-bot
npm init -y
npm pkg set type=module
npm install ws- Nachrichteninhalt freischalten. Damit dein Bot den Text von
!pinglesen darf, unter Entwickler → Intents und Befehle den Schalter Nachrichteninhalt einschalten. Ohne Freigabe lehnt das Gateway die Anmeldung mit Code4014ab. Nur mit Slash-Befehlen brauchst du ihn nicht. - Code schreiben. Speichere das Beispiel als
bot.js. Es meldet sich am Gateway an, verbindet sich bei Abbrüchen neu, antwortet auf!pingund legt den Slash-Befehl/halloan.
import WebSocket from 'ws'
const TOKEN = process.env.NITE_BOT_TOKEN
const API = 'https://nite-portal.com/api/v1'
const INTENTS = (1 << 0) | (1 << 9) | (1 << 15) // Server, Nachrichten, Nachrichtentext
const api = (path, body, method = 'POST') => fetch(API + path, {
method,
headers: { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined
}).then(r => r.ok ? r.json().catch(() => null) : r.json().then(e => Promise.reject(e)))
function connect() {
const ws = new WebSocket('wss://nite-portal.com/gateway')
let beat
ws.on('message', async raw => {
const { op, t, d } = JSON.parse(raw)
if (op === 10) {
beat = setInterval(() => ws.send(JSON.stringify({ op: 1, d: null })), d.heartbeat_interval)
ws.send(JSON.stringify({ op: 2, d: { token: `Bot ${TOKEN}`, intents: INTENTS } }))
}
if (op !== 0) return
if (t === 'READY') console.log('Online als', d.user.username)
if (t === 'MESSAGE_CREATE' && d.content === '!ping') {
await api(`/channels/${d.channel_id}/messages`, { content: 'Pong!', reply_to: d.id })
}
if (t === 'INTERACTION_CREATE' && d.type === 2 && d.data.name === 'hallo') {
const name = d.member?.user.global_name ?? d.user?.global_name ?? 'du'
await fetch(`${API}/interactions/${d.id}/${d.token}/callback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 4, data: { content: `Hallo ${name}! 👋` } })
})
}
})
ws.on('close', code => {
clearInterval(beat)
if (code === 4004 || code === 4014) return console.error('Anmeldung abgelehnt, Token oder Intents prüfen:', code)
setTimeout(connect, 5000)
})
}
await api(`/applications/${process.env.NITE_APP_ID}/commands`, [
{ name: 'hallo', description: 'Der Bot sagt Hallo' }
], 'PUT')
connect()- Starten. Bot-Token und Anwendungs-ID (steht unter Entwickler bei deiner Anwendung) mitgeben:
# Windows (PowerShell)
$env:NITE_BOT_TOKEN="DEIN_TOKEN"; $env:NITE_APP_ID="DEINE_APP_ID"; node bot.js
# Linux / macOS
NITE_BOT_TOKEN=DEIN_TOKEN NITE_APP_ID=DEINE_APP_ID node bot.js- Auf den Server holen. In den Servereinstellungen → Integrationen die Bot-ID einfügen. Dafür brauchst du dort das Recht Server verwalten. Damit andere deinen Bot auf ihre Server holen können, schaltest du ihn unter Entwickler auf öffentlich.
- Rechte geben. Ein neuer Bot hat nur die Rechte von
@everyone. Leg eine Rolle an, gib ihr genau die Rechte, die der Bot braucht, und weise sie ihm zu. Mitglieder mit einer höheren Rolle kann er nicht entfernen, bannen oder stummschalten. - Testen und beobachten. Schreib
!pingin einen Kanal oder tipp/hallo. Unter Entwickler → Protokoll siehst du jede Verbindung, jeden Befehl und jeden Fehler deines Bots, Serverbesitzer sehen dasselbe für ihren Server unter Integrationen → Bot-Protokoll.
Was Bots dürfen
Bots bekommen nur die Teile der Schnittstelle, die sie für ihre Arbeit brauchen. Alles andere beantwortet NITE mit 403 und bot_not_allowed, und der Versuch steht im Protokoll.
| Erlaubt | Nachrichten lesen, schreiben, bearbeiten und löschen, Reaktionen, Anheften, Umfragen, Threads und Forenbeiträge, Kanäle, Rollen, Mitglieder, Banns, Emojis, Events, Einladungen erstellen, Dateien hochladen, Slash-Befehle, Webhooks des Kanals. Jeweils nur mit den Rechten, die der Bot auf dem Server hat. |
|---|---|
| Direktnachrichten | Nur an Personen, mit denen der Bot einen Server teilt und die dort Direktnachrichten zulassen. Wer den Bot blockiert, bekommt nichts mehr. |
| Gesperrt | Freundschaftsanfragen, Gruppen, Server erstellen oder über Einladungen beitreten, Shop, Käufe, Boosts, Konto-, Sitzungs- und Datenschutz-Einstellungen, KI-Funktionen wie Übersetzen und Zusammenfassen. |
| Limits | 600 Anfragen pro Minute und Bot, dazu die Limits der einzelnen Schnittstellen. Slash-Befehle: höchstens 100 pro Bereich, 200 Anlegen-Aufrufe pro Tag. |
Jeder Server hat ausserdem einen eigenen Server-Bot mit Name und Bild des Servers. Er begrüsst neue Tickets und verschickt Hinweise. Er hat keinen Token, lässt sich nicht anschreiben und nicht als Freund hinzufügen.
Sicherheit
- Token durchgesickert? Unter Entwickler sofort Token neu erstellen. Der alte ist ab diesem Moment ungültig, und alle offenen Gateway-Verbindungen damit werden getrennt (Code
4004). - Gib deinem Bot so wenig Rechte wie möglich. Administrator braucht fast kein Bot.
- Prüf in deinem Code, wer einen Befehl auslöst. NITE schickt bei jedem Aufruf
member.permissionsmit, und überdefault_member_permissionsblendest du Befehle für Leute ohne Recht ganz aus. - Verlass dich nie auf Text aus Nachrichten als Befehl für etwas Gefährliches. Slash-Befehle mit festen Optionen sind sicherer als
!befehl. - Bots, die gegen die Richtlinien verstossen, kann das NITE-Team sperren. Ein gesperrter Bot kann sich nicht mehr anmelden.
Nachricht als Bot senden
POST /channels/{kanal_id}/messages mit content und optional reply_to (ID der Nachricht, auf die geantwortet wird), embeds und components (Buttons & Formulare).
curl -X POST "https://nite-portal.com/api/v1/channels/KANAL_ID/messages" \
-H "Authorization: Bot BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "Hallo aus meinem Bot"}'Weitere häufige Aufrufe: GET /channels/{kanal_id}/messages?limit=50 liest den Verlauf, GET /gateway/bot liefert die Gateway-Adresse.
Gateway
Über das Gateway bekommt dein Bot Ereignisse in Echtzeit. Der Ablauf:
| op 10 · HELLO | Kommt nach dem Verbinden, enthält heartbeat_interval in Millisekunden. |
|---|---|
| op 2 · IDENTIFY | Innerhalb von 15 Sekunden senden: { "token": "Bot TOKEN", "intents": 33281 } |
| op 1 · HEARTBEAT | Im angegebenen Abstand senden, NITE antwortet mit op 11. |
| op 0 · DISPATCH | Ereignisse mit Name t, Daten d und Zähler s. Zuerst READY (mit session_id), dann GUILD_CREATE für jeden Server. |
| op 6 · RESUME | Nach einem Verbindungsabbruch innerhalb von 2 Minuten neu verbinden und statt IDENTIFY senden: { "token": "Bot TOKEN", "session_id": "…", "seq": letzter_s }. NITE liefert alle verpassten Ereignisse nach und meldet dann RESUMED. |
| op 9 · INVALID SESSION | Fortsetzen nicht möglich (Fenster abgelaufen oder Sitzung unbekannt): normal mit IDENTIFY anmelden. |
Intents
Mit Intents bestimmst du, welche Ereignisse ankommen. Die Werte werden addiert. Mit * markierte Intents musst du in den Entwickler-Einstellungen der Anwendung freischalten.
| 1 | GUILDS · Server, Kanäle, Rollen |
|---|---|
| 2 * | GUILD_MEMBERS · Beitritte, Austritte, Änderungen |
| 4 | GUILD_MODERATION · Sperren |
| 8 | GUILD_EXPRESSIONS · Emojis, Sticker, Sounds |
| 128 | GUILD_VOICE_STATES · Sprachkanäle |
| 256 * | GUILD_PRESENCES · Online-Status |
| 512 | GUILD_MESSAGES · Nachrichten auf Servern |
| 1024 | GUILD_MESSAGE_REACTIONS |
| 2048 | GUILD_MESSAGE_TYPING |
| 4096 | DIRECT_MESSAGES |
| 32768 * | MESSAGE_CONTENT · Ohne diesen Intent ist der Text fremder Nachrichten leer, ausser dein Bot wird erwähnt. |
| 65536 | GUILD_SCHEDULED_EVENTS |
import WebSocket from 'ws'
const TOKEN = process.env.NITE_BOT_TOKEN
const ws = new WebSocket('wss://nite-portal.com/gateway')
let beat
ws.on('message', raw => {
const { op, t, d } = JSON.parse(raw)
if (op === 10) {
beat = setInterval(() => ws.send(JSON.stringify({ op: 1, d: null })), d.heartbeat_interval)
ws.send(JSON.stringify({ op: 2, d: { token: `Bot ${TOKEN}`, intents: (1 << 0) | (1 << 9) | (1 << 15) } }))
}
if (op === 0 && t === 'READY') console.log('Angemeldet als', d.user.username)
if (op === 0 && t === 'MESSAGE_CREATE' && d.content === '!ping') {
fetch(`https://nite-portal.com/api/v1/channels/${d.channel_id}/messages`, {
method: 'POST',
headers: { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ content: 'Pong!', reply_to: d.id })
})
}
})
ws.on('close', code => { clearInterval(beat); console.log('Verbindung beendet', code) })Gründe für einen Verbindungsabbruch
| 4004 | Token ungültig, neu erstellt oder Bot gesperrt |
|---|---|
| 4009 | IDENTIFY nicht innerhalb von 15 Sekunden gesendet |
| 4013 | Ungültige Intents |
| 4014 | Privilegierte Intents nicht freigeschaltet |
Bots in Sprachkanälen
Bots können in Sprachkanälen sprechen, zum Beispiel für Musik, Ansagen oder Sounds. Der Bot schickt Opus-Audio über RTP an den NITE-Medienserver, verschlüsselt mit SRTP; alle im Kanal hören ihn wie eine Person. Zuhören können Bots nicht. Der Bot braucht im Kanal die Rechte „Verbinden“ und „Sprechen“.
- Über das Gateway beitreten:
{ "op": 5, "d": { "channel_id": "KANAL_ID" } }. Bots, die das Discord-Format senden (op 4mitguild_id,channel_id,self_mute,self_deaf), gehen genauso. - NITE antwortet mit
VOICE_SERVER_UPDATE:ip,port,ssrc,payload_typeundsrtp.key(SuiteAES_CM_128_HMAC_SHA1_80). Klappt es nicht, kommtVOICE_REJECTEDmiterror. - Opus mit 48 kHz in Stereo an diese Adresse schicken, zum Beispiel mit ffmpeg.
- Verlassen:
{ "op": 5, "d": { "channel_id": null } }.
ffmpeg -re -i musik.mp3 -vn -c:a libopus -b:a 128k -ac 2 -ar 48000 \
-ssrc SSRC -payload_type 101 -f rtp \
-srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params SCHLUESSEL \
"srtp://IP:PORT?rtcpport=PORT&pkt_size=1200"import { spawn } from 'node:child_process'
ws.send(JSON.stringify({ op: 5, d: { channel_id: SPRACHKANAL } }))
// im message-Handler des Gateways
if (op === 0 && t === 'VOICE_SERVER_UPDATE') {
spawn('ffmpeg', ['-re', '-i', 'musik.mp3', '-vn', '-c:a', 'libopus', '-b:a', '128k', '-ac', '2', '-ar', '48000',
'-ssrc', String(d.ssrc), '-payload_type', String(d.payload_type), '-f', 'rtp',
'-srtp_out_suite', d.srtp.suite, '-srtp_out_params', d.srtp.key,
`srtp://${d.ip}:${d.port}?rtcpport=${d.port}&pkt_size=1200`], { stdio: 'inherit' })
}Ob Pakete ankommen, zeigt GET /voice/bot-stats?channel_id=KANAL_ID (mit dem Bot-Token): connected, packets, bytes.
Jeder Beitritt erzeugt einen neuen Schlüssel und Port. Der Medienserver nimmt nur Pakete mit diesem Schlüssel an, und nur von der Adresse, die als Erste sendet. Bei einem Wechsel des Kanals einfach neu beitreten und ffmpeg neu starten.
| voice_server_unavailable | Der Medienserver läuft nicht. Bots brauchen ihn, direkte Verbindungen gehen für Bots nicht. |
|---|---|
| missing_permissions | Recht „Verbinden“ oder „Sprechen“ fehlt, der Bot ist stummgeschaltet, oder auf einer Bühne ist er nicht als Sprecher eingetragen. |
| channel_full | Personenlimit des Kanals erreicht. |
| not_voice | Kein Sprachkanal und keine Bühne. Bots sprechen nur auf Servern, nicht in Direktnachrichten. |
Slash-Befehle
Befehle erscheinen im Chat, sobald jemand / tippt. Globale Befehle gelten überall, wo der Bot ist, Server-Befehle nur auf einem Server.
| PUT | /applications/{app_id}/commands ersetzt alle globalen Befehle (höchstens 100) |
|---|---|
| PUT | /applications/{app_id}/guilds/{server_id}/commands für einen einzelnen Server |
| POST | gleiche Adresse, legt einen einzelnen Befehl an oder aktualisiert ihn |
Namen: klein geschrieben, ohne Leerzeichen. Beschreibung: 1 bis 100 Zeichen. Optionstypen: 1 Unterbefehl, 2 Gruppe, 3 Text, 4 Ganzzahl, 5 Ja/Nein, 6 Nutzer, 7 Kanal, 8 Rolle, 9 Erwähnbar, 10 Zahl.
curl -X PUT "https://nite-portal.com/api/v1/applications/APP_ID/commands" \
-H "Authorization: Bot BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '[{
"name": "wetter",
"description": "Zeigt das Wetter für eine Stadt",
"options": [{ "type": 3, "name": "stadt", "description": "Welche Stadt?", "required": true }]
}]'Vorschläge beim Tippen
Mit "autocomplete": true bei einer Text- oder Zahl-Option (Typ 3, 4 oder 10) fragt NITE deinen Bot, während jemand tippt. Feste choices und autocomplete zusammen gehen nicht.
{
"name": "wetter",
"description": "Wetter für eine Stadt",
"options": [{ "type": 3, "name": "stadt", "description": "Welche Stadt?", "required": true, "autocomplete": true }]
}Dein Bot bekommt dabei INTERACTION_CREATE mit type: 4. Das Feld, in dem gerade getippt wird, hat focused: true. Antworte innerhalb von 2,5 Sekunden mit Callback type: 8 und höchstens 25 Vorschlägen.
if (op === 0 && t === 'INTERACTION_CREATE' && d.type === 4) {
const getippt = d.data.options.find(o => o.focused).value.toLowerCase()
const staedte = ['Zürich', 'Bern', 'Basel', 'Luzern', 'Genf', 'Lugano']
await fetch(`https://nite-portal.com/api/v1/interactions/${d.id}/${d.token}/callback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 8, data: { choices: staedte
.filter(s => s.toLowerCase().startsWith(getippt))
.map(s => ({ name: s, value: s })) } })
})
}Auf einen Befehl antworten
Beim Aufruf kommt INTERACTION_CREATE über das Gateway. Dein Bot muss innerhalb von 3,5 Sekunden antworten, sonst sieht die Person eine Fehlermeldung.
if (op === 0 && t === 'INTERACTION_CREATE' && d.type === 2 && d.data.name === 'wetter') {
const stadt = d.data.options.find(o => o.name === 'stadt').value
await fetch(`https://nite-portal.com/api/v1/interactions/${d.id}/${d.token}/callback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 4, data: { content: `In ${stadt} sind es gerade 18 Grad.` } })
})
}| type 4 | Sofort antworten. In data: content (bis 2000 Zeichen), optional embeds und flags: 64 für eine Antwort, die nur die aufrufende Person sieht. |
|---|---|
| type 5 | Später antworten: zeigt „denkt nach …“. Danach die Antwort mit PATCH /webhooks/{app_id}/{token}/messages/@original setzen. |
await fetch(`https://nite-portal.com/api/v1/interactions/${d.id}/${d.token}/callback`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 5 })
})
const text = await langeBerechnung()
await fetch(`https://nite-portal.com/api/v1/webhooks/${d.application_id}/${d.token}/messages/@original`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: text })
})Mit POST /webhooks/{app_id}/{token} schickst du weitere Nachrichten hinterher. Ein Interaktions-Token gilt 15 Minuten.
Buttons, Auswahlmenüs und Formulare
Bots hängen an Nachrichten Buttons und Auswahlmenüs an und öffnen Formulare, so wie bei Discord. Komponenten gehen überall, wo ein Bot schreibt: POST /channels/{kanal_id}/messages, Antworten auf Interaktionen (Typ 4) und PATCH …/messages/@original. Dort sind jetzt auch embeds erlaubt. Eigene Nachrichten änderst du mit PATCH /channels/{kanal_id}/messages/{nachricht_id}, auch nur components.
| type 1 | Zeile. Bis zu 5 Zeilen pro Nachricht; jede Zeile hat bis zu 5 Buttons oder genau ein Auswahlmenü. |
|---|---|
| type 2 | Button: style 1 Hauptfarbe, 2 grau, 3 grün, 4 rot, 5 Link (mit url statt custom_id). Dazu label (bis 80 Zeichen), optional emoji und disabled. |
| type 3 | Auswahlmenü: custom_id, options (1 bis 25, je label, value, optional description, emoji, default), placeholder, min_values, max_values. |
await api(`/channels/${kanal}/messages`, {
content: 'Brauchst du Hilfe?',
components: [{ type: 1, components: [
{ type: 2, style: 1, label: 'Ticket öffnen', custom_id: 'ticket_open' },
{ type: 2, style: 5, label: 'Regeln', url: 'https://nite-portal.com/richtlinien' }
] }]
})Klick empfangen
Ein Klick kommt als INTERACTION_CREATE mit type: 3. In data stehen custom_id, component_type und bei Auswahlmenüs values; message ist die Nachricht mit dem Button. Antworte innerhalb von 3 Sekunden über POST /interactions/{id}/{token}/callback:
| type 4 | Neue Nachricht, wie bei Slash-Befehlen (mit flags: 64 nur für die klickende Person). |
|---|---|
| type 5 | „denkt nach …“, danach eine neue Nachricht mit PATCH …/@original. |
| type 6 | Nur bestätigen. Die Nachricht mit dem Button änderst du danach mit PATCH /webhooks/{app_id}/{token}/messages/@original. |
| type 7 | Die Nachricht mit dem Button sofort ändern: content, embeds, components (zum Beispiel Button deaktivieren). |
| type 9 | Formular öffnen (auch als Antwort auf einen Slash-Befehl): custom_id, title (bis 45 Zeichen) und bis zu 5 Zeilen mit je einem Textfeld (type: 4, custom_id, label, style 1 kurz oder 2 lang, optional min_length, max_length, required, value, placeholder). |
if (t === 'INTERACTION_CREATE' && d.type === 3 && d.data.custom_id === 'ticket_open') {
await callback(d, { type: 9, data: {
custom_id: 'ticket_form', title: 'Neues Ticket',
components: [{ type: 1, components: [
{ type: 4, custom_id: 'grund', label: 'Worum geht es?', style: 2, max_length: 1000 }
] }]
} })
}
if (t === 'INTERACTION_CREATE' && d.type === 5) {
const grund = d.data.components[0].components[0].value
await callback(d, { type: 4, data: { content: `Danke! Wir melden uns: ${grund}`, flags: 64 } })
}Ein abgeschicktes Formular kommt als INTERACTION_CREATE mit type: 5; in data.components stehen die Felder mit custom_id und value. Ein Formular bleibt 15 Minuten offen.
OAuth2 · Mit NITE anmelden
Mit OAuth2 melden sich Leute auf deiner Webseite oder in deinem Tool mit ihrem NITE-Konto an, so wie mit „Mit Discord anmelden“. Deine Anwendung bekommt nur, was die Person auf der Zustimmungsseite erlaubt, und sieht nie ein Passwort. NITE nutzt den Autorisierungscode-Ablauf mit PKCE (RFC 6749 und 7636); fertige OAuth-Bibliotheken funktionieren ohne Anpassung.
- In NITE unter Einstellungen → Entwickler → OAuth2 eine Anwendung erstellen und die Weiterleitungsadresse eintragen, zum Beispiel
https://deine-seite.ch/auth/nite/callback. Das Client-Secret wird nur einmal angezeigt. - Die Person auf
/api/v1/oauth2/authorizeschicken, mit zufälligemstate. - Nach der Zustimmung leitet NITE mit
codeundstatezurück. Stimmtstatenicht mit deinem Wert überein, brich ab. - Den Code auf deinem Server gegen Tokens tauschen und damit
/oauth2/users/@meabfragen.
| GET | https://nite-portal.com/api/v1/oauth2/authorize · client_id, response_type=code, redirect_uri, scope, state, bei PKCE code_challenge und code_challenge_method=S256 |
|---|---|
| POST | /oauth2/token · grant_type=authorization_code mit code, redirect_uri (und code_verifier) oder grant_type=refresh_token mit refresh_token. Client per HTTP-Basic oder client_id/client_secret im Körper, als Formular oder JSON |
| POST | /oauth2/token/revoke · token (Zugriffs- oder Refresh-Token) widerrufen |
| GET | /oauth2/users/@me · Konto der Person (identify, mit email auch E-Mail) |
| GET | /oauth2/users/@me/guilds · Server der Person mit owner und permissions (guilds) |
| GET | /oauth2/@me · Anwendung, erlaubte Scopes und Ablaufzeit des Tokens |
Die übrigen Adressen beginnen ebenfalls mit https://nite-portal.com/api/v1. Die Metadaten für OAuth-Bibliotheken liegen unter https://nite-portal.com/api/v1/.well-known/oauth-authorization-server.
Scopes
identify | ID, Benutzername, Anzeigename, Profilbild, Banner, Sprache, Konto erstellt am. Immer dabei. |
|---|---|
email | E-Mail-Adresse und ob sie bestätigt ist (verified) |
guilds | Liste der Server (höchstens 200) mit Name, Symbol, Besitzer und Rechten |
Nachrichten lesen oder im Namen der Person schreiben kann eine OAuth-Anwendung nicht. Dafür gibt es Bots.
Beispiel: Node.js mit Express
import express from 'express'
import session from 'express-session'
import { randomBytes } from 'node:crypto'
const NITE = 'https://nite-portal.com'
const CLIENT_ID = process.env.NITE_CLIENT_ID
const CLIENT_SECRET = process.env.NITE_CLIENT_SECRET
const REDIRECT = 'https://deine-seite.ch/auth/nite/callback'
const app = express()
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }))
app.get('/auth/nite', (req, res) => {
req.session.state = randomBytes(16).toString('hex')
const url = new URL(NITE + '/api/v1/oauth2/authorize')
url.search = new URLSearchParams({
client_id: CLIENT_ID, response_type: 'code', redirect_uri: REDIRECT,
scope: 'identify email', state: req.session.state
})
res.redirect(url)
})
app.get('/auth/nite/callback', async (req, res) => {
if (!req.query.code || req.query.state !== req.session.state) return res.status(400).send('Anmeldung abgebrochen')
delete req.session.state
const token = await fetch(NITE + '/api/v1/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + Buffer.from(CLIENT_ID + ':' + CLIENT_SECRET).toString('base64')
},
body: new URLSearchParams({ grant_type: 'authorization_code', code: req.query.code, redirect_uri: REDIRECT })
}).then(r => r.json())
if (!token.access_token) return res.status(400).send('Code ungültig')
const user = await fetch(NITE + '/api/v1/oauth2/users/@me', {
headers: { Authorization: 'Bearer ' + token.access_token }
}).then(r => r.json())
req.session.user = { id: user.id, name: user.global_name, avatar: user.avatar }
res.redirect('/')
})
app.listen(3000)Ohne eigenen Server: PKCE
Single-Page-Apps und Handy-Apps können kein Secret geheim halten. Hake beim Erstellen Öffentlicher Client an; dann gibt es kein Secret und PKCE ist Pflicht. Für Handy-Apps sind eigene Adressen wie com.firma.app:/callback erlaubt, für Entwicklung http://localhost.
const b64 = bytes => btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const verifier = b64(crypto.getRandomValues(new Uint8Array(48)))
const challenge = b64(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))))
sessionStorage.setItem('nite_pkce', verifier)
location.href = 'https://nite-portal.com/api/v1/oauth2/authorize?' + new URLSearchParams({
client_id: 'DEINE_CLIENT_ID', response_type: 'code', redirect_uri: location.origin + '/callback',
scope: 'identify', state: crypto.randomUUID(), code_challenge: challenge, code_challenge_method: 'S256'
})
// auf /callback: Code mit dem gemerkten verifier eintauschen
const tokens = await fetch('https://nite-portal.com/api/v1/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code', client_id: 'DEINE_CLIENT_ID', code: new URLSearchParams(location.search).get('code'),
redirect_uri: location.origin + '/callback', code_verifier: sessionStorage.getItem('nite_pkce')
})
}).then(r => r.json())Button-Vorlage
Ein fertiger Knopf im NITE-Stil zum Kopieren. Den Link /auth/nite auf deine eigene Anmelde-Adresse ändern und die Grösse an deine übrigen Buttons anpassen, das Logo kommt direkt von nite-portal.com.
<!-- Mit NITE anmelden: Link auf deine Anmelde-Adresse ändern. Grösse an deine anderen Buttons anpassen (siehe CSS unten). -->
<a class="nite-login" href="/auth/nite">
<img src="https://nite-portal.com/assets/mark.webp" alt="" width="32" height="32">
<span>Mit NITE anmelden</span>
</a>
<style>
/* Grösse an deine anderen Anmelde-Buttons anpassen: height, padding, font-size und Logo (img width/height) gemeinsam ändern */
.nite-login {
display: inline-flex; align-items: center; gap: 12px;
height: 50px; padding: 0 26px 0 10px; box-sizing: border-box;
border-radius: 999px; text-decoration: none; white-space: nowrap;
font: 600 16px/1 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color: #fff;
background: linear-gradient(180deg, #1B1529, #0F0B19);
box-shadow: inset 0 0 0 1px rgba(167, 139, 250, .28), 0 8px 24px -10px rgba(0, 0, 0, .7);
transition: box-shadow .15s ease, transform .15s ease;
}
.nite-login img { width: 32px; height: 32px; border-radius: 50%; box-shadow: 0 0 0 2px rgba(139, 92, 246, .45); }
.nite-login:hover { transform: translateY(-1px); box-shadow: inset 0 0 0 1px rgba(167, 139, 250, .6), 0 10px 28px -10px rgba(139, 92, 246, .55); }
.nite-login:focus-visible { outline: 2px solid #A78BFA; outline-offset: 3px; }
</style>Tokens und Sicherheit
| Code | Gilt 10 Minuten und nur einmal. Wird ein Code ein zweites Mal eingelöst, sperrt NITE alle damit ausgestellten Tokens. |
|---|---|
| Zugriffstoken | Beginnt mit nite_at_, gilt 1 Stunde und nur für die /oauth2-Adressen, nie für die übrige Schnittstelle. |
| Refresh-Token | Beginnt mit nite_rt_, gilt 30 Tage und wird bei jeder Nutzung ersetzt. Taucht ein schon ersetzter Refresh-Token wieder auf, wird die ganze Kette gesperrt. Mit scope kannst du beim Erneuern weniger Rechte anfragen, nie mehr. |
| Weiterleitung | Muss exakt einer eingetragenen Adresse entsprechen, auch der Schrägstrich am Ende. Erlaubt sind https://, http://localhost und eigene App-Schemas, keine Adressen mit #. |
| Trennen | Personen sehen alle Anwendungen unter Einstellungen → Verbundene Anwendungen und können den Zugriff jederzeit entziehen. Danach gelten alle Tokens sofort nicht mehr. |
Fehler folgen dem OAuth-Standard: invalid_client, invalid_grant, invalid_scope, unsupported_grant_type, invalid_token, insufficient_scope; lehnt die Person ab, kommt error=access_denied an deine Weiterleitungsadresse. Willst du deine Anwendung im App-Verzeichnis von NITE zeigen, reiche sie in der App unter Entdecken → Apps → App einreichen ein.
FiveM
Verbindet deinen Spielserver mit deinem NITE-Server: Live-Status mit Spielerzahl, Verknüpfung von Spieler- und NITE-Konto, auf Wunsch eine Whitelist mit Bewerbungen direkt in NITE und ein Rollen-Abgleich in beide Richtungen.
- In NITE in den Servereinstellungen → FiveM auf Schlüssel erstellen klicken.
- nite_script herunterladen und den Ordner
nite_scriptin deinenresources-Ordner legen. - Die Zeilen unten in die
server.cfgeintragen und den Server neu starten. Immersetverwenden, niesetrodersets, sonst sehen Spieler den Schlüssel.
set nite_script_api "https://nite-portal.com"
set nite_script_key "DEIN_SERVERSCHLUESSEL"
# Nur für den Rollen-Abgleich NITE-Rolle → ACE-Gruppe nötig
add_ace resource.nite_script command.add_principal allow
add_ace resource.nite_script command.remove_principal allow
ensure nite_scriptESX wird automatisch erkannt, wenn es_extended vor nite_script startet. Den Quellcode der Ressource kannst du hier ansehen.
Konto verknüpfen und Whitelist
Spieler tippen im Spiel /nite und geben den Code im NITE-Server unter FiveM ein; er gilt 10 Minuten. Ist die Whitelist an, kommen nur verknüpfte Mitglieder mit angenommener Bewerbung auf den Server, alle anderen sehen beim Verbinden, was ihnen fehlt. Ist NITE einmal nicht erreichbar, lässt die Ressource Spieler trotzdem herein.
Rollen-Abgleich
In den Servereinstellungen → FiveM → Rollen-Abgleich ordnest du NITE-Rollen zu. Eine Zuordnung kann eine oder beide Richtungen haben:
| NITE → Spiel | Wer die NITE-Rolle hat, bekommt im Spiel die ACE-Gruppe group.<name>, zum Beispiel Team-Rolle → group.admin. Wird die Rolle in NITE entfernt, verschwindet die Gruppe spätestens nach 30 Sekunden. |
|---|---|
| Spiel → NITE | Wer im Spiel einen ESX-Job ab einem Rang hat, bekommt die NITE-Rolle, zum Beispiel police ab Rang 2 → Rolle „LSPD“. Beim Jobwechsel wird sie wieder entfernt. Solche Rollen dürfen keine Admin- oder Moderationsrechte haben. |
Andere Ressourcen fragen das verknüpfte Konto mit exports.nite_script:GetNiteAccount(source) ab.
Schnittstelle für Spielserver
| POST | /fivem/heartbeat · alle 30 Sekunden: players, max, optional name, beim Start sync mit allen Spielern, danach joins, drops und jobs (license, job, grade). Spieler in sync und joins dürfen job und grade mitbringen. Antwort: groups, die ACE-Gruppen je license der verknüpften Spieler, die gerade online sind |
|---|---|
| POST | /fivem/connect · beim Verbinden: license, name, optional want_code. Antwort: allowed, linked, reason, code, nite, groups |
| GET | /fivem/players/{license} · verknüpftes NITE-Konto eines Spielers, mit groups |
curl "https://nite-portal.com/api/v1/fivem/players/license:0123456789abcdef0123456789abcdef01234567" \
-H "Authorization: Server DEIN_SERVERSCHLUESSEL"Als license gilt license: oder, wenn ein Konto keine hat, license2:. Mögliche Werte für reason: not_linked, not_member, banned, account_disabled, not_whitelisted, application_pending, application_denied.
Fehler & Limits
Fehler kommen immer als JSON mit einem festen Code, zum Beispiel { "error": "missing_permissions" }. Bei ungültigen Eingaben steht zusätzlich in field, welches Feld nicht passt.
| 400 | Eingabe ungültig, etwa invalid_input oder content_too_long |
|---|---|
| 401 | Token oder Schlüssel fehlt oder ist ungültig |
| 403 | Fehlende Rechte, etwa missing_permissions oder role_hierarchy. Bei Bots auch bot_not_allowed (Schnittstelle für Bots gesperrt) oder dms_closed. |
| 404 | Nicht gefunden, etwa unknown_channel oder unknown_webhook |
| 429 | Zu viele Anfragen. retry_after und der Kopf Retry-After sagen, wie viele Sekunden du warten musst. |
Jede Schnittstelle hat eigene Limits. Im Kopf jeder Antwort stehen X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset. Fragen oder etwas fehlt? Schreib uns in NITE unter Home → NITE HQ.