--- title: "Demo availability and booking guide | Data Hippo" description: "Read available times with a simple API, then book a founder-led demo with the attendee’s approval." canonical: "https://datahippo.ai/docs/booking/" lang: "en" modified: "2026-09-18T00:32:11.000Z" --- # Demo availability and booking guide | Data Hippo Source: https://datahippo.ai/docs/booking/ ## Markdown navigation - [Whole site](https://datahippo.ai/site.md) - [Home](https://datahippo.ai/index.md) - [Contact](https://datahippo.ai/contact.md) - [Platform guide (English)](https://datahippo.ai/docs/index.md) - [Product guide (English)](https://datahippo.ai/docs/products.md) - [Booking guide (English)](https://datahippo.ai/docs/booking.md) - [Privacy (English)](https://datahippo.ai/privacy.md) - [Terms (English)](https://datahippo.ai/terms.md) - [BAA (English)](https://datahippo.ai/baa.md) Available languages: [English](https://datahippo.ai/site.md) · [Português](https://datahippo.ai/pt/site.md) · [Español](https://datahippo.ai/es/site.md) · [Français](https://datahippo.ai/fr/site.md) # Demo availability and booking guide Read available times with a simple API, then book a founder-led demo with the attendee’s approval. This public interface schedules a 30-minute Google Meet demonstration. It does not access customer data, the Data Hippo platform, or an administrative calendar. [Read this guide as Markdown →](https://datahippo.ai/docs/booking.md) · [OpenAPI specification →](https://datahippo.ai/openapi.json) ## When should an agent use this interface? Use [the platform guide](https://datahippo.ai/docs/index.md) and [product reference](https://datahippo.ai/docs/products.md) to answer questions about Data Hippo. Use `GET /demo/slots` only when someone wants to see meeting times. Use `POST /demo/book` only when the attendee has approved one specific time and their contact details. Reading the website is not permission to book. This is a public demo scheduler, not a developer API for querying healthcare data or controlling a customer deployment. If the request needs those capabilities, [contact the team](https://datahippo.ai/contact.md) rather than inventing an endpoint. ## Read availability, then book an approved time These examples use the production availability endpoint. Discover the current endpoint from `servers[0].url` in the [OpenAPI specification](https://datahippo.ai/openapi.json). A locally served specification points to development infrastructure. Both environments use the real calendar; development is not a booking dry run. Run one example in your terminal. Each reads the next seven days from the current UTC instant, using America/New\_York as the display timezone. Change that value to the attendee’s IANA timezone. No credential, package install, or forged browser headers are needed. By default, these examples only read availability; they never choose or book the first slot. After the attendee approves a returned time and their details, create `booking.json` using the template below. Replace every placeholder. Set `attendee_confirmed` to `true` only after approval. Optional fields are `organization` and `comments`. Never include health data or secrets. ```json { "start": "REPLACE_WITH_APPROVED_UTC_ISO_TIME", "timezone": "America/New_York", "name": "REPLACE_WITH_APPROVED_NAME", "email": "REPLACE_WITH_APPROVED_EMAIL", "attendee_confirmed": false } ``` Create and retain one `IDEMPOTENCY_KEY` for this exact booking: current Unix milliseconds, a period, and a UUIDv4. For example, JavaScript generates it with `Date.now() + '.' + require('node:crypto').randomUUID()`; Python uses `str(int(time.time() * 1000)) + '.' + str(uuid.uuid4())`; PowerShell uses `[string][DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + '.' + [guid]::NewGuid().ToString()`. Bash can use the Node or Python command. Save the resulting value before submission; do not regenerate it for a retry. Set the environment variables `IDEMPOTENCY_KEY` to that value and `BOOKING_CONFIRMED=yes` only when ready to submit the approved request. Rerun the same example. It reads availability again, then sends your chosen payload once. Keep responses private: they may contain attendee cancellation and rescheduling links. Save JavaScript as `availability.cjs` and run `node availability.cjs`. Save Python as `availability.py` and run `python3 availability.py`. Paste the Bash or PowerShell example into its respective shell. ### JavaScript · Node.js ```javascript // Node.js: built-in https; no packages or browser headers. const https = require('node:https'); const url = new URL('https://dh-booking-prod.fritz-9f9.workers.dev/v1/demo/slots'); url.search = new URLSearchParams({ after: new Date().toISOString(), days: '7', timezone: 'America/New_York' }); const request = https.get(url, { timeout: 15000 }, response => { let body = ''; response.setEncoding('utf8'); response.on('data', chunk => { body += chunk; }); response.on('end', () => { if (response.statusCode !== 200) { console.error('HTTP', response.statusCode, body); if (response.statusCode === 429) console.error('Retry after:', response.headers['retry-after'] || '60', 'seconds'); process.exitCode = 1; return; } console.log(body); // Optional second step, only after the attendee approves booking.json. if (process.env.BOOKING_CONFIRMED !== 'yes') return; const key = process.env.IDEMPOTENCY_KEY; if (!key) throw new Error('Set and retain IDEMPOTENCY_KEY first'); const payload = require('node:fs').readFileSync('booking.json', 'utf8'); if (JSON.parse(payload).attendee_confirmed !== true) throw new Error('Approval required'); const booking = https.request(new URL('/v1/demo/book', url), { method: 'POST', timeout: 20000, headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key } }, result => { let receipt = ''; result.setEncoding('utf8'); result.on('data', chunk => { receipt += chunk; }); result.on('end', () => { console.log('Booking HTTP', result.statusCode, receipt); if (result.statusCode !== 200) process.exitCode = 1; }); }); booking.on('timeout', () => booking.destroy(new Error('Booking status unknown; retain the same key and payload'))); booking.on('error', error => { console.error(error.message); process.exitCode = 1; }); booking.end(payload); }); }); request.on('timeout', () => request.destroy(new Error('Availability request timed out'))); request.on('error', error => { console.error(error.message); process.exitCode = 1; }); ``` ### Python · standard library ```python from datetime import datetime, timezone from urllib.parse import urlencode from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError import sys import os import json params = urlencode({ "after": datetime.now(timezone.utc).isoformat(), "days": 7, "timezone": "America/New_York" }) url = "https://dh-booking-prod.fritz-9f9.workers.dev/v1/demo/slots?" + params try: request = Request(url, headers={"User-Agent": "DataHippoAvailabilityExample/1.0"}) with urlopen(request, timeout=15) as response: print(response.read().decode("utf-8")) # Optional second step, only after the attendee approves booking.json. if os.environ.get("BOOKING_CONFIRMED") == "yes": key = os.environ["IDEMPOTENCY_KEY"] # Retain this key for this exact request. with open("booking.json", encoding="utf-8") as source: payload = json.load(source) if payload.get("attendee_confirmed") is not True: raise ValueError("Approval required") booking = Request("https://dh-booking-prod.fritz-9f9.workers.dev/v1/demo/book", data=json.dumps(payload).encode(), method="POST", headers={"Content-Type": "application/json", "Idempotency-Key": key, "User-Agent": "DataHippoAvailabilityExample/1.0"}) with urlopen(booking, timeout=20) as response: print(response.read().decode("utf-8")) except HTTPError as error: print("HTTP", error.code, error.read().decode("utf-8"), file=sys.stderr) if error.code == 429: print("Retry after:", error.headers.get("Retry-After", "60"), "seconds", file=sys.stderr) sys.exit(1) except (URLError, TimeoutError, ValueError, KeyError, OSError) as error: print(str(error), file=sys.stderr) sys.exit(1) ``` ### Bash · curl and date ```bash # macOS or Linux. curl prints response headers (including Retry-After) and JSON. # --fail-with-body requires curl 7.76 or later; no jq or packages required. curl --silent --show-error --fail-with-body --include --max-time 15 \ --get 'https://dh-booking-prod.fritz-9f9.workers.dev/v1/demo/slots' \ --data-urlencode "after=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ --data-urlencode 'days=7' \ --data-urlencode 'timezone=America/New_York' || exit $? # Optional second step. Review booking.json with the attendee before setting yes. if [ "${BOOKING_CONFIRMED:-}" = yes ]; then : "${IDEMPOTENCY_KEY:?Set and retain IDEMPOTENCY_KEY first}" curl --silent --show-error --fail-with-body --include --max-time 20 \ 'https://dh-booking-prod.fritz-9f9.workers.dev/v1/demo/book' \ -H 'Content-Type: application/json' -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ --data-binary @booking.json fi ``` ### PowerShell · native cmdlet ```powershell # PowerShell 5.1 or 7+, no modules required. $after = [Uri]::EscapeDataString([DateTimeOffset]::UtcNow.ToString("o")) $zone = [Uri]::EscapeDataString("America/New_York") $url = "https://dh-booking-prod.fritz-9f9.workers.dev/v1/demo/slots?after=$after&days=7&timezone=$zone" try { $result = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 15 -ErrorAction Stop $result | ConvertTo-Json -Depth 5 # Optional second step, only after the attendee approves booking.json. if ($env:BOOKING_CONFIRMED -eq 'yes') { if (-not $env:IDEMPOTENCY_KEY) { throw 'Set and retain IDEMPOTENCY_KEY first' } $payload = Get-Content -Raw booking.json if (($payload | ConvertFrom-Json).attendee_confirmed -ne $true) { throw 'Approval required' } $booking = @{ Uri = 'https://dh-booking-prod.fritz-9f9.workers.dev/v1/demo/book' Method = 'Post'; ContentType = 'application/json'; Body = $payload Headers = @{ 'Idempotency-Key' = $env:IDEMPOTENCY_KEY }; TimeoutSec = 20; ErrorAction = 'Stop' } Invoke-RestMethod @booking | ConvertTo-Json -Depth 5 } } catch { $response = $_.Exception.Response if ($response) { Write-Error ("HTTP " + [int]$response.StatusCode + ": " + $_.ErrorDetails.Message) if ([int]$response.StatusCode -eq 429) { Write-Host "Wait at least 60 seconds before requesting availability again." } } else { Write-Error $_.Exception.Message } exit 1 } ``` The JSON response contains `slots` with UTC `iso` start times, and `earliest`/`latest` bounds in Unix milliseconds. An empty list means no availability in that interval. A read does not reserve a time. Calendly owns minimum notice, working hours, and calendar conflicts; do not infer a fixed notice period. Only complete months within the next 90 days are offered. Availability requests time out after 15 seconds; booking requests after 20 seconds. For HTTP 429, wait the number of seconds in `Retry-After` before another read; if absent, wait at least 60 seconds. For other failures, show the error instead of presenting stale slots as available. Do not repeatedly poll or automatically retry booking submissions. ## Book with an agent or in a browser 1. Read availability, then present a few choices in the attendee’s timezone. Recheck availability before final selection; times can change. 2. Get the attendee’s explicit approval of the date, time, timezone, name, and email before submitting. Never submit health data or secrets. 3. POST the approved request to `/demo/book` with `attendee_confirmed: true` and the retained `Idempotency-Key`, or use the browser calendar at `https://datahippo.ai/contact/`. Both create a real invitation. 4. HTTP 200 confirms the booking. The same key and unchanged payload replay the original result, not another invitation. After a timeout, retain the exact key and payload; never generate a replacement key automatically. 5. Keys expire 24 hours after their timestamp. HTTP 409 with `idempotency_conflict` means the payload changed; `booking_unconfirmed` means the outcome is pending or uncertain; `idempotency_expired` means the key expired. For an uncertain or expired outcome, check the invitation or contact us before any new attempt. 6. Keep the invitee’s cancellation and rescheduling links private. Share them only with the attendee; they are not administrative links. Submission creates a real invitation. HTTP 429 requires waiting for `Retry-After`; HTTP 503 means scheduling is unavailable. Do not automatically retry submissions. If booking cannot be confirmed, give the attendee `https://datahippo.ai/contact/` and the proposed times instead. [Contact information →](https://datahippo.ai/contact.md) ## Errors, rate limits, and API changes API errors follow the `ApiError` schema: an `error` message, machine-readable `code`, and safe next action in `resolution`. Handle HTTP status first, then a recognized code; do not depend on exact message wording. Unknown errors should be shown safely to the requester, without treating the booking as confirmed. - **400:** Check required fields, explicit approval, and the timestamped key. Unknown booking fields are rejected. - **403:** The request origin is not allowed. Server clients omit `Origin`, `Sec-Fetch-Site`, and `Sec-Fetch-Dest`; native fetch `Sec-Fetch-Mode` is accepted. Do not spoof browser headers. - **404:** Check the method and route against the OpenAPI specification. - **409:** A slot or idempotency conflict needs attention. Pending or uncertain bookings must be checked before another attempt. - **413 / 415:** Send JSON with `Content-Type: application/json`, within the 4,096-byte request limit. - **429:** Respect `Retry-After` in seconds; the current limit window is 60 seconds. Avoid polling loops. Keep the same key and payload if a booking is retried. - **503:** Scheduling is unavailable. Do not interpret this as proof that an earlier booking did not happen. The API publishes `RateLimit-Limit` and `RateLimit-Policy`: 30 availability reads or 3 booking attempts per 60-second window, per client IP and Cloudflare location. Enforcement is distributed, not an exact global counter. No remaining-quota or reset timestamp is claimed; use `Retry-After` on HTTP 429. The live [OpenAPI document](https://datahippo.ai/openapi.json) is the current contract. Its server URL includes `/v1`; append the documented operation path. Legacy unversioned routes remain supported. Changes within v1 remain backward-compatible and additive; a breaking change requires a new major-version URL. Handle unfamiliar optional fields and error codes safely. `info.version` identifies the document revision. There is no fixed deprecation deadline or API support SLA; contact us about compatibility needs.