v1.0.0

HoloSafari API Reference

The backend platform powering augmented-reality animal parks. Each park runs on the HoloSafari platform under its own brand — from Dinoskop to Fabel Rijk.

https://api.holosafari.com
JSON (UTF-8)
None required
Open (all origins)
Every 5 seconds

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.

Parks
GET /parks List all active parks ▼

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.

Request
GET https://api.holosafari.com/parks
Parameters

No parameters.

Responses
200 Array of park objects. Empty array when no parks are active.
500 Database error.
Example response — 200 OK
application/json
{
  "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"
    }
  ]
}
GET /parks/{id}/zones List zones in a park ▼

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.

Request
GET https://api.holosafari.com/parks/1/zones
Path parameters
NameTypeDescription
id required integer The numeric park ID, obtained from GET /parks.
Responses
200 Park object and array of zone objects.
404 No active park found with the given ID.
500 Database error.
Example response — 200 OK
application/json
{
  "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
    }
  ]
}
Zones
GET /zones/{id}/animals Get animals with live positions ▼

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.

Request
GET https://api.holosafari.com/zones/1/animals
Path parameters
NameTypeDescription
id required integer The numeric zone ID, obtained from GET /parks/{id}/zones.
Responses
200 Zone metadata and array of animal instances with live positions.
404 No zone found with the given ID.
500 Database error.
Example response — 200 OK
application/json
{
  "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"
      }
    }
  ]
}
Data Models
Park
FieldTypeDescription
idintegerPrimary key
namestringPublic brand name (e.g. "Dinoskop")
citystringCity where the park is located
themeThemeTheme object (see below)
boundsBoundsGPS bounding box
activebooleanWhether the park is open
zone_countintegerNumber of zones in this park
Zone
FieldTypeDescription
idintegerPrimary key
park_idintegerParent park ID
namestringZone display name
boundsBoundsGPS bounding box
sizeSizeZone size classification
animal_countintegerTotal animal instances in zone
Animal Instance
FieldTypeDescription
instance_idintegerUnique ID for this individual animal
animalAnimalAnimal master data (name, size, etc.)
positionPosition|nullLive GPS position, or null if pending
Position
FieldTypeDescription
latitudefloatWGS-84 latitude (8 decimals)
longitudefloatWGS-84 longitude (8 decimals)
directionfloatHeading 0–360° (0=North, 90=East)
last_updateddatetimeUTC timestamp of last movement tick
Bounds
FieldTypeDescription
lat_minfloatSouthern boundary latitude
lat_maxfloatNorthern boundary latitude
lng_minfloatWestern boundary longitude
lng_maxfloatEastern boundary longitude
Size
ValueLabelNotes
smallSmallFast, erratic movement; 8 m personal space
mediumMediumModerate speed; 15 m personal space
largeLargeSlower, deliberate movement; 30 m personal space
Movement Engine

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):

  1. Wander — apply a small random direction perturbation (±8–25° depending on size).
  2. Boundary avoidance — when within 30 m of a zone edge, a proportional repulsion force steers the animal back toward the interior.
  3. Separation — if another animal is within personal-space range, a repulsion vector steers both apart. Larger animals have larger personal spaces.
  4. Move — advance the animal at its per-tick speed in the resolved direction, then hard-clamp the position inside the zone bounds.
  5. Persist — write the new latitude, longitude, and direction to animal_positions.

Speed & personal-space by size:

SizeSpeed (m/tick)Wander (°/tick)Personal space
small4 m±25°8 m
medium6 m±15°15 m
large3 m±8°30 m

Running the daemon:

bash
nohup php /var/www/holosafari/api/movement/update_positions.php \
     >> /var/log/holosafari-movement.log 2>&1 &