Skip to content
Developer API · version 1

Developer reference

Put a dive centre's live menu, dive sites, schedule and shop on its own website. One key, JSON, no SDK.

Quick start

  1. In the DiveOps dashboard, open Settings, Show on your website, API keys.
  2. Turn the surface on and copy a key (men_… for menu, sites and schedule, cat_… for the shop).
  3. Call an endpoint with ?key=:
curl "https://app.diveops.ai/api/public/menu?key=YOUR_KEY"

Conventions

Authentication
One publishable key per surface in the key query parameter. A single men_ key authorises all three of menu, dive-sites and schedule. These are publishable keys, safe to put in a public web page (the embed snippet does exactly that). Rotate or revoke them anytime in the dashboard.
Versioning
Every response includes "version": 1. Fields are added over time but never renamed or removed within a version. Read fields by name, not position.
Caching and polling
Responses are edge cached for about 60 seconds and carry an ETag. Send it back as If-None-Match for a cheap 304 Not Modified. Polling every few minutes is fine.
Rate limits
60 requests per 60 seconds per IP. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset so you can self-throttle. Over the limit returns 429 with Retry-After.
Errors
A bad or disabled key returns an opaque 404. We never reveal whether a centre or key exists. CORS is open (Access-Control-Allow-Origin: *).

Endpoints

GET/api/public/menu

The live dive menu: courses, course bundles, activities and trips, grouped by category in display order.

curl "https://app.diveops.ai/api/public/menu?key=YOUR_KEY"
{
  "version": 1,
  "centre": { "name": "Your Dive Centre", "currency": "THB", "accent": "#2CC5AC" },
  "categories": ["Beginner", "Fun Dive", "Specialty"],
  "services": [
    {
      "id": "ec2fbbd0-…", "type": "course", "category": "Beginner",
      "name": "Open Water Course", "description": "…", "price": 11000,
      "duration": "3-4 days", "duration_days": 4, "min_age": 10,
      "is_featured": true, "event_date": null
    }
  ]
}
FieldTypeNotes
iduuidStable. Use it to deep-link into the booking page.
typestringcourse · bundle · activity · organised_trip … (custom types possible; fun_dive and package before July 2026)
categorystringDisplay category (also listed in top-level categories[]).
namestring
descriptionstring|nullCapped at 600 characters.
pricenumber|nullIn the centre currency (centre.currency).
durationstring|nullFree text, e.g. "3-4 days".
duration_daysinteger|null
min_ageinteger|null
is_featuredboolean
event_datedate|nullSet for fixed-date organised trips; expired ones are dropped.
GET/api/public/dive-sites

Active dive sites with GPS, depth and difficulty. Same men_ key.

curl "https://app.diveops.ai/api/public/dive-sites?key=YOUR_KEY"
{
  "version": 1,
  "centre": { "name": "Your Dive Centre", "currency": "THB" },
  "dive_sites": [
    {
      "id": "…", "name": "North Pinnacle", "depth_range": "14-36m",
      "difficulty": "intermediate-advanced", "distance": "45 min by boat",
      "highlights": "Reef sharks and barracuda", "best_for": "Advanced divers",
      "latitude": 7.80, "longitude": 98.40, "updated_at": "2026-06-20T…Z"
    }
  ]
}
FieldTypeNotes
iduuid
namestring
depth_rangestring|null
difficultystringeasy · easy-intermediate · intermediate · intermediate-advanced · advanced
distancestring|null
highlightsstring|null
best_forstring|null
latitude / longitudenumber|nullDecimal degrees.
updated_atdate-time
GET/api/public/schedule

The daily trip plan, already weather-resolved. Only the active option for each date is returned. Optional from and to dates (YYYY-MM-DD); defaults to today to plus 30 days, capped at a 90-day window.

curl "https://app.diveops.ai/api/public/schedule?key=YOUR_KEY&from=2026-07-01&to=2026-07-07"
{
  "version": 1,
  "centre": { "name": "Your Dive Centre", "timezone": "Asia/Bangkok" },
  "from": "2026-07-01", "to": "2026-07-07",
  "days": [
    {
      "date": "2026-07-01", "option": "A",
      "slots": {
        "morning": {
          "planned":  [{ "site_id": "…", "name": "North Pinnacle", "difficulty": "advanced", "latitude": 7.80, "longitude": 98.40, "count": 2 }],
          "fallback": [{ "site_id": "…", "name": "House Reef", "difficulty": "easy", "latitude": null, "longitude": null, "count": 2 }]
        },
        "afternoon": { "planned": [], "fallback": [] },
        "evening":   { "planned": [], "fallback": [] }
      }
    }
  ]
}
FieldTypeNotes
datedate
optionstringThe resolved weather option for the date (e.g. "A"/"B"). Internal A/B variants are not exposed.
slots.{morning,afternoon,evening}objectEach has planned[] and fallback[].
…planned[]arrayThe headline plan. Show this.
…fallback[]arrayBad-weather alternative (display secondary; may be empty, and always empty when the centre has switched Plan B off).
site.site_iduuid|nullMatches a dive_sites id; null for a free-text entry.
site.name / difficulty / latitude / longitudemixedResolved from the dive site when site_id is set.
site.countintegerNumber of dives at this site in the slot.
GET/api/public/catalogue

Retail shop products with a coarse stock badge. Exact quantities are never exposed. Uses the cat_ key.

curl "https://app.diveops.ai/api/public/catalogue?key=YOUR_SHOP_KEY"
{
  "version": 1,
  "centre": { "name": "Your Dive Centre", "currency": "THB", "accent": "#2CC5AC" },
  "products": [
    { "name": "Mares Avanti Quattro+ Fins", "category": "Fins", "price": 4200, "currency": "THB", "stock": "in" }
  ]
}
FieldTypeNotes
namestring
categorystring|null
pricenumber|null
currencystring
stockstringin · low · out (coarse, never exact quantities).

Machine-readable spec

Import the OpenAPI 3.1 spec into Postman, Insomnia or your codegen of choice:

https://app.diveops.ai/api/public/openapi.yaml

Building something with DiveOps?

Manage keys in the dashboard under Settings, Show on your website. Need a hand wiring it up?