All responses
Every response is a JSON object with a success boolean and either a data array or an error string.
HTTP Methods
All endpoints are GET. CORS headers are set to allow any origin — safe to call from a mobile app or browser.
Live Positions
Animal coordinates update every 5 seconds via the movement daemon. Poll /zones/{id}/animals at your desired rate.
Returns all active HoloSafari parks. Each park is an independently branded AR experience (e.g. Dinoskop, Fabel Rijk) running on the shared HoloSafari platform. The response includes the park's GPS bounding box, theme details, and the number of zones.
No parameters.
{
"success": true,
"count": 3,
"data": [
{
"id": 1,
"name": "Dinoskop",
"city": "Amsterdam",
"theme": {
"id": 1,
"name": "Dinosaur",
"description": "Journey back 65 million years to the age of the terrible lizards.",
"icon": "dinosaur"
},
"bounds": {
"lat_min": 52.353,
"lat_max": 52.362,
"lng_min": 4.865,
"lng_max": 4.878
},
"active": true,
"zone_count": 3,
"created_at": "2026-02-23 09:00:00"
}
]
}
Returns all zones belonging to the specified park. A zone is a named GPS-bounded area within the park that contains a specific set of animal instances. Each zone also carries a size classification that governs its capacity.
| Name | Type | Description |
|---|---|---|
| id required | integer | The numeric park ID, obtained from GET /parks. |
{
"success": true,
"park": {
"id": 1,
"name": "Dinoskop"
},
"count": 3,
"data": [
{
"id": 1,
"park_id": 1,
"name": "Jurassic Jungle",
"bounds": {
"lat_min": 52.353,
"lat_max": 52.3575,
"lng_min": 4.865,
"lng_max": 4.872
},
"size": {
"id": 3,
"name": "large",
"label": "Large"
},
"animal_count": 8
},
{
"id": 2,
"park_id": 1,
"name": "Carnivore Canyon",
"bounds": {
"lat_min": 52.3575,
"lat_max": 52.362,
"lng_min": 4.865,
"lng_max": 4.872
},
"size": {
"id": 2,
"name": "medium",
"label": "Medium"
},
"animal_count": 5
}
]
}
Returns every animal instance in the specified zone along with its current GPS position, heading direction (0–360°, where 0 = North), and the timestamp of the last position update.
Positions are updated every 5 seconds by the movement daemon. Multiple visitors polling this endpoint simultaneously will all receive the same coordinates — every visitor sees the same animals in the same locations.
If position is null, the movement daemon hasn't run yet since this animal was added to the zone.
| Name | Type | Description |
|---|---|---|
| id required | integer | The numeric zone ID, obtained from GET /parks/{id}/zones. |
{
"success": true,
"zone": {
"id": 1,
"name": "Jurassic Jungle",
"park": {
"id": 1,
"name": "Dinoskop"
},
"bounds": {
"lat_min": 52.353,
"lat_max": 52.3575,
"lng_min": 4.865,
"lng_max": 4.872
}
},
"count": 8,
"data": [
{
"instance_id": 1,
"animal": {
"id": 2,
"name": "Triceratops",
"description": "Three horns and a large bony frill distinguish this iconic herbivore...",
"storyable": true,
"size": {
"id": 3,
"name": "large",
"label": "Large"
},
"theme": {
"id": 1,
"name": "Dinosaur"
}
},
"position": {
"latitude": 52.35512340,
"longitude": 4.86823400,
"direction": 47.25,
"last_updated": "2026-02-23 12:00:05"
}
},
{
"instance_id": 2,
"animal": {
"id": 2,
"name": "Triceratops",
"description": "Three horns and a large bony frill...",
"storyable": true,
"size": { "id": 3, "name": "large", "label": "Large" },
"theme": { "id": 1, "name": "Dinosaur" }
},
"position": {
"latitude": 52.35401200,
"longitude": 4.86921500,
"direction": 195.80,
"last_updated": "2026-02-23 12:00:05"
}
}
]
}
The movement daemon (movement/update_positions.php) runs as a background process and updates every animal's position every 5 seconds. All visitors to the same zone see the same coordinates — the experience is shared and consistent.
Algorithm (per animal, per tick):
- Wander — apply a small random direction perturbation (±8–25° depending on size).
- Boundary avoidance — when within 30 m of a zone edge, a proportional repulsion force steers the animal back toward the interior.
- Separation — if another animal is within personal-space range, a repulsion vector steers both apart. Larger animals have larger personal spaces.
- Move — advance the animal at its per-tick speed in the resolved direction, then hard-clamp the position inside the zone bounds.
- Persist — write the new latitude, longitude, and direction to
animal_positions.
Speed & personal-space by size:
| Size | Speed (m/tick) | Wander (°/tick) | Personal space |
|---|---|---|---|
| small | 4 m | ±25° | 8 m |
| medium | 6 m | ±15° | 15 m |
| large | 3 m | ±8° | 30 m |
Running the daemon:
nohup php /var/www/holosafari/api/movement/update_positions.php \
>> /var/log/holosafari-movement.log 2>&1 &