Zones API Reference
The Zones API provides endpoints for creating and managing personal areas for participants on boards. Student Zones enable isolated workspaces, exam mode with privacy controls, and group activities where each participant has their own drawing area. All zone operations require API key authentication.
Overview
Student Zones are personal rectangular areas on a board where only assigned participants can draw. This unique feature (not available in competing platforms like Miro) is designed specifically for EdTech use cases:
- Online Exams: Each student works in isolation without seeing others' work
- Practice Sessions: Individual workspaces while teacher monitors all progress
- Assessment: Easy comparison of student work side-by-side
Key Concepts
| Concept | Description |
|---|---|
| Zone | Frame with assignment to specific participant/user |
| Assignment | Participant or user ID who owns the zone |
| Zone Visibility | Controls who can see zone content: visible, hidden, host_only |
| Zone Version | Incremental counter for race condition protection |
| Permissions | Only zone owner and host can edit content inside |
Zone Visibility Modes
| Mode | Behavior | Use Case |
|---|---|---|
visible | Everyone sees all zones (read-only for non-owners) | Practice, teacher reviews work |
hidden | Participants see only their zone, host sees all | Exams, tests, independent work |
host_only | Only host sees zone (draft mode) | Preparing zones before assignment |
Permission Model
Zone Assignment:
├── Host: Can view and edit ALL zones
├── Assigned Participant/User: Can view and edit ONLY their zone
│ └── Cannot move/resize/delete the zone itself
└── Other Participants:
├── visible mode: Can view (read-only), cannot edit
└── hidden mode: Cannot see or editAuthentication
All Zones API endpoints require API key authentication:
curl -H "X-API-Key: your-api-key" \
https://api.boardapi.io/api/v1/boards/:boardUuid/zonesAPI keys are passed via the X-API-Key header. Get your API key from the Developer Dashboard.
Base URL
https://api.boardapi.io/api/v1For development:
http://localhost:4000/api/v1Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
POST | /boards/:boardUuid/frames | Create a new zone (frame with assignment) |
GET | /boards/:boardUuid/frames | List all zones for a board |
GET | /boards/:boardUuid/frames/:frameId | Get a specific zone |
PATCH | /boards/:boardUuid/frames/:frameId | Update zone properties |
DELETE | /boards/:boardUuid/frames/:frameId | Archive a zone |
POST | /boards/:boardUuid/frames/:frameId/assign | Assign participant to zone |
POST | /boards/:boardUuid/frames/:frameId/unassign | Remove participant assignment |
POST | /boards/:boardUuid/zones/bulk-assign | Assign multiple participants at once |
Note: Zones are implemented as Frames with additional assignment fields. Use the Frames endpoints with zone-specific parameters.
Create Zone
Creates a new zone and optionally assigns it to a participant.
Request
POST /boards/:boardUuid/frames
X-API-Key: your-api-key
Content-Type: application/json
{
"name": "Student 1 Zone",
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": "part-uuid-123",
"zone_visibility": "hidden",
"lock_mode": "soft"
}URL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Zone name (e.g., "Student 1", "Group A") |
bounds_x | number | No | X coordinate in pixels (default: 0) |
bounds_y | number | No | Y coordinate in pixels (default: 0) |
bounds_width | number | No | Width in pixels (default: 1200, min: 100) |
bounds_height | number | No | Height in pixels (default: 800, min: 100) |
assigned_participant_id | string | No | Participant UUID (session-based) |
assigned_user_id | string | No | User UUID (authenticated, priority over participant_id) |
zone_visibility | string | No | Privacy mode: visible, hidden, host_only (default: visible) |
lock_mode | string | No | Viewport lock: none, soft, hard (default: none) |
background_color | string | No | Hex color code (e.g., #FFFFFF) |
Important: Either
assigned_participant_idorassigned_user_idmust be provided to create a zone. If both are provided,assigned_user_idtakes priority.
Response
Status Code: 201 Created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"board_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Student 1 Zone",
"order": 0,
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"lock_mode": "soft",
"background_color": null,
"visibility": "active",
"assigned_participant_id": "part-uuid-123",
"assigned_user_id": null,
"zone_visibility": "hidden",
"zone_version": 1,
"created_at": "2025-11-26T10:00:00Z",
"updated_at": "2025-11-26T10:00:00Z"
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Zone UUID |
board_id | string | Parent board UUID |
name | string | Zone name |
bounds_x | number | X coordinate |
bounds_y | number | Y coordinate |
bounds_width | number | Width in pixels |
bounds_height | number | Height in pixels |
assigned_participant_id | string | Assigned participant UUID or null |
assigned_user_id | string | Assigned user UUID or null |
zone_visibility | string | Privacy mode |
zone_version | number | Version counter (increments on changes) |
lock_mode | string | Viewport control mode |
visibility | string | Frame state (active, hidden, archived) |
created_at | string | ISO 8601 creation timestamp |
updated_at | string | ISO 8601 update timestamp |
Example Requests
JavaScript/TypeScript:
// Create hidden zone for exam
const response = await fetch(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames',
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Student 1 Zone',
bounds_x: 0,
bounds_y: 0,
bounds_width: 1200,
bounds_height: 800,
assigned_participant_id: 'part-uuid-123',
zone_visibility: 'hidden',
lock_mode: 'hard' // Prevent navigation outside zone
})
}
);
const zone = await response.json();
console.log(`Created zone: ${zone.name} (v${zone.zone_version})`);cURL:
curl -X POST https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "Student 1 Zone",
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": "part-uuid-123",
"zone_visibility": "hidden",
"lock_mode": "hard"
}'PHP:
<?php
$ch = curl_init('https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: your-api-key',
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Student 1 Zone',
'bounds_x' => 0,
'bounds_y' => 0,
'bounds_width' => 1200,
'bounds_height' => 800,
'assigned_participant_id' => 'part-uuid-123',
'zone_visibility' => 'hidden',
'lock_mode' => 'hard'
]),
CURLOPT_RETURNTRANSFER => true
]);
$zone = json_decode(curl_exec($ch), true);
echo "Created zone: {$zone['name']}\n";Use Cases
- Create isolated exam zones for each student
- Set up practice areas with visible zones
- Prepare zones in host_only mode before class
- Create group workspaces with privacy controls
Possible Errors
| Status Code | Error | Description |
|---|---|---|
400 | Bad Request | Invalid zone data (missing assignment, width < 100, invalid zone_visibility) |
404 | Not Found | Board or participant not found |
401 | Unauthorized | Invalid or missing API key |
409 | Conflict | Zone overlaps with existing zone |
List All Zones
Retrieves all zones for a board. Only returns frames with participant/user assignments.
Request
GET /boards/:boardUuid/frames?assigned=true
X-API-Key: your-api-keyURL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
assigned | boolean | No | Filter for assigned frames only (zones) |
includeArchived | boolean | No | Include archived zones (default: false) |
Response
Status Code: 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"board_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Student 1 Zone",
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": "part-uuid-123",
"assigned_user_id": null,
"zone_visibility": "hidden",
"zone_version": 1,
"lock_mode": "soft",
"visibility": "active",
"created_at": "2025-11-26T10:00:00Z",
"updated_at": "2025-11-26T10:00:00Z"
},
{
"id": "660f9500-f3ac-52e5-b827-557766551111",
"board_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Student 2 Zone",
"bounds_x": 1300,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": "part-uuid-456",
"assigned_user_id": null,
"zone_visibility": "visible",
"zone_version": 2,
"lock_mode": "none",
"visibility": "active",
"created_at": "2025-11-26T10:05:00Z",
"updated_at": "2025-11-26T10:10:00Z"
}
]Example Requests
JavaScript/TypeScript:
// Get all zones
const response = await fetch(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames?assigned=true',
{
headers: {
'X-API-Key': 'your-api-key'
}
}
);
const zones = await response.json();
console.log(`Total zones: ${zones.length}`);
// Find zones by visibility mode
const hiddenZones = zones.filter(z => z.zone_visibility === 'hidden');
console.log(`Hidden zones (exam mode): ${hiddenZones.length}`);cURL:
# Get all zones
curl -H "X-API-Key: your-api-key" \
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames?assigned=true'PHP:
<?php
$headers = ['X-API-Key: your-api-key'];
$zones = json_decode(
file_get_contents(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames?assigned=true',
false,
stream_context_create(['http' => ['header' => implode("\r\n", $headers)]])
),
true
);
foreach ($zones as $zone) {
echo "{$zone['name']} - Visibility: {$zone['zone_visibility']}\n";
}Use Cases
- Display teacher dashboard showing all student zones
- Monitor exam progress
- Check which students have assigned zones
- Build zone navigation UI
Possible Errors
| Status Code | Error | Description |
|---|---|---|
404 | Not Found | Board does not exist |
401 | Unauthorized | Invalid API key |
Get Zone
Retrieves a specific zone by ID.
Request
GET /boards/:boardUuid/frames/:frameId
X-API-Key: your-api-keyURL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
frameId | string | Zone (frame) UUID |
Response
Status Code: 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"board_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Student 1 Zone",
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": "part-uuid-123",
"assigned_user_id": null,
"zone_visibility": "hidden",
"zone_version": 1,
"lock_mode": "soft",
"visibility": "active",
"created_at": "2025-11-26T10:00:00Z",
"updated_at": "2025-11-26T10:00:00Z"
}Example Requests
JavaScript/TypeScript:
const zoneId = '550e8400-e29b-41d4-a716-446655440000';
const response = await fetch(
`https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/${zoneId}`,
{
headers: {
'X-API-Key': 'your-api-key'
}
}
);
const zone = await response.json();
console.log(`Zone: ${zone.name} (Version: ${zone.zone_version})`);
console.log(`Assigned to: ${zone.assigned_participant_id || zone.assigned_user_id}`);cURL:
curl -H "X-API-Key: your-api-key" \
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400-e29b-41d4-a716-446655440000'Possible Errors
| Status Code | Error | Description |
|---|---|---|
404 | Not Found | Zone or board not found |
401 | Unauthorized | Invalid API key |
Update Zone
Updates zone properties such as visibility mode, bounds, or assignment.
Request
PATCH /boards/:boardUuid/frames/:frameId
X-API-Key: your-api-key
Content-Type: application/json
{
"zone_visibility": "visible",
"name": "Updated Zone Name"
}URL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
frameId | string | Zone UUID |
Request Body Parameters
All fields are optional. Only provided fields will be updated.
| Parameter | Type | Description |
|---|---|---|
name | string | New zone name |
bounds_x | number | New X coordinate |
bounds_y | number | New Y coordinate |
bounds_width | number | New width (min: 100) |
bounds_height | number | New height (min: 100) |
zone_visibility | string | New privacy mode: visible, hidden, host_only |
lock_mode | string | New lock mode: none, soft, hard |
background_color | string | New hex color or null to remove |
Note: Updating bounds increments
zone_versionto protect against race conditions.
Response
Status Code: 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"board_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Updated Zone Name",
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": "part-uuid-123",
"assigned_user_id": null,
"zone_visibility": "visible",
"zone_version": 2,
"lock_mode": "soft",
"visibility": "active",
"created_at": "2025-11-26T10:00:00Z",
"updated_at": "2025-11-26T10:30:00Z"
}Example Requests
JavaScript/TypeScript:
// Switch from exam mode to practice mode
const response = await fetch(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400',
{
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
zone_visibility: 'visible', // Students can now see each other's work
lock_mode: 'none' // Remove viewport lock
})
}
);
const updated = await response.json();
console.log(`Updated to v${updated.zone_version}`);cURL:
# Change to hidden mode (exam)
curl -X PATCH https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400 \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"zone_visibility": "hidden",
"lock_mode": "hard"
}'
# Resize zone
curl -X PATCH https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400 \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"bounds_width": 1600,
"bounds_height": 900
}'PHP:
<?php
$ch = curl_init('https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'X-API-Key: your-api-key',
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode([
'zone_visibility' => 'visible',
'lock_mode' => 'none'
]),
CURLOPT_RETURNTRANSFER => true
]);
$updated = json_decode(curl_exec($ch), true);
echo "Updated: {$updated['name']} (v{$updated['zone_version']})\n";Use Cases
- Switch between exam and practice modes
- Resize zones after content changes
- Change zone names for clarity
- Adjust zone positions (triggers sticky behavior - objects move with zone)
Sticky Behavior
When a host moves a zone (updates bounds_x or bounds_y), all objects inside the zone automatically move with it. This ensures student work stays within zone boundaries.
Possible Errors
| Status Code | Error | Description |
|---|---|---|
400 | Bad Request | Invalid update data (e.g., width < 100, invalid zone_visibility) |
404 | Not Found | Zone or board not found |
401 | Unauthorized | Invalid API key |
403 | Forbidden | Non-host attempting to modify zone (only host can modify zones) |
409 | Conflict | New bounds overlap with existing zone |
Delete Zone
Soft deletes a zone by setting visibility to archived. Objects inside the zone are preserved.
Request
DELETE /boards/:boardUuid/frames/:frameId
X-API-Key: your-api-keyURL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
frameId | string | Zone UUID |
Response
Status Code: 200 OK
{
"message": "Frame archived successfully"
}Example Requests
JavaScript/TypeScript:
const response = await fetch(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400',
{
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
}
);
const result = await response.json();
console.log(result.message);cURL:
curl -X DELETE \
-H "X-API-Key: your-api-key" \
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400'PHP:
<?php
$ch = curl_init('https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['X-API-Key: your-api-key'],
CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
echo $result['message'];Behavior
- Zone visibility changes to
archived - Zone is hidden from zone lists (unless
includeArchived=true) - Objects within zone are NOT deleted (student work is preserved)
- Assignment is removed (
assigned_participant_idandassigned_user_idset to null) - Objects become "unassigned" and editable by everyone
Possible Errors
| Status Code | Error | Description |
|---|---|---|
404 | Not Found | Zone or board not found |
401 | Unauthorized | Invalid API key |
403 | Forbidden | Non-host attempting to delete zone |
Assign Participant
Assigns or reassigns a participant/user to an existing frame, converting it to a zone.
Request
POST /boards/:boardUuid/frames/:frameId/assign
X-API-Key: your-api-key
Content-Type: application/json
{
"participant_id": "part-uuid-123"
}Or assign authenticated user:
POST /boards/:boardUuid/frames/:frameId/assign
X-API-Key: your-api-key
Content-Type: application/json
{
"user_id": "user-uuid-456"
}URL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
frameId | string | Frame UUID |
Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
participant_id | string | No* | Participant UUID (session-based) |
user_id | string | No* | User UUID (authenticated, priority over participant_id) |
Note: Either
participant_idoruser_idmust be provided. If both are provided,user_idtakes priority.
Response
Status Code: 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"board_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Zone 1",
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": "part-uuid-123",
"assigned_user_id": null,
"zone_visibility": "visible",
"zone_version": 2,
"lock_mode": "none",
"visibility": "active",
"created_at": "2025-11-26T10:00:00Z",
"updated_at": "2025-11-26T10:35:00Z"
}Example Requests
JavaScript/TypeScript:
// Assign participant to zone
const response = await fetch(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400/assign',
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
participant_id: 'part-uuid-123'
})
}
);
const zone = await response.json();
console.log(`Assigned to: ${zone.assigned_participant_id}`);cURL:
# Assign participant
curl -X POST https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400/assign \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"participant_id": "part-uuid-123"}'
# Assign authenticated user
curl -X POST https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400/assign \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"user_id": "user-uuid-456"}'PHP:
<?php
$ch = curl_init('https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400/assign');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: your-api-key',
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode([
'participant_id' => 'part-uuid-123'
]),
CURLOPT_RETURNTRANSFER => true
]);
$zone = json_decode(curl_exec($ch), true);
echo "Assigned to: {$zone['assigned_participant_id']}\n";Use Cases
- Assign student to pre-created zone
- Reassign zone to different participant
- Convert regular frame to student zone
- Handle participant reconnection with new session ID
Possible Errors
| Status Code | Error | Description |
|---|---|---|
400 | Bad Request | Missing both participant_id and user_id |
404 | Not Found | Frame, board, or participant not found |
401 | Unauthorized | Invalid API key |
403 | Forbidden | Non-host attempting to assign |
Unassign Participant
Removes participant/user assignment from a zone, converting it back to a regular frame.
Request
POST /boards/:boardUuid/frames/:frameId/unassign
X-API-Key: your-api-keyURL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
frameId | string | Zone UUID |
Response
Status Code: 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"board_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Zone 1",
"bounds_x": 0,
"bounds_y": 0,
"bounds_width": 1200,
"bounds_height": 800,
"assigned_participant_id": null,
"assigned_user_id": null,
"zone_visibility": "visible",
"zone_version": 3,
"lock_mode": "none",
"visibility": "active",
"created_at": "2025-11-26T10:00:00Z",
"updated_at": "2025-11-26T10:40:00Z"
}Example Requests
JavaScript/TypeScript:
const response = await fetch(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400/unassign',
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key'
}
}
);
const frame = await response.json();
console.log('Zone unassigned, now a regular frame');cURL:
curl -X POST \
-H "X-API-Key: your-api-key" \
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400/unassign'PHP:
<?php
$ch = curl_init('https://api.boardapi.io/api/v1/boards/a1b2c3d4/frames/550e8400/unassign');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['X-API-Key: your-api-key'],
CURLOPT_RETURNTRANSFER => true
]);
$frame = json_decode(curl_exec($ch), true);
echo "Unassigned zone\n";Behavior
- Sets
assigned_participant_idandassigned_user_idto null - Objects within zone remain (not deleted)
- Objects become editable by all participants
- Zone version increments
Use Cases
- Remove student from zone
- Reuse zone for different participant
- Clean up after class session
Possible Errors
| Status Code | Error | Description |
|---|---|---|
404 | Not Found | Zone or board not found |
401 | Unauthorized | Invalid API key |
403 | Forbidden | Non-host attempting to unassign |
Bulk Assign
Assigns multiple participants to zones in a single request. Useful for quickly setting up class sessions.
Request
POST /boards/:boardUuid/zones/bulk-assign
X-API-Key: your-api-key
Content-Type: application/json
{
"assignments": [
{
"frame_id": "550e8400-e29b-41d4-a716-446655440000",
"participant_id": "part-uuid-123"
},
{
"frame_id": "660f9500-f3ac-52e5-b827-557766551111",
"user_id": "user-uuid-456"
}
]
}URL Parameters
| Parameter | Type | Description |
|---|---|---|
boardUuid | string | Board UUID |
Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
assignments | array | Yes | Array of assignment objects (max 50) |
assignments[].frame_id | string | Yes | Frame UUID to assign |
assignments[].participant_id | string | No* | Participant UUID |
assignments[].user_id | string | No* | User UUID (priority) |
Note: Each assignment must have either
participant_idoruser_id.
Response
Status Code: 200 OK
{
"assigned": 2,
"failed": 0,
"results": [
{
"frame_id": "550e8400-e29b-41d4-a716-446655440000",
"success": true,
"zone_version": 2
},
{
"frame_id": "660f9500-f3ac-52e5-b827-557766551111",
"success": true,
"zone_version": 1
}
]
}Example Requests
JavaScript/TypeScript:
// Assign 10 students to 10 zones at once
const zones = ['zone-1-uuid', 'zone-2-uuid', /* ... */];
const participants = ['part-1-uuid', 'part-2-uuid', /* ... */];
const assignments = zones.map((frameId, i) => ({
frame_id: frameId,
participant_id: participants[i]
}));
const response = await fetch(
'https://api.boardapi.io/api/v1/boards/a1b2c3d4/zones/bulk-assign',
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ assignments })
}
);
const result = await response.json();
console.log(`Assigned: ${result.assigned}, Failed: ${result.failed}`);cURL:
curl -X POST https://api.boardapi.io/api/v1/boards/a1b2c3d4/zones/bulk-assign \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"assignments": [
{
"frame_id": "550e8400-e29b-41d4-a716-446655440000",
"participant_id": "part-uuid-123"
},
{
"frame_id": "660f9500-f3ac-52e5-b827-557766551111",
"participant_id": "part-uuid-456"
}
]
}'PHP:
<?php
$zones = ['550e8400', '660f9500', /* ... */];
$participants = ['part-uuid-123', 'part-uuid-456', /* ... */];
$assignments = array_map(function($frameId, $participantId) {
return [
'frame_id' => $frameId,
'participant_id' => $participantId
];
}, $zones, $participants);
$ch = curl_init('https://api.boardapi.io/api/v1/boards/a1b2c3d4/zones/bulk-assign');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: your-api-key',
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode(['assignments' => $assignments]),
CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
echo "Assigned: {$result['assigned']}, Failed: {$result['failed']}\n";Use Cases
- Quick class setup (assign all students at once)
- Reset zones after session
- Batch reassignment after roster changes
Performance Limit
Maximum 50 assignments per request. For larger batches, make multiple requests.
Possible Errors
| Status Code | Error | Description |
|---|---|---|
400 | Bad Request | Invalid assignments array, exceeds 50 items, or missing IDs |
404 | Not Found | Board not found |
401 | Unauthorized | Invalid API key |
403 | Forbidden | Non-host attempting bulk assign |
WebSocket Events
Zones support real-time synchronization via WebSocket. See WebSocket API for connection details.
Zone Events
| Event | Direction | Description |
|---|---|---|
zone:assigned | Server → Clients | Participant assigned to zone |
zone:unassigned | Server → Clients | Assignment removed |
zone:visibility-changed | Server → Clients | Privacy mode changed |
zone:violation | Server → Client | Participant tried to edit outside their zone |
zone:cache-invalidate | Server → Clients | Force clients to refetch zones |
zone:bounds-changed | Server → Clients | Zone moved/resized (sticky behavior) |
Event Payloads
zone:assigned
{
"frameId": "550e8400-e29b-41d4-a716-446655440000",
"participantId": "part-uuid-123",
"participantName": "John Doe",
"userId": null,
"zoneVersion": 2,
"timestamp": "2025-11-26T10:00:00Z"
}zone:unassigned
{
"frameId": "550e8400-e29b-41d4-a716-446655440000",
"previousParticipantId": "part-uuid-123",
"previousUserId": null,
"zoneVersion": 3,
"timestamp": "2025-11-26T10:05:00Z"
}zone:visibility-changed
{
"frameId": "550e8400-e29b-41d4-a716-446655440000",
"visibility": "hidden", // 'visible' | 'hidden' | 'host_only'
"zoneVersion": 4,
"timestamp": "2025-11-26T10:10:00Z"
}zone:violation
{
"objectId": "obj-uuid-789",
"reason": "Cannot draw in another student's zone",
"operation": "create", // 'create' | 'update' | 'delete'
"timestamp": "2025-11-26T10:15:00Z"
}zone:cache-invalidate
{
"boardUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"reason": "Zone assignments modified",
"timestamp": "2025-11-26T10:20:00Z"
}zone:bounds-changed
{
"frameId": "550e8400-e29b-41d4-a716-446655440000",
"bounds": {
"x": 100,
"y": 50,
"width": 1200,
"height": 800
},
"zoneVersion": 5,
"timestamp": "2025-11-26T10:25:00Z"
}Client-Side Handling
Listen for violations:
socket.on('zone:violation', (data) => {
console.warn(`Violation: ${data.reason}`);
// Revert object to original position
undoLastAction(data.objectId);
// Show toast notification
showToast(`You can only draw in your assigned zone`, 'error');
});Listen for visibility changes:
socket.on('zone:visibility-changed', async (data) => {
if (data.visibility === 'hidden') {
// Exam mode activated - hide other students' work
await filterBoardSnapshot();
console.log('Exam mode: You can only see your zone');
} else {
// Practice mode - show all zones
await refetchBoardSnapshot();
console.log('Practice mode: All zones visible');
}
});Handle zone version conflicts:
// When creating/updating object, pass current zone version
const result = await socket.emit('object:create', {
...objectData,
zoneVersion: myZone.zone_version
});
if (result.error && result.zoneChanged) {
// Zone boundaries changed, refetch and retry
myZone = await refetchMyZone();
console.log(`Zone version updated to v${myZone.zone_version}`);
// Retry operation
}React to cache invalidation:
socket.on('zone:cache-invalidate', async () => {
// Refetch zones from server
const zones = await fetch('/boards/a1b2c3d4/frames?assigned=true')
.then(r => r.json());
updateZonesCache(zones);
});Response Status Codes
| Code | Meaning | Description |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Zone created successfully |
400 | Bad Request | Invalid zone data, missing assignment, or invalid visibility mode |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Non-host attempting protected operation |
404 | Not Found | Zone, board, or participant not found |
409 | Conflict | Zone overlap detected or version conflict |
500 | Internal Server Error | Server error occurred |
Common Use Cases
Setup Exam Mode
// Complete exam setup workflow
async function setupExamMode(boardId, students) {
const zones = [];
// 1. Create hidden zones for each student
for (let i = 0; i < students.length; i++) {
const zone = await fetch(
`https://api.boardapi.io/api/v1/boards/${boardId}/frames`,
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: `${students[i].name} - Exam Zone`,
bounds_x: 1300 * i,
bounds_y: 0,
bounds_width: 1200,
bounds_height: 800,
assigned_participant_id: students[i].participantId,
zone_visibility: 'hidden', // Students can't see each other
lock_mode: 'hard' // Prevent navigation outside zone
})
}
).then(r => r.json());
zones.push(zone);
}
console.log(`Created ${zones.length} exam zones`);
return zones;
}
// Usage
const students = [
{ name: 'Alice', participantId: 'part-uuid-1' },
{ name: 'Bob', participantId: 'part-uuid-2' },
{ name: 'Charlie', participantId: 'part-uuid-3' }
];
await setupExamMode('a1b2c3d4', students);Switch from Exam to Practice Mode
// Change all zones from hidden to visible
async function switchToPracticeMode(boardId) {
// Get all zones
const zones = await fetch(
`https://api.boardapi.io/api/v1/boards/${boardId}/frames?assigned=true`,
{
headers: { 'X-API-Key': 'your-api-key' }
}
).then(r => r.json());
// Update each zone
for (const zone of zones) {
await fetch(
`https://api.boardapi.io/api/v1/boards/${boardId}/frames/${zone.id}`,
{
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
zone_visibility: 'visible',
lock_mode: 'none'
})
}
);
}
console.log(`Switched ${zones.length} zones to practice mode`);
}Create Group Workspaces
// Create zones for group collaboration
async function createGroupZones(boardId, groups) {
const zones = [];
for (let i = 0; i < groups.length; i++) {
const zone = await fetch(
`https://api.boardapi.io/api/v1/boards/${boardId}/frames`,
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: groups[i].name,
bounds_x: 1400 * i,
bounds_y: 0,
bounds_width: 1300,
bounds_height: 1000,
zone_visibility: 'visible', // Groups can see each other's work
background_color: groups[i].color
})
}
).then(r => r.json());
zones.push(zone);
// Assign all group members
for (const member of groups[i].members) {
await fetch(
`https://api.boardapi.io/api/v1/boards/${boardId}/frames/${zone.id}/assign`,
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
participant_id: member.participantId
})
}
);
}
}
return zones;
}
// Usage
const groups = [
{
name: 'Blue Team',
color: '#E3F2FD',
members: [
{ participantId: 'part-uuid-1' },
{ participantId: 'part-uuid-2' }
]
},
{
name: 'Red Team',
color: '#FFEBEE',
members: [
{ participantId: 'part-uuid-3' },
{ participantId: 'part-uuid-4' }
]
}
];
await createGroupZones('a1b2c3d4', groups);Monitor Student Progress
// Real-time teacher dashboard
async function monitorStudentZones(boardId) {
const zones = await fetch(
`https://api.boardapi.io/api/v1/boards/${boardId}/frames?assigned=true`,
{
headers: { 'X-API-Key': 'your-api-key' }
}
).then(r => r.json());
// Get board snapshot with all student work
const board = await fetch(
`https://api.boardapi.io/api/v1/boards/${boardId}`,
{
headers: { 'X-API-Key': 'your-api-key' }
}
).then(r => r.json());
// Analyze each zone
const progress = zones.map(zone => {
const objectsInZone = board.state.objects.filter(obj =>
isInsideZone(obj.bounds, zone.bounds)
);
return {
student: zone.name,
objectCount: objectsInZone.length,
isEmpty: objectsInZone.length === 0,
version: zone.zone_version
};
});
console.table(progress);
return progress;
}
function isInsideZone(objBounds, zoneBounds) {
return (
objBounds.x >= zoneBounds.x &&
objBounds.x + objBounds.width <= zoneBounds.x + zoneBounds.width &&
objBounds.y >= zoneBounds.y &&
objBounds.y + objBounds.height <= zoneBounds.y + zoneBounds.height
);
}Race Condition Protection
Zones use versioning to prevent concurrent modification conflicts:
Client-Side Version Tracking
// Track zone version on client
let myZone = {
id: '550e8400-e29b-41d4-a716-446655440000',
zone_version: 1,
bounds: { x: 0, y: 0, width: 1200, height: 800 }
};
// Pass version when creating objects
socket.emit('object:create', {
...objectData,
zoneVersion: myZone.zone_version
});
// Handle version conflicts
socket.on('object:create:result', (result) => {
if (result.error && result.zoneChanged) {
// Zone was modified (host moved it), refetch
console.warn('Zone boundaries changed, refetching...');
refetchMyZone().then(updatedZone => {
myZone = updatedZone;
// Retry operation with new version
});
}
});Server-Side Version Check
The server automatically increments zone_version when:
- Zone bounds change (host moves/resizes zone)
- Participant assignment changes
- Visibility mode changes
If client provides outdated version, request is rejected with zoneChanged: true.
Performance Limits
| Parameter | Limit | Note |
|---|---|---|
| Zones per board | 50 | Performance optimized for this limit |
| Objects per zone | 100 (soft limit) | Warning shown, not enforced |
| Bulk assignments | 50 per request | For larger batches, make multiple requests |
| Concurrent students | 50 | WebSocket capacity |
Known Limitations
Mobile/Touch
- Pinch-to-zoom may move objects outside zone boundaries
- Recommended to use desktop for exam mode
Reconnection
- Participant ID (session-based): Lost on reconnect, requires reassignment
- User ID (authenticated): Preserved across sessions
Export/Import
- Zone assignments are removed when board is exported/imported
- Objects are preserved, but become unassigned
Architecture
For detailed technical architecture including permission checks, snapshot filtering, and WebSocket integration, see:
- EPIC-024 Specification
- Features Matrix - Student Zones
- WebSocket API - Real-time events
Next Steps
- WebSocket API - Real-time zone events
- Frames API - Zone foundation
- Boards API - Parent board operations
- Integration Guide - Complete examples