Entwickler

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-Basishttps://nite-portal.com/api/v1
Gatewaywss://nite-portal.com/gateway
FormatJSON in UTF-8, Zeiten als Unix-Sekunden, IDs als Zeichenketten
AnmeldungAuthorization: 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}

contentPflicht. Text der Nachricht, 1 bis 2000 Zeichen, Markdown wie im Chat
usernameOptional. Anzeigename für diese Nachricht, höchstens 32 Zeichen. Ohne Angabe gilt der Name des Webhooks.
Shell
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"}'
Lua · FiveM
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' })
JavaScript
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

  1. 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.
  2. 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.
  3. Projekt anlegen. Du brauchst Node.js 18 oder neuer. Dann:
Shell
mkdir mein-bot && cd mein-bot
npm init -y
npm pkg set type=module
npm install ws
  1. Nachrichteninhalt freischalten. Damit dein Bot den Text von !ping lesen darf, unter Entwickler → Intents und Befehle den Schalter Nachrichteninhalt einschalten. Ohne Freigabe lehnt das Gateway die Anmeldung mit Code 4014 ab. Nur mit Slash-Befehlen brauchst du ihn nicht.
  2. Code schreiben. Speichere das Beispiel als bot.js. Es meldet sich am Gateway an, verbindet sich bei Abbrüchen neu, antwortet auf !ping und legt den Slash-Befehl /hallo an.
JavaScript · Node.js · bot.js
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()
  1. Starten. Bot-Token und Anwendungs-ID (steht unter Entwickler bei deiner Anwendung) mitgeben:
Shell
# 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
  1. 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.
  2. 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.
  3. Testen und beobachten. Schreib !ping in 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.

ErlaubtNachrichten 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.
DirektnachrichtenNur an Personen, mit denen der Bot einen Server teilt und die dort Direktnachrichten zulassen. Wer den Bot blockiert, bekommt nichts mehr.
GesperrtFreundschaftsanfragen, Gruppen, Server erstellen oder über Einladungen beitreten, Shop, Käufe, Boosts, Konto-, Sitzungs- und Datenschutz-Einstellungen, KI-Funktionen wie Übersetzen und Zusammenfassen.
Limits600 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.permissions mit, und über default_member_permissions blendest 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).

Shell
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 · HELLOKommt nach dem Verbinden, enthält heartbeat_interval in Millisekunden.
op 2 · IDENTIFYInnerhalb von 15 Sekunden senden: { "token": "Bot TOKEN", "intents": 33281 }
op 1 · HEARTBEATIm angegebenen Abstand senden, NITE antwortet mit op 11.
op 0 · DISPATCHEreignisse mit Name t, Daten d und Zähler s. Zuerst READY (mit session_id), dann GUILD_CREATE für jeden Server.
op 6 · RESUMENach 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 SESSIONFortsetzen 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.

1GUILDS · Server, Kanäle, Rollen
2 *GUILD_MEMBERS · Beitritte, Austritte, Änderungen
4GUILD_MODERATION · Sperren
8GUILD_EXPRESSIONS · Emojis, Sticker, Sounds
128GUILD_VOICE_STATES · Sprachkanäle
256 *GUILD_PRESENCES · Online-Status
512GUILD_MESSAGES · Nachrichten auf Servern
1024GUILD_MESSAGE_REACTIONS
2048GUILD_MESSAGE_TYPING
4096DIRECT_MESSAGES
32768 *MESSAGE_CONTENT · Ohne diesen Intent ist der Text fremder Nachrichten leer, ausser dein Bot wird erwähnt.
65536GUILD_SCHEDULED_EVENTS
JavaScript · Node.js
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

4004Token ungültig, neu erstellt oder Bot gesperrt
4009IDENTIFY nicht innerhalb von 15 Sekunden gesendet
4013Ungültige Intents
4014Privilegierte 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“.

  1. Über das Gateway beitreten: { "op": 5, "d": { "channel_id": "KANAL_ID" } }. Bots, die das Discord-Format senden (op 4 mit guild_id, channel_id, self_mute, self_deaf), gehen genauso.
  2. NITE antwortet mit VOICE_SERVER_UPDATE: ip, port, ssrc, payload_type und srtp.key (Suite AES_CM_128_HMAC_SHA1_80). Klappt es nicht, kommt VOICE_REJECTED mit error.
  3. Opus mit 48 kHz in Stereo an diese Adresse schicken, zum Beispiel mit ffmpeg.
  4. Verlassen: { "op": 5, "d": { "channel_id": null } }.
