Complete API Reference for the Nimaora Platform
RESTful API + WebSocket Events
The Nimaora API is a RESTful API that supports:
- JSON request/response format
- Session-based authentication via headers
- Real-time updates via WebSocket
- Rate limiting per endpoint
Authentication is session-based using the X-Session-Id header.
Content-Type: application/json
Accept: application/json
X-Session-Id: your-unique-session-id- Client generates a unique session ID
- Client joins a battle with username + session ID
- Server validates username uniqueness per battle
- Session ID is used for all subsequent requests
- Heartbeat keeps session alive
| Environment | URL |
|---|---|
| Development | http://localhost:8000/api |
| Staging | https://api.staging.nimaora.dwin.codes |
| Production | https://api.nimaora.dwin.codes |
| Endpoint Category | Limit | Window |
|---|---|---|
| Join/Leave | 10 requests | 1 minute |
| Answer Submission | 30 requests | 1 minute |
| Attack | 20 requests | 1 minute |
| Leaderboard | 60 requests | 1 minute |
| Heartbeat | 60 requests | 1 minute |
Response headers include rate limit info:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1699999999Check API health status.
Request:
GET /api/healthResponse:
{
"status": "ok",
"timestamp": "2024-12-05T10:30:00.000Z",
"services": {
"database": "ok",
"redis": "ok",
"queue": "ok"
}
}List all active battles.
Request:
GET /api/battlesResponse:
{
"success": true,
"data": [
{
"id": 1,
"name": "Fall Programming Competition",
"description": "Nimaora Programming Competition - Fall 2024",
"starts_at": "2024-12-01T10:00:00.000Z",
"ends_at": "2024-12-08T10:00:00.000Z",
"is_active": true,
"questions_per_participant": 15,
"participant_count": 1250
}
]
}Get battle details.
Request:
GET /api/battles/1Response:
{
"success": true,
"data": {
"id": 1,
"name": "Fall Programming Competition",
"description": "Nimaora Programming Competition - Fall 2024",
"starts_at": "2024-12-01T10:00:00.000Z",
"ends_at": "2024-12-08T10:00:00.000Z",
"is_active": true,
"questions_per_participant": 15,
"participant_count": 1250,
"status": "running"
}
}Join a battle with a username.
Request:
POST /api/battles/1/join
Content-Type: application/json
X-Session-Id: abc123-unique-session
{
"username": "player_one"
}Success Response (200):
{
"success": true,
"data": {
"id": 12345,
"username": "player_one",
"points": 0,
"shields": 0,
"arrows": 0,
"questions": [
{
"id": 1,
"title": "Sum of Numbers",
"description": "Write a program that calculates the sum of numbers from 1 to n.",
"points": 10,
"solved": null
},
{
"id": 5,
"title": "Fibonacci Sequence",
"description": "Write a program that prints the first n terms of the Fibonacci sequence.",
"points": 25,
"solved": null
}
]
}
}Error Response (409 - Username Taken):
{
"success": false,
"message": "Another person is already competing with this username",
"error_code": "USERNAME_IN_USE"
}Leave the current battle.
Request:
POST /api/battles/1/leave
X-Session-Id: abc123-unique-sessionResponse:
{
"success": true,
"message": "Successfully left the battle"
}Keep session alive and get current stats.
Request:
POST /api/battles/1/heartbeat
X-Session-Id: abc123-unique-sessionResponse:
{
"success": true,
"data": {
"points": 45,
"shields": 2,
"arrows": 3,
"rank": 15,
"online_participants": 1250
}
}Get all questions assigned to the participant.
Request:
GET /api/battles/1/questions
X-Session-Id: abc123-unique-sessionResponse:
{
"success": true,
"data": [
{
"id": 1,
"title": "Sum of Numbers",
"description": "Write a program that calculates the sum of numbers from 1 to n.",
"points": 10,
"difficulty": "easy",
"solved": null
},
{
"id": 5,
"title": "Fibonacci Sequence",
"description": "Write a program that prints the first n terms of the Fibonacci sequence.",
"points": 25,
"difficulty": "medium",
"solved": true
},
{
"id": 12,
"title": "Dijkstra's Algorithm",
"description": "Implement Dijkstra's algorithm for finding the shortest path.",
"points": 50,
"difficulty": "hard",
"solved": false
}
]
}Get a specific question.
Request:
GET /api/battles/1/questions/5
X-Session-Id: abc123-unique-sessionResponse:
{
"success": true,
"data": {
"id": 5,
"title": "Fibonacci Sequence",
"description": "Write a program that prints the first n terms of the Fibonacci sequence. The sequence is: 0, 1, 1, 2, 3, 5, 8, 13, ...",
"points": 25,
"difficulty": "medium",
"solved": null
}
}Submit an answer for a question.
Request:
POST /api/battles/1/questions/5/submit
Content-Type: application/json
X-Session-Id: abc123-unique-session
{
"is_correct": true
}Success Response (200):
{
"success": true,
"data": {
"is_correct": true,
"points_earned": 25,
"total_points": 70,
"needs_reward_selection": true
}
}Error Response (400 - Already Answered):
{
"success": false,
"message": "You have already answered this question",
"error_code": "ALREADY_ANSWERED"
}Select reward after correct answer (arrow or shield).
Request:
POST /api/battles/1/reward
Content-Type: application/json
X-Session-Id: abc123-unique-session
{
"reward_type": "arrow"
}Response:
{
"success": true,
"data": {
"arrows": 4,
"shields": 2
}
}Attack another participant.
Request:
POST /api/battles/1/attack
Content-Type: application/json
X-Session-Id: abc123-unique-session
{
"target_username": "player_two"
}Success Response (200 - Attack Hit):
{
"success": true,
"data": {
"blocked": false,
"points_deducted": 1,
"attacker_arrows": 3,
"target_new_points": 44
}
}Success Response (200 - Attack Blocked):
{
"success": true,
"data": {
"blocked": true,
"points_deducted": 0,
"attacker_arrows": 3,
"message": "Target blocked the attack with a shield"
}
}Error Response (400 - No Arrows):
{
"success": false,
"message": "You have no arrows to attack",
"error_code": "NO_ARROWS"
}Error Response (400 - Invalid Target):
{
"success": false,
"message": "This player has no points to lose",
"error_code": "INVALID_TARGET"
}Get the battle leaderboard.
Request:
GET /api/battles/1/leaderboard?limit=100&offset=0
X-Session-Id: abc123-unique-sessionQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| limit | integer | 100 | Number of entries to return |
| offset | integer | 0 | Offset for pagination |
Response:
{
"success": true,
"data": {
"leaderboard": [
{
"rank": 1,
"username": "top_coder",
"points": 450,
"shields": 5,
"arrows": 2,
"is_online": true
},
{
"rank": 2,
"username": "algo_master",
"points": 425,
"shields": 3,
"arrows": 4,
"is_online": true
},
{
"rank": 3,
"username": "player_one",
"points": 380,
"shields": 2,
"arrows": 3,
"is_online": true
}
],
"total_participants": 1250,
"my_rank": 3,
"updated_at": "2024-12-05T10:30:00.000Z"
}
}Get current user's position and surrounding ranks.
Request:
GET /api/battles/1/leaderboard/me
X-Session-Id: abc123-unique-sessionResponse:
{
"success": true,
"data": {
"my_rank": 15,
"my_points": 120,
"surrounding": [
{ "rank": 13, "username": "user_13", "points": 125 },
{ "rank": 14, "username": "user_14", "points": 122 },
{ "rank": 15, "username": "player_one", "points": 120, "is_me": true },
{ "rank": 16, "username": "user_16", "points": 118 },
{ "rank": 17, "username": "user_17", "points": 115 }
]
}
}Connect to WebSocket server using Laravel Echo:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
const echo = new Echo({
broadcaster: 'reverb',
key: process.env.NEXT_PUBLIC_REVERB_APP_KEY,
wsHost: process.env.NEXT_PUBLIC_REVERB_HOST,
wsPort: process.env.NEXT_PUBLIC_REVERB_PORT,
wssPort: process.env.NEXT_PUBLIC_REVERB_PORT,
forceTLS: true,
enabledTransports: ['ws', 'wss'],
});| Channel | Type | Description |
|---|---|---|
battle.{id} |
Public | Leaderboard updates |
presence-battle.{id} |
Presence | Online participants |
private-participant.{id} |
Private | Attack notifications |
Broadcast when leaderboard changes.
Channel: battle.{id}
Payload:
{
"event": "leaderboard.updated",
"data": {
"leaderboard": [
{
"rank": 1,
"username": "top_coder",
"points": 450,
"shields": 5,
"arrows": 2,
"is_online": true
}
],
"updated_at": "2024-12-05T10:30:00.000Z"
}
}Sent to a specific participant when they are attacked.
Channel: private-participant.{id}
Payload:
{
"event": "attack.received",
"data": {
"attacker": "enemy_player",
"blocked": false,
"points_lost": 1,
"timestamp": "2024-12-05T10:30:00.000Z"
}
}Broadcast when participant stats change.
Channel: battle.{id}
Payload:
{
"event": "participant.updated",
"data": {
"username": "player_one",
"points": 125,
"shields": 3,
"arrows": 2,
"rank": 14
}
}// Subscribe to leaderboard updates
echo.channel(`battle.${battleId}`)
.listen('.leaderboard.updated', (data) => {
console.log('Leaderboard updated:', data.leaderboard);
});
// Subscribe to attack notifications
echo.private(`participant.${participantId}`)
.listen('.attack.received', (data) => {
console.log('You were attacked by:', data.attacker);
if (data.blocked) {
console.log('Attack blocked by shield!');
} else {
console.log('You lost', data.points_lost, 'points');
}
});
// Subscribe to presence channel
echo.join(`presence-battle.${battleId}`)
.here((users) => {
console.log('Online users:', users);
})
.joining((user) => {
console.log('User joined:', user);
})
.leaving((user) => {
console.log('User left:', user);
});{
"success": false,
"message": "Human-readable error message",
"error_code": "MACHINE_READABLE_CODE",
"errors": {
"field_name": ["Validation error message"]
}
}| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 422 | Validation Error |
| 429 | Too Many Requests |
| 500 | Server Error |
| Code | Description |
|---|---|
USERNAME_IN_USE |
Another session is using this username |
BATTLE_NOT_ACTIVE |
Battle is not currently active |
NOT_PARTICIPANT |
User is not a participant in this battle |
ALREADY_ANSWERED |
Question has already been answered |
INVALID_REWARD |
Invalid reward type |
NO_ARROWS |
No arrows available to attack |
INVALID_TARGET |
Target cannot be attacked |
SELF_ATTACK |
Cannot attack yourself |
RATE_LIMITED |
Too many requests |
const sessionId = generateUUID();
const headers = {
'Content-Type': 'application/json',
'X-Session-Id': sessionId,
};
// 1. Join battle
const joinResponse = await fetch('/api/battles/1/join', {
method: 'POST',
headers,
body: JSON.stringify({ username: 'my_username' }),
});
const { data: participant } = await joinResponse.json();
// 2. Answer a question
const answerResponse = await fetch('/api/battles/1/questions/5/submit', {
method: 'POST',
headers,
body: JSON.stringify({ is_correct: true }),
});
const { data: result } = await answerResponse.json();
// 3. Select reward
if (result.needs_reward_selection) {
await fetch('/api/battles/1/reward', {
method: 'POST',
headers,
body: JSON.stringify({ reward_type: 'arrow' }),
});
}
// 4. Attack another player
const attackResponse = await fetch('/api/battles/1/attack', {
method: 'POST',
headers,
body: JSON.stringify({ target_username: 'other_player' }),
});
// 5. Keep session alive (call every 30 seconds)
setInterval(async () => {
await fetch('/api/battles/1/heartbeat', {
method: 'POST',
headers,
});
}, 30000);# Join battle
curl -X POST https://api.nimaora.dwin.codes/api/battles/1/join \
-H "Content-Type: application/json" \
-H "X-Session-Id: my-session-123" \
-d '{"username": "test_user"}'
# Submit answer
curl -X POST https://api.nimaora.dwin.codes/api/battles/1/questions/5/submit \
-H "Content-Type: application/json" \
-H "X-Session-Id: my-session-123" \
-d '{"is_correct": true}'
# Get leaderboard
curl -X GET "https://api.nimaora.dwin.codes/api/battles/1/leaderboard?limit=50" \
-H "X-Session-Id: my-session-123"
# Attack
curl -X POST https://api.nimaora.dwin.codes/api/battles/1/attack \
-H "Content-Type: application/json" \
-H "X-Session-Id: my-session-123" \
-d '{"target_username": "enemy"}'RESTful API | WebSocket Events | Real-time Updates