REST API endpoints documentation.
Stage Flow Tools supports three admin authentication variants:
- Browser login with username and password, which creates the
admin_tokenHTTP-only cookie. Authorization: Bearer <token>on admin APIs.?token=<token>on protected/adminpages, which verifies the token and then establishes the normal admin cookie for the browser session.
Login as administrator.
This endpoint is for browser-style admin login with username and password. It sets the session cookie used by the admin UI.
Request:
{
"username": "string",
"password": "string"
}Response:
Sets admin_token HTTP-only cookie and returns success.
Logout the administrator. Clears the admin_token cookie.
Response:
{
"success": true
}Verify authentication token (admin only).
Accepted auth inputs:
- Cookie:
admin_token(set automatically by login) - Or
Authorization: Bearer <token>
Authorization: Bearer <token> accepts two admin auth modes:
- A JWT created by
POST /api/auth/login - The exact static token configured in
NUXT_ADMIN_TOKENfor software-to-software admin access
Response:
{
"valid": true,
"user": {
/* decoded JWT payload or static-token admin payload */
}
}Static token example:
curl \
-H "Authorization: Bearer $NUXT_ADMIN_TOKEN" \
http://localhost:3000/api/auth/verifyTokenized admin page example:
http://localhost:3000/admin/results?token=<token>&core&visibility=show
Use ?token= only when software needs to open a protected admin page in a browser or iframe. The route middleware forwards that token to /api/auth/verify, and successful verification sets the normal admin_token cookie for subsequent requests.
Query tokens in URLs can leak through browser history, server logs, and referrer headers. Use this flow only for initial bootstrap, then remove the token from the URL immediately after /api/auth/verify establishes the normal admin_token cookie.
Load the authenticated Drizzle Studio shell used by /admin/database.
Load Drizzle Studio static assets through the authenticated proxy.
Get a high-level quiz overview for presenter software (admin only).
This endpoint is meant for low-frequency use, such as a one-time fetch from presenter slides when a session starts.
Headers:
- Cookie:
admin_token - Or
Authorization: Bearer <token>
Response:
{
"totalQuestions": 3,
"questions": [
{
"id": "string",
"key": "string",
"question_text": {
"en": "string"
}
}
]
}Get detailed presenter state for the active question (admin only).
This endpoint is meant for polling, such as once per second from presenter slides.
totalUsers means active WebSocket connections at request time.
Headers:
- Cookie:
admin_token - Or
Authorization: Bearer <token>
Response:
{
"hasActiveQuestion": true,
"totalUsers": 42,
"receivedAnswers": 15,
"receivedAnswersPercent": 36,
"currentQuestion": {
"id": "string",
"key": "string",
"index": 2,
"totalQuestions": 5,
"question_text": {
"en": "string"
},
"note": {
"en": "string"
},
"is_active": true,
"is_locked": false,
"createdAt": "2026-05-03T12:00:00.000Z",
"answer_options": [
{
"text": {
"en": "string"
},
"emoji": "🔥",
"count": 10,
"percent": 67
}
]
}
}Response (no active question):
{
"hasActiveQuestion": false,
"totalUsers": 42,
"receivedAnswers": 0,
"receivedAnswersPercent": 0,
"currentQuestion": null
}Internal admin-only Drizzle Studio RPC compatibility endpoint used by the embedded frame. Treat this as internal transport, not a public integration API.
Get all questions (admin only).
Response: Array of question objects.
Get the currently active question (public). Returns a simplified version without emojis, admin notes, key, and alreadyPublished.
Response (active question):
{
"id": "string",
"question_text": { "en": "string", "de": "string" },
"answer_options": [{ "text": { "en": "string", "de": "string" } }],
"is_active": true,
"is_locked": false,
"createdAt": "ISO 8601"
}Response (no active question):
{
"message": "No active question"
}Create new question (admin only).
English answer_options[].text.en values must be unique. Matching is case-insensitive.
Request:
{
"key": "string (optional, unique identifier)",
"question_text": { "en": "string", "de": "string (optional)" },
"answer_options": [
{
"text": { "en": "string", "de": "string (optional)" },
"emoji": "string (optional)"
}
],
"note": { "en": "string (optional)" }
}Update an existing unpublished and inactive question (admin only).
Editable fields are key, question_text, answer_options, and note.
Active questions and already-published questions return 409.
English answer_options[].text.en values must be unique. Matching is case-insensitive.
Request:
{
"questionId": "string",
"key": "string (optional, unique identifier)",
"question_text": { "en": "string", "de": "string (optional)" },
"answer_options": [
{
"text": { "en": "string", "de": "string (optional)" },
"emoji": "string (optional)"
}
],
"note": { "en": "string (optional)" }
}Response: Updated question object.
Publish question as active by key (admin only). Clears existing answers and broadcasts to all WebSocket clients.
Request:
{
"key": "string"
}Publish the next unpublished question in creation order (admin only). Finds the earliest question where alreadyPublished is false, publishes it, and broadcasts to all WebSocket clients.
Request: No body required.
Response: The published question object, or 404 if no unpublished questions remain.
Deactivate the currently active question (admin only). Clears answers and broadcasts null as the new question.
Request: No body required.
Response:
{
"success": true,
"message": "Active question unpublished."
}Toggle answer lock on active question (admin only). Broadcasts lock status via WebSocket.
Request:
{
"questionId": "string"
}Submit or update a user answer.
Request:
{
"user_id": "string",
"user_nickname": "string",
"selected_answer": { "en": "string" }
}Response:
{
"success": true
}Retract a user's answer.
Request:
{
"user_id": "string",
"question_id": "string"
}Response:
{
"success": true
}Clear all submitted answers for current active question (admin only).
This endpoint derives target question from current active question. Request body is empty.
Response:
{
"success": true,
"questionId": "string"
}Error cases:
404- no active question
Submit an emoji reaction. Broadcasts to all clients on the emojis WebSocket channel. Enforces per-user cooldown.
Request:
{
"emoji": "string (single emoji)",
"user_id": "string"
}Get current question results (admin only).
Response:
{
"question": {
/* question object */
},
"results": {
"Option A": { "count": 10, "emoji": "optional" },
"Option B": { "count": 5 }
},
"totalVotes": 15,
"totalConnections": 42
}Pick a random user who voted for a specific option (admin only). Sends a winner-selected WebSocket event to the chosen user.
Request:
{
"questionId": "string",
"option": "string"
}Response: 204 No Content on success. Returns 404 if no answers or no users found for the option. Returns 503 if the winner is not currently connected.
Get aggregated player leaderboard across all published questions (admin only). A correct answer is any option with the ⭐ emoji.
Response:
{
"leaderboard": [
{
"rank": 1,
"userId": "string",
"nickname": "string",
"correctAnswers": 5
}
],
"totalQuestionsWithCorrectAnswers": 10
}Get active WebSocket connections (admin only).
Response: Array of connection objects with id and url.
{
"statusCode": 400,
"statusMessage": "Error description"
}{
"statusCode": 401,
"statusMessage": "Unauthorized"
}{
"statusCode": 403,
"statusMessage": "Question is locked"
}{
"statusCode": 404,
"statusMessage": "No active question"
}{
"statusCode": 429,
"statusMessage": "You are sending emojis too fast. Please wait a moment."
}