# Claro public API > Claro is a clinical platform for aesthetic and medical clinics in Australia and the US. Its public API is a stable, read-only (for now) REST contract for partners, a clinic's own developers and AI agents: clinics, practitioners, patients and appointments. The full contract is the OpenAPI document at /api/public/v1/openapi.json; this file is the same thing in prose. ## Basics - Base path: /api/public/v1 on the realm's host: https://api-au.claro.clinic for AU, https://api-us.claro.clinic for the US. Each realm (AU, US) is a separate deployment and database. A credential, and every id it sees, belongs to one realm. Every response names its realm in the x-claro-realm header. - Authentication: `Authorization: Bearer `, where the credential is an API key (claro_sk_live_au_… or claro_sk_live_us_…, or claro_sk_test_… on the sandbox; older keys start claro_k) or an OAuth access token. A signed-in app session is refused. A test key is refused on a live deployment and a live key on the sandbox. - An API key names its realm in its prefix (au or us). Sent to the other realm's host, it is refused before anything is looked up with 421 wrong_region; param is the base URL to use instead. Nothing is wrong with the key: send the same request there. - Version: every response carries Claro-Version (currently 2026-11-01). Additive changes (new fields, new string values) arrive without a new version: ignore fields you do not know, and treat an unknown string value as the documented default. - One clinic per request: send the clinic's id (from GET /clinics) in the Claro-Clinic header. A credential that reaches exactly one clinic, which every API key does, may leave it out. A credential that reaches several and leaves it out gets 400 clinic_required. - Ids are opaque prefixed strings: clinic_…, org_…, prac_…, pat_…, appt_…. Store and compare them as strings; never parse them. Each object also carries external_ids, the ids this clinic has linked it to in other systems, as [{source, id}]. ## Data classes and scopes - Operational (the resource's own scope): clinics (any credential), practitioners (practitioners:read), appointments (appointments:read): times, status, practitioners, type. - Clinical metadata (patients:read): patients, and an appointment's patient. - Events (events:read): what one of the clinic's webhook subscriptions is sent. Who the patient is, in an event, needs patients:read too. - Clinical content (clinical_content:read AND the clinic's opt-in for your app): an appointment's notes; in events, the extra detail a subscription that takes clinical content is sent. - A field you may not see is absent from the object, not null. - Government identifiers (IHI, Medicare, DVA, provider and prescriber numbers) are never returned. - Every read is audited and visible to the clinic. ## Endpoints - GET /api/public/v1/clinics: the clinics this credential reaches. - GET /api/public/v1/clinics/{id}: one clinic. - GET /api/public/v1/practitioners: practitioners and prescribers at the clinic. - GET /api/public/v1/practitioners/{id}: one practitioner. - GET /api/public/v1/patients: active patients at the clinic, oldest change first. - GET /api/public/v1/patients/{id}: one patient. - GET /api/public/v1/appointments: appointments at the clinic, every status, oldest change first. - GET /api/public/v1/appointments/{id}: one appointment. - GET /api/public/v1/events: one webhook subscription's events (subscription=whsub_…, required), oldest first, to catch up on missed deliveries. - GET /api/public/v1/openapi.json: the OpenAPI document (no credential needed). - GET /api/public/v1/llms.txt: this file (no credential needed). ## Lists and syncing - A list is {"object": "list", "data": [...], "has_more": bool, "next_cursor": string or null}. - limit is 1 to 100 (default 50). Pass next_cursor back as cursor for the next page. A cursor belongs to one list. - Patients and appointments accept updated_since (ISO 8601 with an offset) and run oldest change first: walk every page, keep the newest updated_at you saw, and next time ask for updated_since that value. Clinics and practitioners run by id and refuse updated_since (400 parameter_unsupported). ## Errors - Every error is {"error": {"type", "code", "message", "param", "request_id"}}. Branch on type and code, never on message. Quote request_id to support. - Types: authentication_error (401), permission_error (403), invalid_request_error (400, 404, 405, 421), idempotency_error (409), rate_limit_error (429), api_error (500). - An API key from the other realm is 421 wrong_region, with the right base URL in param (https://api-au.claro.clinic or https://api-us.claro.clinic). - A path that does not exist is 404 route_not_found. A method a path does not take is 405 method_not_allowed, with an Allow header. ## Rate limits and retries - Each credential has its own limit per 60-second window. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds) and RateLimit-Policy. A 429 also carries Retry-After. - POST requests accept an Idempotency-Key header (up to 255 characters, kept 24 hours). The same key with the same request replays the first response (Idempotent-Replayed: true); with a different request it is a 409 idempotency_key_reused. There are no POST endpoints in this version. ## Webhooks and events - The clinic sets up webhook subscriptions (or an app does, through the webhook API with webhooks:manage). Deliveries follow Standard Webhooks: webhook-id, webhook-timestamp and webhook-signature headers. - Every payload names its objects by public id, beside the internal references it has always carried: clinic_public_id (beside clinic_id), patient_public_id (beside patient_uuid), appointment_public_id (beside appointment_id, or appointment_uuid on a sale) and practitioner_public_id (beside practitioner_id). Fetch the record with GET /clinics/{id}, /patients/{id}, /appointments/{id} or /practitioners/{id}. A public id is null when its reference is. - Order is not guaranteed and a delivery can repeat: dedupe on webhook-id, and re-fetch the record rather than trust the payload. - GET /events?subscription=whsub_… lists what that subscription is sent. Each item is exactly what its delivery carries, plus id (the delivery's webhook-id). Filters: type (comma-separated, each one the subscription takes), since (ISO 8601 with an offset), cursor and limit (1 to 100). next_cursor is returned on the last page too: keep it and poll with it. The subscription's id is its public_id in the webhook API.