Shell · ffmpeg
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"
JavaScript · Node.js
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_unavailableDer Medienserver läuft nicht. Bots brauchen ihn, direkte Verbindungen gehen für Bots nicht.
missing_permissionsRecht „Verbinden“ oder „Sprechen“ fehlt, der Bot ist stummgeschaltet, oder auf einer Bühne ist er nicht als Sprecher eingetragen.
channel_fullPersonenlimit des Kanals erreicht.
not_voiceKein 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
POSTgleiche 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.

Shell
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.

JSON · Befehl
{
  "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.

JavaScript
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.

JavaScript
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 4Sofort antworten. In data: content (bis 2000 Zeichen), optional embeds und flags: 64 für eine Antwort, die nur die aufrufende Person sieht.
type 5Später antworten: zeigt „denkt nach …“. Danach die Antwort mit PATCH /webhooks/{app_id}/{token}/messages/@original setzen.
JavaScript
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 1Zeile. Bis zu 5 Zeilen pro Nachricht; jede Zeile hat bis zu 5 Buttons oder genau ein Auswahlmenü.
type 2Button: 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 3Auswahlmenü: custom_id, options (1 bis 25, je label, value, optional description, emoji, default), placeholder, min_values, max_values.
JavaScript
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 4Neue 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 6Nur bestätigen. Die Nachricht mit dem Button änderst du danach mit PATCH /webhooks/{app_id}/{token}/messages/@original.
type 7Die Nachricht mit dem Button sofort ändern: content, embeds, components (zum Beispiel Button deaktivieren).
type 9Formular ö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).
JavaScript
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.

  1. 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.
  2. Die Person auf /api/v1/oauth2/authorize schicken, mit zufälligem state.
  3. Nach der Zustimmung leitet NITE mit code und state zurück. Stimmt state nicht mit deinem Wert überein, brich ab.
  4. Den Code auf deinem Server gegen Tokens tauschen und damit /oauth2/users/@me abfragen.
GEThttps://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

identifyID, Benutzername, Anzeigename, Profilbild, Banner, Sprache, Konto erstellt am. Immer dabei.
emailE-Mail-Adresse und ob sie bestätigt ist (verified)
guildsListe 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

server.js
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.

JavaScript (Browser)
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.

HTML
<!-- 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

CodeGilt 10 Minuten und nur einmal. Wird ein Code ein zweites Mal eingelöst, sperrt NITE alle damit ausgestellten Tokens.
ZugriffstokenBeginnt mit nite_at_, gilt 1 Stunde und nur für die /oauth2-Adressen, nie für die übrige Schnittstelle.
Refresh-TokenBeginnt 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.
WeiterleitungMuss exakt einer eingetragenen Adresse entsprechen, auch der Schrägstrich am Ende. Erlaubt sind https://, http://localhost und eigene App-Schemas, keine Adressen mit #.
TrennenPersonen 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.

  1. In NITE in den Servereinstellungen → FiveM auf Schlüssel erstellen klicken.
  2. nite_script herunterladen und den Ordner nite_script in deinen resources-Ordner legen.
  3. Die Zeilen unten in die server.cfg eintragen und den Server neu starten. Immer set verwenden, nie setr oder sets, sonst sehen Spieler den Schlüssel.
server.cfg
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_script

ESX 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 → SpielWer 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 → NITEWer 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
Shell
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.

400Eingabe ungültig, etwa invalid_input oder content_too_long
401Token oder Schlüssel fehlt oder ist ungültig
403Fehlende Rechte, etwa missing_permissions oder role_hierarchy. Bei Bots auch bot_not_allowed (Schnittstelle für Bots gesperrt) oder dms_closed.
404Nicht gefunden, etwa unknown_channel oder unknown_webhook
429Zu 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.