REST API Reference
General conventions
Base URL
http://localhost:3000 (development)
https://[your-domain] (production)
All endpoints are under the /api/ prefix.
Response format
Success:
{ "success": true, "data": { ... } }
Error:
{ "error": "Explicit error message" }
HTTP status codes
| Code | Meaning |
|---|---|
200 |
Success (GET, PATCH, DELETE) |
201 |
Resource created (POST) |
400 |
Invalid request (missing fields, incorrect format) |
401 |
Not authenticated |
403 |
Access forbidden (resource belonging to another user) |
404 |
Resource not found |
429 |
Too many requests (rate limiting) |
500 |
Internal server error |
Authentication
Three authentication modes coexist depending on the endpoint:
| Type | Mechanism | Endpoints |
|---|---|---|
| Admin | auth-token cookie (JWT HS256, 7 days) |
/api/admin/*, /api/auth/* |
| Speaker | Code in the body (DB lookup, localStorage session) | /api/speaker/login, /api/audio/upload |
| Public | None | /api/languages, /api/languages/[id]/content, /api/statistics/track, /api/quiz/*, /api/contact, /api/health/* |
Health
GET /api/health/live
Liveness probe — only verifies that the Node.js process is responding. Used by the Docker HEALTHCHECK. Does not depend on any external service (no DB connection).
curl http://localhost:3000/api/health/live
200 response:
{ "status": "ok" }
⚠️ This endpoint always responds
200as long as Next.js is running. It does NOT verify that PostgreSQL or MinIO are available.
Admin Authentication
POST /api/auth/login
Authenticates an administrator. Returns a auth-token JWT cookie valid for 7 days.
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@racines.app","password":"password123"}' \
-c cookies.txt
Body:
{
"email": "admin@racines.app",
"password": "password123"
}
200 response:
{
"success": true,
"message": "Authentication successful",
"admin": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "admin@racines.app"
}
}
The auth-token cookie is set in the response's Set-Cookie headers (HttpOnly, Secure, SameSite=Lax).
Errors:
| Case | Code | Message |
|---|---|---|
| Missing fields | 400 | "Email and password required" |
| Invalid email | 400 | "Invalid email" |
| Password < 8 characters | 400 | "Password must be at least 8 characters" |
| Incorrect credentials | 401 | "Incorrect email or password" |
Security note: Whether the email doesn't exist or the password is wrong, the response is identical (no information leak about whether the account exists).
GET /api/auth/login
Checks whether the admin session cookie is valid. Used by the frontend to detect an expired session.
curl http://localhost:3000/api/auth/login \
-b cookies.txt
Response if authenticated:
{
"authenticated": true,
"admin": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "admin@racines.app"
}
}
Response if not authenticated:
{ "authenticated": false }
POST /api/auth/logout
Invalidates the admin session cookie (cookie set to immediate expiration).
curl -X POST http://localhost:3000/api/auth/logout \
-b cookies.txt
200 response:
{ "success": true }
Speaker Authentication
POST /api/speaker/login
Verifies a speaker's access code. The session is managed client-side in localStorage (no server cookie).
curl -X POST http://localhost:3000/api/speaker/login \
-H "Content-Type: application/json" \
-d '{"accessCode":"RACINES-ABCDE-2025"}'
Body:
{
"accessCode": "RACINES-ABCDE-2025"
}
The code is normalized to uppercase server-side before the database lookup.
200 response:
{
"success": true,
"speaker": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Amadou Diallo",
"access_code": "RACINES-ABCDE-2025",
"language_id": "language-uuid",
"language_code": "pul",
"language_name": "Pular"
}
}
Errors:
| Case | Code | Message |
|---|---|---|
| Missing code | 400 | "Access code required" |
| Invalid code or inactive speaker | 401 | "Invalid access code or inactive speaker" |
Languages
GET /api/languages
Retrieves the list of active languages. Can be filtered by code.
# All active languages
curl http://localhost:3000/api/languages
# Filter by code
curl "http://localhost:3000/api/languages?code=pul"
Query params:
| Param | Type | Description |
|---|---|---|
| code | string | Optional — filter by language code (pul, wol, etc.) |
200 response:
{
"success": true,
"languages": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Pular",
"code": "pul",
"access_code": "RACINES-XXXXX-2025",
"is_active": true,
"display_order": 1,
"primary_color": "#D97706",
"icon_url": "https://[MINIO_PUBLIC_URL]/audios/icons/pul.png",
"created_at": "2025-01-15T10:00:00Z"
}
]
}
Note:
speaker_access_codeandspeaker_nameare NOT returned by this public endpoint (admin-only information).
POST /api/languages
Admin auth required.
Creates a new language with its first speaker.
curl -X POST http://localhost:3000/api/languages \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"name": "Wolof",
"code": "wol",
"access_code": "RACINES-WOLOF-2025",
"speaker_access_code": "RACINES-SPK-WOL-2025",
"speaker_name": "Fatou Diop",
"primary_color": "#2563EB",
"is_active": false,
"display_order": 2
}'
Body:
{
"name": "Wolof",
"code": "wol",
"access_code": "RACINES-WOLOF-2025",
"speaker_access_code": "RACINES-SPK-WOL-2025",
"speaker_name": "Fatou Diop",
"primary_color": "#2563EB",
"is_active": false,
"display_order": 2
}
201 response:
{
"success": true,
"data": {
"id": "new-uuid",
"name": "Wolof",
"code": "wol",
"access_code": "RACINES-WOLOF-2025",
"is_active": false,
"display_order": 2,
"primary_color": "#2563EB",
"created_at": "2026-01-15T10:00:00Z"
}
}
PATCH /api/languages/[id]
Admin auth required.
Updates one or more properties of a language. All fields are optional.
curl -X PATCH http://localhost:3000/api/languages/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"is_active": true}'
Body (all fields optional):
{
"name": "Pular (Fuuta Tooro)",
"is_active": true,
"primary_color": "#B45309",
"display_order": 1,
"speaker_name": "Amadou Diallo",
"speaker_access_code": "RACINES-SPK-NEW-2025",
"icon_url": "https://[MINIO]/audios/icons/pul-v2.png"
}
200 response:
{
"success": true,
"data": { /* complete updated language */ }
}
DELETE /api/languages/[id]
Admin auth required.
Deletes a language and all its dependencies in cascade (cards, items, speakers, statistics). Irreversible action. Audio files in MinIO are not deleted.
curl -X DELETE http://localhost:3000/api/languages/550e8400-e29b-41d4-a716-446655440000 \
-b cookies.txt
200 response:
{
"success": true,
"message": "Language deleted successfully"
}
POST /api/languages/verify-code
Verifies that a player access code matches a given language. Used when unlocking a language.
curl -X POST http://localhost:3000/api/languages/verify-code \
-H "Content-Type: application/json" \
-d '{"languageId":"language-uuid","code":"RACINES-XXXXX-2025"}'
Body:
{
"languageId": "550e8400-e29b-41d4-a716-446655440000",
"code": "RACINES-XXXXX-2025"
}
The code is normalized to uppercase server-side before comparison.
200 response:
{ "valid": true }
or
{ "valid": false }
This endpoint never reveals whether a language exists or not. An unknown language simply returns
{ "valid": false }.
GET /api/languages/[id]/content
Retrieves the full content of a language for offline download (IndexedDB). Returns the language, all its cards, and all its items in a single request.
curl "http://localhost:3000/api/languages/550e8400-e29b-41d4-a716-446655440000/content"
200 response:
{
"success": true,
"language": {
"id": "uuid",
"name": "Pular",
"code": "pul",
"primary_color": "#D97706",
"icon_url": "..."
},
"cards": [
{
"id": "card-uuid",
"language_id": "uuid",
"type": "word",
"card_number": 1,
"theme": "Family"
}
],
"items": [
{
"id": "item-uuid",
"card_id": "card-uuid",
"position": 1,
"original_text": "Baaba",
"translation": "Father",
"audio_url": "https://[CDN]/audios/pul/item-uuid/audio.mp3",
"lot_number": null,
"item_type": null
}
]
}
This endpoint is used exclusively for offline download. Content is stored in IndexedDB v3.
Cards
POST /api/cards
Admin auth required.
Creates a new card for a language.
curl -X POST http://localhost:3000/api/cards \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"language_id": "language-uuid",
"type": "word",
"card_number": 81,
"theme": "Animals"
}'
Body:
{
"language_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "word",
"card_number": 81,
"theme": "Animals"
}
type must be "word" or "phrase".
201 response:
{
"success": true,
"data": {
"id": "new-card-uuid",
"language_id": "language-uuid",
"type": "word",
"card_number": 81,
"theme": "Animals",
"created_at": "2026-01-15T10:00:00Z"
}
}
POST /api/cards/import
Admin auth required.
Bulk import via Excel file (.xlsx). Creates cards and their items from a structured file.
curl -X POST http://localhost:3000/api/cards/import \
-b cookies.txt \
-F "file=@content_pular.xlsx" \
-F "languageId=language-uuid" \
-F "type=word"
Form data:
| Field | Type | Description |
|---|---|---|
| file | File | .xlsx file |
| languageId | string | Language UUID |
| type | string | "word" or "phrase" |
For the exact Excel file structure, see 06-import-excel.md.
200 response:
{
"success": true,
"cardsCreated": 80,
"itemsCreated": 480
}
Items
POST /api/items
Admin auth required.
Creates one or more items. Supports individual or bulk insertion.
Individual insertion:
curl -X POST http://localhost:3000/api/items \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"card_id": "card-uuid",
"position": 1,
"original_text": "Baaba",
"translation": "Father"
}'
Bulk insertion:
curl -X POST http://localhost:3000/api/items \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"items": [
{"card_id": "card-uuid", "position": 1, "original_text": "Baaba", "translation": "Father"},
{"card_id": "card-uuid", "position": 2, "original_text": "Yaye", "translation": "Mother"},
{"card_id": "card-uuid", "position": 3, "original_text": "Tonton", "translation": "Uncle"},
{"card_id": "card-uuid", "position": 4, "original_text": "Tata", "translation": "Aunt"},
{"card_id": "card-uuid", "position": 5, "original_text": "Aan", "translation": "Elder sister"},
{"card_id": "card-uuid", "position": 6, "original_text": "Kaaño", "translation": "Younger brother"}
]
}'
Body fields:
| Field | Type | Required | Description |
|---|---|---|---|
| card_id | string | ✅ | Parent card UUID |
| position | integer | ✅ | 1–6 for words, 1–4 for phrases |
| original_text | string | ✅ | Text in the African language |
| translation | string | ✅ | French translation |
| audio_url | string | ❌ | URL to MinIO (null by default) |
| lot_number | integer | ❌ | 1 or 2 (phrases only) |
| item_type | string | ❌ | "question" or "answer" (phrases only) |
201 response (bulk insertion):
{
"success": true,
"data": [
{
"id": "item-uuid-1",
"card_id": "card-uuid",
"position": 1,
"original_text": "Baaba",
"translation": "Father",
"audio_url": null,
"lot_number": null,
"item_type": null,
"created_at": "2026-01-15T10:00:00Z"
}
]
}
PATCH /api/items/[id]
Admin auth required.
Updates an item. All fields are optional.
curl -X PATCH http://localhost:3000/api/items/item-uuid \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"original_text": "Baaba (correction)", "translation": "Father (formal)"}'
Body:
{
"original_text": "Baaba (correction)",
"translation": "Father (formal)",
"audio_url": "https://[CDN]/audios/pul/item-uuid/audio.mp3"
}
200 response:
{
"success": true,
"data": { /* updated item */ }
}
Speakers
POST /api/speakers
Admin auth required.
Creates a new speaker for a language.
curl -X POST http://localhost:3000/api/speakers \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"language_id": "language-uuid",
"name": "Mariama Baldé",
"access_code": "RACINES-SPK-MRB-2025",
"is_active": true
}'
Body:
{
"language_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Mariama Baldé",
"access_code": "RACINES-SPK-MRB-2025",
"is_active": true
}
201 response:
{
"success": true,
"data": {
"id": "speaker-uuid",
"language_id": "language-uuid",
"name": "Mariama Baldé",
"access_code": "RACINES-SPK-MRB-2025",
"is_active": true,
"created_at": "2026-01-15T10:00:00Z"
}
}
Audio
POST /api/audio/upload
Speaker auth required (code passed in headers or session cookie).
Uploads an audio file for an item. The file is automatically converted to mono 96kbps MP3 via FFmpeg, then stored in MinIO.
curl -X POST http://localhost:3000/api/audio/upload \
-H "X-Speaker-Code: RACINES-SPK-MRB-2025" \
-F "audio=@recording.wav" \
-F "itemId=item-uuid"
Form data:
| Field | Type | Description |
|---|---|---|
| audio | File | Audio file (MP3, WAV, WebM, OGG, M4A, FLAC) |
| itemId | string | Target item UUID |
Limits: - Maximum size: 50 MB - Accepted formats: MP3, WAV, WebM, OGG, M4A, FLAC
Automatic FFmpeg processing: - Conversion to MP3 - 96 kbps bitrate - Mono (1 channel) - 44,100 Hz sample rate - Volume normalization (EBU R128 loudnorm)
Generated MinIO key: [language_code]/[item_uuid]/audio.mp3
200 response:
{
"success": true,
"audioUrl": "https://[MINIO_PUBLIC_URL]/audios/pul/item-uuid/audio.mp3"
}
The items.audio_url column is automatically updated in the database.
Errors:
| Case | Code | Message |
|---|---|---|
| Speaker not authenticated | 401 | "Authentication required" |
| File too large | 400 | "File too large (max 50 MB)" |
| Unsupported format | 400 | "Unsupported audio format" |
| Item not found | 404 | "Item not found" |
GET /api/audio/serve/[[...path]]
MinIO proxy — used only in local development or when AUDIO_CDN_URL is absent.
curl "http://localhost:3000/api/audio/serve/pul/item-uuid/audio.mp3"
Path traversal protection: Paths containing .. or starting with / are rejected with a 400 error.
200 response:
Binary audio/mpeg stream from MinIO.
In production, audio is served directly from the MinIO CDN (
AUDIO_CDN_URL) without going through this proxy. See 05-audio-minio.md.
Statistics
POST /api/statistics/track
Records a user event. Creates or increments a daily entry in the statistics table (upsert by date).
curl -X POST http://localhost:3000/api/statistics/track \
-H "Content-Type: application/json" \
-d '{
"eventType": "play_audio",
"languageId": "language-uuid",
"cardId": "card-uuid",
"itemId": "item-uuid"
}'
Body:
{
"eventType": "play_audio",
"languageId": "550e8400-e29b-41d4-a716-446655440000",
"cardId": "card-uuid",
"itemId": "item-uuid"
}
Event types:
eventType |
Trigger | cardId |
itemId |
|---|---|---|---|
unlock_language |
Language unlocked | null | null |
view_card |
Card displayed | required | null |
play_audio |
Audio played | required | required |
200 response:
{
"success": true,
"count": 42
}
count = cumulative number of occurrences for that day.
Offline behavior: Events are queued in IndexedDB (
statistics-queue) when the network is unavailable, then automatically synchronized upon reconnection.
GET /api/statistics/by-language
Admin auth required.
Retrieves aggregated statistics by language for the dashboard.
curl "http://localhost:3000/api/statistics/by-language?from=2026-01-01&to=2026-01-31" \
-b cookies.txt
Query params:
| Param | Type | Description |
|---|---|---|
| from | string | Format YYYY-MM-DD (optional) |
| to | string | Format YYYY-MM-DD (optional) |
200 response:
[
{
"language_id": "uuid",
"language_name": "Pular",
"language_code": "pul",
"unlock_count": 245,
"view_count": 1830,
"audio_count": 956
}
]
GET /api/statistics/top-cards
Admin auth required.
Retrieves the most viewed cards.
curl "http://localhost:3000/api/statistics/top-cards?languageId=uuid&limit=10" \
-b cookies.txt
Query params:
| Param | Type | Description |
|---|---|---|
| languageId | string | Optional — filter by language |
| limit | integer | Number of results (default: 10) |
| from | string | Format YYYY-MM-DD (optional) |
| to | string | Format YYYY-MM-DD (optional) |
200 response:
[
{
"card_id": "uuid",
"card_number": 5,
"type": "word",
"theme": "Family",
"language_name": "Pular",
"view_count": 312,
"audio_count": 187
}
]
Quiz (MCQ Revision)
POST /api/quiz/start
Starts a quiz session. Creates an entry in quiz_sessions with completed_at = null.
curl -X POST http://localhost:3000/api/quiz/start \
-H "Content-Type: application/json" \
-d '{
"languageId": "language-uuid",
"categorySlug": "family",
"categoryName": "Family",
"totalQuestions": 15
}'
Body:
{
"languageId": "550e8400-e29b-41d4-a716-446655440000",
"categorySlug": "family",
"categoryName": "Family",
"totalQuestions": 15
}
totalQuestions: 15, 18, or 20 depending on the number of items available in the category (calculated by getQuestionCount()).
201 response:
{
"sessionId": "session-uuid"
}
POST /api/quiz/complete
Records the final score of a quiz session. Updates quiz_sessions.score and completed_at.
curl -X POST http://localhost:3000/api/quiz/complete \
-H "Content-Type: application/json" \
-d '{"sessionId": "session-uuid", "score": 12}'
Body:
{
"sessionId": "session-uuid",
"score": 12
}
score = number of correct answers (between 0 and totalQuestions).
200 response:
{ "success": true }
Incomplete sessions (
completed_at = null) are considered abandoned and are not counted in statistics.
Contact
POST /api/contact
Sends a contact message. Persists the message in the database, sends an email to admins via SMTP, and sends a confirmation to the sender.
curl -X POST http://localhost:3000/api/contact \
-H "Content-Type: application/json" \
-d '{
"name": "John Smith",
"email": "john@example.com",
"subject": "Question about the app",
"message": "Hello, I have a question..."
}'
Body:
{
"name": "John Smith",
"email": "john@example.com",
"subject": "Question about the app",
"message": "Hello, I have a question...",
"_t": 5000
}
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Sender's name |
| email | string | ✅ | Sender's email |
| subject | string | ✅ | Message subject |
| message | string | ✅ | Message body |
| website | string | ❌ | Honeypot — must remain empty |
| _t | number | ❌ | Typing start timestamp (anti-spam timing) |
Anti-spam protection (4 layers):
- Honeypot (
website): If filled, silent200response (fake success) - Minimum timing (
_t): Silent rejection if submitted in < 4 seconds - Rate limiting: Maximum 3 messages per hour per IP (via
X-Forwarded-For) - Disposable domains: Rejection of emails from temporary domains
200 response:
{ "success": true, "message": "Emails sent successfully" }
Errors:
| Case | Code | Message |
|---|---|---|
| Missing fields | 400 | "All required fields must be filled" |
| Disposable email domain | 400 | "Please use a permanent email address." |
| Rate limit reached | 429 | "Too many messages sent. Please try again in an hour." |
| SMTP error | 500 | "Error sending emails" |
Contact Messages (Admin)
GET /api/admin/contact
Admin auth required.
Retrieves contact messages with pagination and filters.
curl "http://localhost:3000/api/admin/contact?page=1&status=unread&search=smith" \
-b cookies.txt
Query params:
| Param | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1, 20 messages/page) |
| status | string | Filter: unread, read, replied, archived |
| search | string | Search in name, email, subject |
200 response:
{
"success": true,
"messages": [
{
"id": "uuid",
"name": "John Smith",
"email": "john@example.com",
"subject": "Question about the app",
"message": "Hello...",
"status": "unread",
"admin_notes": null,
"created_at": "2026-01-15T10:00:00Z"
}
],
"total": 47,
"page": 1,
"totalPages": 3
}
PATCH /api/admin/contact/[id]
Admin auth required.
Updates the status or notes of a message.
curl -X PATCH http://localhost:3000/api/admin/contact/message-uuid \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"status": "replied", "admin_notes": "Replied by email on 01/15"}'
Body:
{
"status": "replied",
"admin_notes": "Replied by email on 01/15"
}
status values: unread, read, replied, archived
200 response:
{ "success": true }
DELETE /api/admin/contact/[id]
Admin auth required.
Permanently deletes a contact message.
curl -X DELETE http://localhost:3000/api/admin/contact/message-uuid \
-b cookies.txt
200 response:
{ "success": true }
Admin Statistics
GET /api/admin/statistics
Admin auth required.
Counts the number of rows in the statistics table according to filters.
curl "http://localhost:3000/api/admin/statistics?languageId=uuid&from=2026-01-01&to=2026-01-31" \
-b cookies.txt
Query params:
| Param | Type | Description |
|---|---|---|
| languageId | string | Optional — filter by language |
| from | string | Format YYYY-MM-DD |
| to | string | Format YYYY-MM-DD |
200 response:
{ "count": 12450 }
DELETE /api/admin/statistics
Admin auth required.
Deletes statistics rows according to filters. Irreversible action.
# Delete stats for a language over a period
curl -X DELETE \
"http://localhost:3000/api/admin/statistics?languageId=uuid&from=2025-01-01&to=2025-12-31" \
-b cookies.txt
# Full purge (required header)
curl -X DELETE http://localhost:3000/api/admin/statistics \
-H "x-delete-all-confirmed: true" \
-b cookies.txt
Query params: same as GET /api/admin/statistics
Full purge protection: Without any filter, the header x-delete-all-confirmed: true is required to prevent accidental deletions.
200 response:
{ "success": true, "deleted": 12450 }
GET /api/admin/quiz/stats
Admin auth required.
Retrieves quiz session statistics (completed and abandoned).
curl "http://localhost:3000/api/admin/quiz/stats?languageId=uuid&from=2026-01-01" \
-b cookies.txt
200 response:
{
"total": 320,
"completed": 215,
"abandoned": 105,
"averageScore": 11.3,
"completionRate": 67.2,
"byCategory": [
{
"categoryName": "Family",
"sessions": 45,
"avgScore": 12.1
}
]
}
Client errors
POST /api/client-errors
Receives client-side JavaScript errors for server-side logging. Used by AudioPlayer to report audio playback errors.
curl -X POST http://localhost:3000/api/client-errors \
-H "Content-Type: application/json" \
-d '{"request":"HEAD https://cdn/audio.mp3","response":"403 Forbidden"}'
Body:
{
"request": "HEAD https://[CDN]/audios/pul/item-uuid/audio.mp3",
"response": "403 Forbidden"
}
200 response:
{ "ok": true }
Errors are logged server-side without personally identifiable data.
Summary table
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/health/live |
GET | None | Liveness probe |
/api/auth/login |
POST | — | Admin login |
/api/auth/login |
GET | Admin | Check session |
/api/auth/logout |
POST | Admin | Logout |
/api/speaker/login |
POST | — | Speaker login |
/api/languages |
GET | None | List active languages |
/api/languages |
POST | Admin | Create a language |
/api/languages/[id] |
PATCH | Admin | Update a language |
/api/languages/[id] |
DELETE | Admin | Delete (cascade) |
/api/languages/verify-code |
POST | None | Verify player code |
/api/languages/[id]/content |
GET | None | Full content (offline) |
/api/cards |
POST | Admin | Create a card |
/api/cards/import |
POST | Admin | Excel import |
/api/items |
POST | Admin | Create one or more items |
/api/items/[id] |
PATCH | Admin | Update an item |
/api/speakers |
POST | Admin | Create a speaker |
/api/audio/upload |
POST | Speaker | Upload audio |
/api/audio/serve/[[...path]] |
GET | None | MinIO proxy (dev) |
/api/statistics/track |
POST | None | Record an event |
/api/statistics/by-language |
GET | Admin | Stats by language |
/api/statistics/top-cards |
GET | Admin | Top cards |
/api/quiz/start |
POST | None | Start a quiz |
/api/quiz/complete |
POST | None | Complete a quiz |
/api/contact |
POST | None | Send a message |
/api/admin/contact |
GET | Admin | List contact messages |
/api/admin/contact/[id] |
PATCH | Admin | Update message |
/api/admin/contact/[id] |
DELETE | Admin | Delete message |
/api/admin/statistics |
GET | Admin | Count stats |
/api/admin/statistics |
DELETE | Admin | Delete stats |
/api/admin/quiz/stats |
GET | Admin | Quiz stats |
/api/client-errors |
POST | None | Client JS errors |