Read API v1 reference

Public, read-only routes for a clinic's services, providers, membership plans and open times: the same information the clinic's booking and membership pages already show, with no key. Plus a secret API key for the clinic's own integration, the route that checks one, and lead routes that create a lead and read its progress with it.

Basics

GET /api/v1/catalog

The clinic (name, slug, phone, booking_mode), its appointment_types (id, name, duration_minutes, visit_mode, bookable_online), providers (id, display_name, title, appointment_type_ids), memberships.available and membership_plans (product_code, name, price_cents, interval).

GET /api/v1/availability

Query: appointment_type_id (required), date (required, YYYY-MM-DD in the clinic's time zone), provider_id (optional).

{
  "data": {
    "date": "2026-11-16",
    "duration": 30,
    "slots": [
      { "time": "09:00", "end_time": "09:30", "provider_id": "8a2b4c6d-1e3f-4a5b-8c7d-9e0f1a2b3c4d", "provider_name": "Dana Reyes", "provider_title": "NP" }
    ]
  },
  "next_cursor": null
}

GET /api/v1/availability/summary

Query: appointment_type_id (required), from and to (required, up to 62 days apart; clamped to start today), provider_id (optional). Open-time counts per day.

{ "data": { "summary": { "2026-11-16": 6, "2026-11-17": 0 } }, "next_cursor": null }

Secret API keys

For the clinic's own integration, called from a server. The clinic's owner creates a key in the EHR (Settings, Developers, API keys), chooses its permissions and how long it lasts (12 months by default, never longer), and copies it once: it is never shown again. A live clinic's key starts mck_live_, a test clinic's mck_test_.

GET /api/v1/key

Checks the key you sent: the clinic it belongs to, its prefix (as the clinic's Settings list shows it), its permissions and when it expires. Any valid key may call it; nothing else is returned.

curl -s https://api.moonshotehr.com/api/v1/key -H 'Authorization: Bearer <key>'
{
  "data": {
    "clinic": "northside",
    "key_prefix": "mck_live_3f9a2c1e",
    "scopes": ["leads:write"],
    "expires_at": "2027-10-05T14:00:00.000Z"
  },
  "next_cursor": null
}

POST /api/v1/leads

Creates a lead on the clinic's Leads board, the same way the clinic's website form does. Needs leads:write. Body (JSON): email (required), first_name, last_name or name, phone, source, service_interest, notes, attribution (an object of UTM and click-id keys). Full field rules: Send leads.

curl -s https://api.moonshotehr.com/api/v1/leads -H 'Authorization: Bearer <key>' \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: form-submission-81723' \
  -d '{ "email": "dana@example.com", "first_name": "Dana", "attribution": { "utm_source": "google" } }'
{ "data": { "id": "8d1c2b3a-4e5f-4a6b-9c7d-0e1f2a3b4c5d", "status": "new", "created_at": "2026-11-16T15:04:05.000Z", "duplicate": false, "note_saved": true }, "next_cursor": null }

PATCH /api/v1/leads/{id}

Corrects first_name, last_name, email, phone or attribution keys (the keys sent replace those keys; others are kept). Needs leads:write. Status, owner, notes and everything else the clinic's staff own are refused with 400 naming the field. Only leads this API created can be corrected; any other id is 404. Idempotency-Key is ignored here.

{ "data": { "id": "8d1c2b3a-4e5f-4a6b-9c7d-0e1f2a3b4c5d", "updated_at": "2026-11-17T09:30:00.000Z" }, "next_cursor": null }

GET /api/v1/leads/{id}/status

Three yes-or-no answers about the person behind the lead, defined as in the clinic's own conversion report: booked (an appointment booked after the lead came in), onboarding_completed (forms completed, none outstanding), member (a membership started after the lead came in). Needs leads:read. Only for leads this API created (a duplicate of a lead that came in another way answers without status); any other id is 404. No names, contact details, clinical data or amounts.

{ "data": { "id": "8d1c2b3a-4e5f-4a6b-9c7d-0e1f2a3b4c5d", "booked": true, "onboarding_completed": false, "member": false, "updated_at": "2026-11-18T14:00:00.000Z" }, "next_cursor": null }

The lead routes answer 403 phi_access_disabled until the clinic's business associate agreement with Moonshot EHR is in place, and never return an email address or phone number.

Limits

Per visitor IP address, per minute, counted separately for each route and separately from the clinic's own booking page: catalog 30, availability 60, summary 20. Keyed routes count per API key instead, per minute: key check 60, create lead 60, update lead 60, lead status 120. Exactly that many calls succeed; the next gets 429 rate_limited with Retry-After (seconds).

Error codes

CodeStatusMeaning
invalid_request400A missing or malformed parameter, or a service that cannot be booked online.
unauthorized401Keyed route: the API key is missing, malformed, unknown, expired or revoked.
online_booking_off403The clinic takes appointment requests, not online booking.
insufficient_scope403Keyed route: the API key does not have the permission this route needs.
tenant_mismatch403Keyed route: X-Tenant-Slug names a different clinic than the API key.
phi_access_disabled403Lead routes: the clinic's business associate agreement with Moonshot EHR is not in place yet.
tenant_not_found404No clinic matches X-Tenant-Slug.
not_found404No v1 route at this path, or no lead with this id at the key's clinic.
method_not_allowed405Wrong method; see the Allow header.
idempotency_conflict409Create lead: the Idempotency-Key was used with a different body, or its first request is still running.
payload_too_large413The request body is larger than 64 KB.
rate_limited429Over the limit; wait Retry-After seconds.
not_implemented501A planned route that is not available yet.
internal_error500Our side; retry shortly and quote X-Request-Id.

From the browser

Only from site addresses the clinic listed with us (see Embed and availability). Errors, including 429, are readable by those sites, and X-Request-Id and Retry-After are exposed to their JavaScript.

const res = await fetch(
  'https://api.moonshotehr.com/api/v1/availability?appointment_type_id=<id>&date=2026-11-16',
  { headers: { 'X-Tenant-Slug': '<clinic>' } },
);
const body = await res.json();
if (!res.ok) throw new Error(body.error.code + ': ' + body.error.message);
for (const slot of body.data.slots) console.log(slot.time, slot.provider_name);

From a server:

curl -s https://api.moonshotehr.com/api/v1/catalog -H 'X-Tenant-Slug: <clinic>'

Coming in v1

Available now: the secret key, the route that checks it, and the lead routes (create, correct, read progress). Planned next, on that same key (server to server only): handing a visitor off to the hosted booking, join or intake page, and reading event delivery history. Those routes are not available yet.