{
  "openapi": "3.1.0",
  "info": {
    "title": "Moonshot EHR partner API",
    "version": "1.0.0",
    "summary": "Read a clinic's public booking catalog and availability, check a clinic's API key, and create leads and read their progress with it.",
    "description": "Public, read-only routes for a clinic's own website or its web team. They return the same information the clinic's public booking and membership pages already show: nothing here identifies a patient, and nothing here signs anyone in or writes anything.\n\nKeyed routes use the clinic's secret API key, server to server: `GET /api/v1/key` checks a key, and the lead routes create a lead, correct its contact details and read its progress (see Leads below). Those are the only routes that write anything, and only to the clinic's own Leads board.\n\n## Choosing the clinic\nSend the clinic's identifier in the `X-Tenant-Slug` header. The API host serves every clinic, so a request without it (or with an unknown one) answers `404 tenant_not_found`.\n\n## Responses\nSuccess: `{ \"data\": ..., \"next_cursor\": null }`. `next_cursor` is always present; it is `null` on every route in this version because none of them paginate.\n\nError: `{ \"error\": { \"code\", \"message\", \"request_id\" } }`. `code` is stable and safe to branch on; `message` is a sentence for a person and may change. Every response carries `X-Request-Id`; quote it when you contact us about a request.\n\nResponses are `Cache-Control: no-store`. Cache the catalog in your own site for a few minutes; do not poll.\n\n## Dates\nDates are `YYYY-MM-DD` real calendar days; anything else is `400 invalid_request`. Availability is only ever reported from today on, in the clinic's time zone: a past `date` answers no slots, and a summary range is clamped to start today (a range wholly in the past is an empty summary).\n\n## Rate limits\nLimits are per visitor IP address, per minute, counted separately for each route and separately from the clinic's own booking page: catalog 30, availability 60, availability summary 20. Exactly that many calls in a minute succeed; the next answers `429 rate_limited` with `Retry-After` (seconds). Per API key, per minute: key 60, create lead 60, update lead 60, lead status 120.\n\n## Methods and paths\nEvery `GET` route also answers `HEAD` (same status and headers, no body). `POST /api/v1/leads` and `PATCH /api/v1/leads/{id}` take a JSON body (`Content-Type: application/json`, at most 64 KB; a larger body answers `413 payload_too_large`). A trailing slash is ignored (`/api/v1/catalog/` is `/api/v1/catalog`).\n\n## Calling from a browser\nJavaScript on the clinic's own website may call these routes once the clinic has listed that site's address with us (the same list that allows embedding the booking page). GET only, and never with `credentials: 'include'`. Error responses (including `429`) are readable by that site too, and `X-Request-Id` and `Retry-After` are exposed to its JavaScript. Other sites, and other clinics' sites, get no cross-origin access.\n\n## Secret API keys\nThe clinic's owner creates a key in the EHR under Settings, Developers, API keys, chooses its permissions and expiry (up to 12 months), and copies it once: it is never shown again. Send it as `Authorization: Bearer <key>`. A live clinic's key starts `mck_live_`, a test clinic's `mck_test_`.\n\n- The key decides the clinic. `X-Tenant-Slug` is not needed; if you send one that names a different clinic, the answer is `403 tenant_mismatch`.\n- A missing, malformed, unknown, expired or revoked key answers `401 unauthorized`, the same way every time. A key without a route's permission answers `403 insufficient_scope`.\n- Keyed routes are server to server only: they send no CORS headers, so a browser cannot read them. Never put a key in a web page or an app.\n- To rotate, create a second key, move your integration to it, then revoke the first. Revoking takes effect on the next request.\n\nPermissions a key can carry: `leads:write`, `leads:read`, `handoffs:write`, `events:read`. The lead routes use `leads:write` (create, update) and `leads:read` (status). The other two are there so a key made today works with the routes listed below when they are released.\n\n## Leads\nServer to server, with a key that has `leads:write` (create, update) or `leads:read` (status). The clinic must have its business associate agreement with Moonshot EHR in place; until then these routes answer `403 phi_access_disabled`.\n\n- **Create** (`POST /api/v1/leads`): `email` is required. Optional: `first_name`, `last_name` (or `name`, split at the first space), `phone`, `source` (your words; mapped to the clinic's source list), `service_interest` (up to 200 characters), `notes` (what the visitor typed, up to 4,000 characters; kept as a note on the lead) and `attribution` (an object of `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `gclid`, `fbclid`, `msclkid`, `landing_page`, `last_page`, `referrer`, `quiz_source`, `session_started_at`; strings up to 256 characters). Any other field is `400 invalid_request` and names it; nothing is silently dropped. The email address is stored in lower case.\n- **Duplicates:** a lead with the same email address (any letter case) that came in during the last 24 hours (through this API or the clinic's website form) is the same lead. The answer is `200` with `duplicate: true` and that lead's id; no second lead is created, and `notes` are added to it once. `note_saved` says whether the notes you sent are on the lead.\n- **Idempotency-Key** (optional header on create only; ignored on update; 1 to 255 visible characters): the same key with the same body within 24 hours returns the original answer and changes nothing; the same key with a different body is `409 idempotency_conflict`, as is a second request while the first with that key is still running.\n- **Update** (`PATCH /api/v1/leads/{id}`): corrects `first_name`, `last_name`, `email`, `phone` and `attribution` keys only. The lead's status, owner, notes and everything else the clinic's staff work with are not changeable here: sending one of them is `400 invalid_request` naming it.\n- **Status** (`GET /api/v1/leads/{id}/status`): `booked` (an appointment was booked after the lead came in, even if later cancelled or missed), `onboarding_completed` (the person has completed their forms and has none outstanding) and `member` (a membership started after the lead came in). The same definitions as the clinic's own conversion report. No names, contact details, clinical data or amounts.\n- Update and status work only for leads this API created; a duplicate of a lead that came in another way answers without `status` and stays unreadable. Any other id, including a lead the clinic's staff or website form entered, answers `404 not_found`, the same as an id that does not exist.\n- Responses never contain an email address or a phone number.\n\n## Coming in v1 (not available yet)\nThese are planned next and are listed so you can plan for them. They will use the secret key above and will only ever be called from a server, never from a browser. Until they are released, any such path answers `501 not_implemented` or `404 not_found`.\n- Hand a visitor off to the clinic's hosted booking, membership join or intake page.\n- Read the clinic's webhook event history and delivery outcomes.\n\nPatient records and clinical data are not part of this API and are not planned for it."
  },
  "servers": [
    {
      "url": "https://api.moonshotehr.com",
      "description": "Moonshot EHR API"
    },
    {
      "url": "https://api.moonshotclinic.com",
      "description": "Same API, original domain"
    }
  ],
  "tags": [
    {
      "name": "Catalog",
      "description": "What the clinic offers online."
    },
    {
      "name": "Availability",
      "description": "Open appointment times."
    },
    {
      "name": "Keys",
      "description": "Check the clinic's secret API key."
    },
    {
      "name": "Leads",
      "description": "Create a lead on the clinic's Leads board and read its progress."
    }
  ],
  "paths": {
    "/api/v1/catalog": {
      "get": {
        "operationId": "getCatalog",
        "tags": [
          "Catalog"
        ],
        "summary": "Services, providers and membership plans the clinic offers online",
        "description": "Built from the clinic's public booking services list and its public membership plan list. A clinic that takes appointment requests instead of online booking (`clinic.booking_mode` = `request`) returns empty `appointment_types` and `providers`, except for any types the clinic still keeps open online. `memberships.available` is false when the clinic does not sell memberships online (or the plan list could not be read just now); `membership_plans` is then empty. A service with `bookable_online: false` is offered only by video and is booked through the patient portal; `/availability` refuses it.",
        "parameters": [
          {
            "name": "X-Tenant-Slug",
            "in": "header",
            "required": false,
            "description": "The clinic's identifier (its subdomain label, for example `northside`). Required on api.moonshotehr.com and api.moonshotclinic.com, which serve every clinic. Letters, digits and hyphens, up to 80 characters. When present it is the whole answer: an identifier that does not match a clinic is a 404, never a fallback to another clinic.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9-]{1,80}$"
            },
            "example": "northside"
          }
        ],
        "responses": {
          "200": {
            "description": "The catalog.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogResponse"
                },
                "example": {
                  "data": {
                    "clinic": {
                      "name": "Northside Health",
                      "slug": "northside",
                      "phone": "(312) 555-0100",
                      "booking_mode": "self_book"
                    },
                    "appointment_types": [
                      {
                        "id": "3f1c9a52-7d4e-4b8a-9c21-5e6f7a8b9c0d",
                        "name": "New Patient Consult",
                        "duration_minutes": 30,
                        "visit_mode": "in_person",
                        "bookable_online": true
                      }
                    ],
                    "providers": [
                      {
                        "id": "8a2b4c6d-1e3f-4a5b-8c7d-9e0f1a2b3c4d",
                        "display_name": "Dana Reyes",
                        "title": "NP",
                        "appointment_type_ids": [
                          "3f1c9a52-7d4e-4b8a-9c21-5e6f7a8b9c0d"
                        ]
                      }
                    ],
                    "memberships": {
                      "available": true
                    },
                    "membership_plans": [
                      {
                        "product_code": "CORE_MONTHLY",
                        "name": "Core Membership",
                        "price_cents": 19900,
                        "interval": "month"
                      }
                    ]
                  },
                  "next_cursor": null
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/TenantNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/availability": {
      "get": {
        "operationId": "getAvailability",
        "tags": [
          "Availability"
        ],
        "summary": "Open times for one service on one day",
        "description": "Exactly the times the clinic's booking page offers for that day, in the clinic's local time. Times already past, inside the clinic's minimum notice, or outside provider hours are not listed. A service that is offered only by video is booked through the patient portal and answers `400 invalid_request`.",
        "parameters": [
          {
            "name": "X-Tenant-Slug",
            "in": "header",
            "required": false,
            "description": "The clinic's identifier (its subdomain label, for example `northside`). Required on api.moonshotehr.com and api.moonshotclinic.com, which serve every clinic. Letters, digits and hyphens, up to 80 characters. When present it is the whole answer: an identifier that does not match a clinic is a 404, never a fallback to another clinic.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9-]{1,80}$"
            },
            "example": "northside"
          },
          {
            "name": "appointment_type_id",
            "in": "query",
            "required": true,
            "description": "An `appointment_types[].id` from the catalog.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "3f1c9a52-7d4e-4b8a-9c21-5e6f7a8b9c0d"
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "The day, in the clinic's time zone.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-11-16"
          },
          {
            "name": "provider_id",
            "in": "query",
            "required": false,
            "description": "Only this provider (a `providers[].id`). Omit for every provider who offers the service.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "8a2b4c6d-1e3f-4a5b-8c7d-9e0f1a2b3c4d"
          }
        ],
        "responses": {
          "200": {
            "description": "Open times. `duration` is the visit length in minutes; each slot carries its own end time.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityResponse"
                },
                "example": {
                  "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
                }
              }
            }
          },
          "400": {
            "description": "A missing or malformed parameter, or a service that cannot be booked online.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "appointment_type_id is required",
                    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/OnlineBookingOff"
          },
          "404": {
            "$ref": "#/components/responses/TenantNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/availability/summary": {
      "get": {
        "operationId": "getAvailabilitySummary",
        "tags": [
          "Availability"
        ],
        "summary": "How many open times each day has, over a date range",
        "description": "For a calendar view. One request covers at most 62 days, starting no earlier than today. Days are keyed `YYYY-MM-DD` in the clinic's time zone; a day with no openings is 0 or absent.",
        "parameters": [
          {
            "name": "X-Tenant-Slug",
            "in": "header",
            "required": false,
            "description": "The clinic's identifier (its subdomain label, for example `northside`). Required on api.moonshotehr.com and api.moonshotclinic.com, which serve every clinic. Letters, digits and hyphens, up to 80 characters. When present it is the whole answer: an identifier that does not match a clinic is a 404, never a fallback to another clinic.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9-]{1,80}$"
            },
            "example": "northside"
          },
          {
            "name": "appointment_type_id",
            "in": "query",
            "required": true,
            "description": "An `appointment_types[].id` from the catalog.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "3f1c9a52-7d4e-4b8a-9c21-5e6f7a8b9c0d"
          },
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "First day of the range.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-11-16"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "Last day of the range (inclusive).",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-11-30"
          },
          {
            "name": "provider_id",
            "in": "query",
            "required": false,
            "description": "Only this provider.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "8a2b4c6d-1e3f-4a5b-8c7d-9e0f1a2b3c4d"
          }
        ],
        "responses": {
          "200": {
            "description": "Open-time counts per day.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilitySummaryResponse"
                },
                "example": {
                  "data": {
                    "summary": {
                      "2026-11-16": 6,
                      "2026-11-17": 0,
                      "2026-11-18": 4
                    }
                  },
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "A missing or malformed parameter, a range over 62 days, or a service that cannot be booked online.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "Date range cannot exceed 62 days",
                    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/OnlineBookingOff"
          },
          "404": {
            "$ref": "#/components/responses/TenantNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/key": {
      "get": {
        "operationId": "getKey",
        "tags": [
          "Keys"
        ],
        "summary": "The clinic, prefix, scopes and expiry of the API key you sent",
        "description": "Check a key before you build on it: which clinic it belongs to, its prefix, its permissions and when it expires. Needs no permission; any valid key may call it. The answer is about the key you sent and nothing else.",
        "security": [
          {
            "partnerKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The key.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyResponse"
                },
                "example": {
                  "data": {
                    "clinic": "northside",
                    "key_prefix": "mck_live_3f9a2c1e",
                    "scopes": [
                      "leads:write"
                    ],
                    "expires_at": "2027-10-05T14:00:00.000Z"
                  },
                  "next_cursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/KeyForbidden"
          },
          "429": {
            "$ref": "#/components/responses/KeyRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/leads": {
      "post": {
        "operationId": "createLead",
        "tags": [
          "Leads"
        ],
        "summary": "Create a lead on the clinic's Leads board",
        "description": "Creates the lead exactly as the clinic's website form does: same source mapping, same duplicate rule, same note from the form. A lead with the same email address from the last 24 hours is answered `200` with `duplicate: true` and no new lead. Needs `leads:write`.",
        "security": [
          {
            "partnerKey": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional, honored on this route only (PATCH ignores it). The same key and body within 24 hours return the original answer; the same key with a different body, or while the first request is still running, is `409 idempotency_conflict`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7E]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadCreateRequest"
              },
              "example": {
                "email": "dana@example.com",
                "first_name": "Dana",
                "last_name": "Prospect",
                "phone": "612-555-0142",
                "source": "website",
                "service_interest": "Weight loss consult",
                "notes": "Mornings work best.",
                "attribution": {
                  "utm_source": "google",
                  "utm_campaign": "fall"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The same lead came in during the last 24 hours. No new lead was created.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadResponse"
                },
                "example": {
                  "data": {
                    "id": "8d1c2b3a-4e5f-4a6b-9c7d-0e1f2a3b4c5d",
                    "status": "new",
                    "created_at": "2026-11-16T15:04:05.000Z",
                    "duplicate": true,
                    "note_saved": false
                  },
                  "next_cursor": null
                }
              }
            }
          },
          "201": {
            "description": "Created.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadResponse"
                },
                "example": {
                  "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
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LeadInvalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LeadForbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/KeyRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/leads/{id}": {
      "patch": {
        "operationId": "updateLead",
        "tags": [
          "Leads"
        ],
        "summary": "Correct a lead's contact or attribution fields",
        "description": "Changes only `first_name`, `last_name`, `email`, `phone` and attribution keys. The clinic's staff own everything else on the lead; sending one of those fields is a 400 that names it. Needs `leads:write`. Works only for a lead this API created; any other id is 404. `Idempotency-Key` is ignored here.",
        "security": [
          {
            "partnerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The lead id from the create response.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadUpdateRequest"
              },
              "example": {
                "phone": "612-555-0199",
                "attribution": {
                  "utm_campaign": "spring"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadUpdateResponse"
                },
                "example": {
                  "data": {
                    "id": "8d1c2b3a-4e5f-4a6b-9c7d-0e1f2a3b4c5d",
                    "updated_at": "2026-11-17T09:30:00.000Z"
                  },
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LeadInvalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LeadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/LeadNotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/KeyRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/leads/{id}/status": {
      "get": {
        "operationId": "getLeadStatus",
        "tags": [
          "Leads"
        ],
        "summary": "A lead's progress: booked, onboarding finished, member",
        "description": "Three yes-or-no answers about the person behind the lead, with the same definitions as the clinic's own conversion report. No names, contact details, clinical data or amounts. Needs `leads:read`. Only for a lead this API created; any other id is 404.",
        "security": [
          {
            "partnerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The lead id from the create response.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The lead's progress.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadStatusResponse"
                },
                "example": {
                  "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
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LeadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/LeadNotFound"
          },
          "429": {
            "$ref": "#/components/responses/KeyRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RequestId": {
        "description": "Identifies this request in our logs. Quote it when you contact us.",
        "schema": {
          "type": "string"
        }
      },
      "CacheControl": {
        "description": "Always `no-store`.",
        "schema": {
          "type": "string",
          "const": "no-store"
        }
      }
    },
    "responses": {
      "TenantNotFound": {
        "description": "No clinic matches the X-Tenant-Slug header (or the host).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "tenant_not_found",
                "message": "No clinic matches this host or X-Tenant-Slug header.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "OnlineBookingOff": {
        "description": "The clinic takes appointment requests, not online booking.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "online_booking_off",
                "message": "This clinic is not taking online bookings right now. Please contact the clinic. If you are already a patient, sign in to the patient portal to request a visit.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests from this IP address. Wait `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          },
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests. Try again later.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Something went wrong on our side.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "Something went wrong. Try again shortly.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The key is missing, malformed, unknown, expired or revoked. One answer for all of them.",
        "headers": {
          "WWW-Authenticate": {
            "description": "`Bearer realm=\"moonshot-ehr-api\"`.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Send a valid API key as Authorization: Bearer <key>.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "KeyForbidden": {
        "description": "`tenant_mismatch`: `X-Tenant-Slug` names a different clinic than the key. `insufficient_scope`: the key lacks the route's permission.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "tenant_mismatch",
                "message": "X-Tenant-Slug names a different clinic than this API key. Leave it out: the key decides the clinic.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "KeyRateLimited": {
        "description": "Too many requests with this key. Wait `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          },
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests. Try again later.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "LeadInvalid": {
        "description": "A field is missing, malformed, not accepted on this route, or kept by the clinic's staff. The message names it.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "These fields are kept by the clinic's staff and cannot be changed through the API: \"status\".",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "LeadForbidden": {
        "description": "`insufficient_scope`: the key lacks the route's permission. `tenant_mismatch`: `X-Tenant-Slug` names a different clinic than the key. `phi_access_disabled`: the clinic's business associate agreement with Moonshot EHR is not in place yet.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "insufficient_scope",
                "message": "This API key does not have the leads:write permission.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "LeadNotFound": {
        "description": "No lead with this id that this clinic's API created.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "No lead with this id.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "The Idempotency-Key was used in the last 24 hours with a different body, or a request with it is still running.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "idempotency_conflict",
                "message": "This Idempotency-Key was already used with a different request body in the last 24 hours.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The body is larger than 64 KB.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "payload_too_large",
                "message": "The request body is larger than 65536 bytes.",
                "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "request_id"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "tenant_not_found",
                  "not_found",
                  "method_not_allowed",
                  "online_booking_off",
                  "rate_limited",
                  "not_implemented",
                  "internal_error",
                  "unauthorized",
                  "insufficient_scope",
                  "tenant_mismatch",
                  "payload_too_large",
                  "idempotency_conflict",
                  "phi_access_disabled"
                ]
              },
              "message": {
                "type": "string"
              },
              "request_id": {
                "type": "string"
              }
            }
          }
        }
      },
      "CatalogResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Always null in this version (no route paginates)."
          },
          "data": {
            "type": "object",
            "required": [
              "clinic",
              "appointment_types",
              "providers",
              "memberships",
              "membership_plans"
            ],
            "properties": {
              "clinic": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "slug": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "booking_mode": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "`self_book` (online booking) or `request` (the clinic takes requests)."
                  }
                }
              },
              "appointment_types": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    },
                    "duration_minutes": {
                      "type": "integer"
                    },
                    "visit_mode": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The visit mode the clinic set for the service, for example `in_person`, `telehealth` or `both`."
                    },
                    "bookable_online": {
                      "type": "boolean",
                      "description": "False for a video-only service: it is booked through the patient portal and `/availability` refuses it."
                    }
                  }
                }
              },
              "providers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "display_name": {
                      "type": "string"
                    },
                    "title": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "appointment_type_ids": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "description": "Services this provider offers. Empty means every service."
                    }
                  }
                }
              },
              "memberships": {
                "type": "object",
                "properties": {
                  "available": {
                    "type": "boolean"
                  }
                }
              },
              "membership_plans": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "product_code": {
                      "type": "string",
                      "description": "The plan identifier. Use it in a membership join link: `/join/?plan=<product_code>`."
                    },
                    "name": {
                      "type": "string"
                    },
                    "price_cents": {
                      "type": "integer"
                    },
                    "interval": {
                      "type": "string",
                      "description": "Billing cadence. Known values today: `month`, `quarter`, `year`; treat any other value as a cadence to display, not an error."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AvailabilityResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Always null in this version (no route paginates)."
          },
          "data": {
            "type": "object",
            "properties": {
              "date": {
                "type": "string",
                "format": "date"
              },
              "duration": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Visit length in minutes; null when no provider offers the service that day."
              },
              "slots": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "time": {
                      "type": "string",
                      "description": "Start, `HH:MM`, clinic local time."
                    },
                    "end_time": {
                      "type": "string",
                      "description": "End, `HH:MM`, clinic local time."
                    },
                    "provider_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "provider_name": {
                      "type": "string"
                    },
                    "provider_title": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AvailabilitySummaryResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Always null in this version (no route paginates)."
          },
          "data": {
            "type": "object",
            "properties": {
              "summary": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "KeyResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "clinic",
              "key_prefix",
              "scopes",
              "expires_at"
            ],
            "properties": {
              "clinic": {
                "type": "string",
                "description": "The clinic's identifier (the same value `X-Tenant-Slug` takes)."
              },
              "key_prefix": {
                "type": "string",
                "description": "The start of the key, as the clinic's Settings list shows it."
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "leads:write",
                    "leads:read",
                    "handoffs:write",
                    "events:read"
                  ]
                }
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "LeadAttribution": {
        "type": "object",
        "additionalProperties": false,
        "description": "Campaign and click attribution. Only these keys; each a string of up to 256 characters.",
        "properties": {
          "utm_source": {
            "type": "string",
            "maxLength": 256
          },
          "utm_medium": {
            "type": "string",
            "maxLength": 256
          },
          "utm_campaign": {
            "type": "string",
            "maxLength": 256
          },
          "utm_content": {
            "type": "string",
            "maxLength": 256
          },
          "utm_term": {
            "type": "string",
            "maxLength": 256
          },
          "gclid": {
            "type": "string",
            "maxLength": 256
          },
          "fbclid": {
            "type": "string",
            "maxLength": 256
          },
          "msclkid": {
            "type": "string",
            "maxLength": 256
          },
          "landing_page": {
            "type": "string",
            "maxLength": 256
          },
          "last_page": {
            "type": "string",
            "maxLength": 256
          },
          "referrer": {
            "type": "string",
            "maxLength": 256
          },
          "quiz_source": {
            "type": "string",
            "maxLength": 256
          },
          "session_started_at": {
            "type": "string",
            "maxLength": 256
          }
        }
      },
      "LeadCreateRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Stored in lower case."
          },
          "first_name": {
            "type": "string",
            "maxLength": 255
          },
          "last_name": {
            "type": "string",
            "maxLength": 255
          },
          "name": {
            "type": "string",
            "maxLength": 255,
            "description": "A full name, split at the first space. `first_name` and `last_name` win when sent."
          },
          "phone": {
            "type": "string",
            "maxLength": 40,
            "description": "Digits with optional spaces, `+`, `-`, `.` and parentheses; 10 to 15 digits."
          },
          "source": {
            "type": "string",
            "maxLength": 64,
            "description": "Where the lead came from, in your words. Mapped to the clinic's source list."
          },
          "service_interest": {
            "type": "string",
            "maxLength": 200,
            "description": "The service the visitor asked about."
          },
          "notes": {
            "type": "string",
            "maxLength": 4000,
            "description": "What the visitor typed. Kept on the lead as a note from the form, as plain text."
          },
          "attribution": {
            "$ref": "#/components/schemas/LeadAttribution"
          }
        }
      },
      "LeadUpdateRequest": {
        "type": "object",
        "additionalProperties": false,
        "minProperties": 1,
        "description": "Contact and attribution fields only. Sending `status`, `assigned_to`, `notes` or any other field the clinic's staff own is a 400 that names it.",
        "properties": {
          "first_name": {
            "type": "string",
            "maxLength": 255
          },
          "last_name": {
            "type": "string",
            "maxLength": 255
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "maxLength": 40
          },
          "attribution": {
            "$ref": "#/components/schemas/LeadAttribution",
            "description": "The keys sent replace those keys; other stored keys are kept."
          }
        }
      },
      "LeadResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "id",
              "created_at",
              "duplicate",
              "note_saved"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string",
                "description": "The lead's stage on the clinic's board: usually `new`, or `converted` when the person is already the clinic's patient. Absent on a duplicate of a lead this API did not create."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "duplicate": {
                "type": "boolean",
                "description": "True when a lead with this email address (any letter case) came in during the last 24 hours, through this API or the website form, and no new lead was created."
              },
              "note_saved": {
                "type": "boolean",
                "description": "True when the `notes` you sent are on the lead (saved now, or already there with the same text). False when no notes were sent or they could not be saved; the lead is created either way."
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "LeadUpdateResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "id",
              "updated_at"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "LeadStatusResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "id",
              "booked",
              "onboarding_completed",
              "member",
              "updated_at"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "booked": {
                "type": "boolean",
                "description": "An appointment was booked after the lead came in (a later cancellation or no-show still counts)."
              },
              "onboarding_completed": {
                "type": "boolean",
                "description": "The person has completed at least one form and has none outstanding."
              },
              "member": {
                "type": "boolean",
                "description": "A membership started after the lead came in."
              },
              "updated_at": {
                "type": "string",
                "format": "date-time",
                "description": "The latest change behind these answers."
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    },
    "securitySchemes": {
      "partnerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A clinic's secret API key (`mck_live_...` or `mck_test_...`), created by the clinic's owner in the EHR. Server to server only."
      }
    }
  }
}
