> ## Documentation Index
> Fetch the complete documentation index at: https://docs.maxcare.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Book an appointment

> Accepts a booking. **The EHR write is asynchronous** (changed deliberately — see below): a `202` with `syncStatus: "pending"` means the slot is RESERVED against everyone else booking through Max AI and the appointment is durably recorded, but it is NOT on the practice's calendar yet and `externalId` is null. Completion is reported by webhook: `appointment.confirmed` (written; `externalId` now known), `appointment.rejected` (the slot went away — the event carries `suggestedSlots`, so offer those rather than restarting the flow), or `appointment.write_failed` (could not be written after retries). Polling `GET /appointments/{id}` works too. **Do not tell a patient they are booked on this response alone.** This previously returned `201` after a synchronous write, which made a slow tenant a slow booking form and a vendor outage a booking outage. Booking now succeeds while the EHR is unreachable. Send an `Idempotency-Key` header — it is honoured for 24 hours per app+organization, and a completed request replays its result with `idempotentReplay: true`. If acceptance could not be confirmed, the same key remains protected for that period and returns 409 `ehr_write_uncertain` with `requiresReconciliation: true`; verify the booking before creating another. The guarantee FAILS CLOSED: if the idempotency store is unreachable the request is refused with 503 `idempotency_unavailable` and nothing is sent to the EHR, rather than proceeding with the guarantee silently withdrawn. That response is safe to retry without checking for a duplicate first. To accept at-least-once delivery instead, omit the header. Returns 409 `slot_unavailable` when capacity went away, 409 `hold_expired` when the supplied hold lapsed, and 503 `no_booking_identity` when the organization has no system connector for unattended writes.



## OpenAPI

````yaml /openapi-v1.json post /appointments
openapi: 3.0.0
info:
  title: Max AI Public API
  description: API for third-party marketplace apps
  version: '1.0'
  contact: {}
servers:
  - url: https://api.maxcare.ai/v1
security: []
tags: []
paths:
  /appointments:
    post:
      tags:
        - Appointments
      summary: Book an appointment
      description: >-
        Accepts a booking. **The EHR write is asynchronous** (changed
        deliberately — see below): a `202` with `syncStatus: "pending"` means
        the slot is RESERVED against everyone else booking through Max AI and
        the appointment is durably recorded, but it is NOT on the practice's
        calendar yet and `externalId` is null. Completion is reported by
        webhook: `appointment.confirmed` (written; `externalId` now known),
        `appointment.rejected` (the slot went away — the event carries
        `suggestedSlots`, so offer those rather than restarting the flow), or
        `appointment.write_failed` (could not be written after retries). Polling
        `GET /appointments/{id}` works too. **Do not tell a patient they are
        booked on this response alone.** This previously returned `201` after a
        synchronous write, which made a slow tenant a slow booking form and a
        vendor outage a booking outage. Booking now succeeds while the EHR is
        unreachable. Send an `Idempotency-Key` header — it is honoured for 24
        hours per app+organization, and a completed request replays its result
        with `idempotentReplay: true`. If acceptance could not be confirmed, the
        same key remains protected for that period and returns 409
        `ehr_write_uncertain` with `requiresReconciliation: true`; verify the
        booking before creating another. The guarantee FAILS CLOSED: if the
        idempotency store is unreachable the request is refused with 503
        `idempotency_unavailable` and nothing is sent to the EHR, rather than
        proceeding with the guarantee silently withdrawn. That response is safe
        to retry without checking for a duplicate first. To accept at-least-once
        delivery instead, omit the header. Returns 409 `slot_unavailable` when
        capacity went away, 409 `hold_expired` when the supplied hold lapsed,
        and 503 `no_booking_identity` when the organization has no system
        connector for unattended writes.
      operationId: AppointmentsPublicController_createAppointment
      parameters:
        - name: idempotency-key
          required: false
          in: header
          description: >-
            Client-generated UUID. Strongly recommended: on a live calendar a
            duplicate appointment is worse than a slow response. Sending it is
            also a request for exactly-once — if that cannot be honoured the
            write is refused (503 `idempotency_unavailable`) rather than
            downgraded.
          schema:
            type: string
        - name: X-Organization-Id
          in: header
          required: true
          schema:
            type: string
          description: Target clinic organization ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAppointmentBody'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateAppointmentSuccessResponse'
        '202':
          description: Accepted — queued for background processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateAppointmentSuccessResponse'
        '400':
          description: Missing or invalid request parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBadRequestResponse'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiUnauthorizedResponse'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiForbiddenResponse'
        '404':
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiNotFoundResponse'
        '409':
          description: Resource conflict (e.g. editing a signed note)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiConflictResponse'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRateLimitResponse'
        '502':
          description: EHR sync failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBadGatewayResponse'
        '503':
          description: EHR integration unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiServiceUnavailableResponse'
      security:
        - api-key: []
components:
  schemas:
    CreateAppointmentBody:
      type: object
      properties:
        patientId:
          type: string
          description: Patient ID (pat_…)
        providerId:
          type: string
          description: Provider ID (prv_…)
        facilityId:
          type: string
          description: Facility ID (fac_…)
        appointmentTypeId:
          type: string
          description: Appointment type ID (atp_…)
        visitReasonId:
          type: string
          description: >-
            Visit reason ID (vrs_…). Only meaningful on EHRs where
            /visit-reasons reports `supported: true`.
        startsAt:
          type: string
          description: Start instant, ISO-8601 with an explicit offset
          example: '2026-08-03T08:00:00-04:00'
        durationMinutes:
          type: number
          description: Appointment length in minutes
          example: 15
        newPatient:
          type: boolean
          description: >-
            Whether this is a new-patient visit. Both EHRs distinguish it, and
            it changes the EHR-side workflow.
          example: true
        holdId:
          type: string
          description: Hold ID (hld_…) taken from POST /schedule/holds
        templateId:
          type: string
          description: >-
            Availability template ID (avt_…) from the chosen slot's
            `templateIds`. On ModMed this pins the appointment to that calendar
            preference so it consumes the right window's capacity.
        notes:
          type: string
          description: Patient-supplied comment
          example: Patient comment text
        source:
          description: >-
            Booking attribution, echoed back on read. Use this instead of
            prefixing appointment notes — notes are a clinical field and get
            read aloud at check-in.
          allOf:
            - $ref: '#/components/schemas/AppointmentSourceBody'
      required:
        - patientId
        - providerId
        - facilityId
        - appointmentTypeId
        - startsAt
        - durationMinutes
        - newPatient
    CreateAppointmentSuccessResponse:
      type: object
      properties:
        code:
          type: string
          description: Response code
          example: success
        data:
          $ref: '#/components/schemas/CreateAppointmentData'
      required:
        - code
        - data
    PublicApiBadRequestResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: bad_request
        message:
          type: string
          description: Human-readable error message
          example: '''id'' must be a valid UUID'
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiUnauthorizedResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: unauthorized
        message:
          type: string
          description: Human-readable error message
          example: Invalid or missing API key
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiForbiddenResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: forbidden
        message:
          type: string
          description: Human-readable error message
          example: Insufficient scope
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiNotFoundResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: not_found
        message:
          type: string
          description: Human-readable error message
          example: Resource not found
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiConflictResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: conflict
        message:
          type: string
          description: Human-readable error message
          example: Cannot edit a signed note
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiRateLimitResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: rate_limit_exceeded
        message:
          type: string
          description: Human-readable error message
          example: Rate limit exceeded. Maximum 1000 requests per 60 seconds.
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiBadGatewayResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: unexpected_integration_error
        message:
          type: string
          description: Human-readable error message
          example: EHR sync failed
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiServiceUnavailableResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: server_unresponsive
        message:
          type: string
          description: Human-readable error message
          example: EHR integration is not available for this note
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    AppointmentSourceBody:
      type: object
      properties:
        system:
          type: string
          description: The booking system creating the appointment
          example: dcc-online-booking
        bookingReference:
          type: string
          description: Your own booking reference, echoed back on read
          example: BK-2026-000123
        campaignId:
          type: string
          description: Marketing campaign identifier
          example: google-gbp-flint
        url:
          type: string
          description: Landing URL the booking came from
          example: https://book.dccderm.com/l/flint?utm_source=gbp
      required:
        - system
    CreateAppointmentData:
      type: object
      properties:
        appointment:
          $ref: '#/components/schemas/CreatedAppointmentResponse'
        idempotentReplay:
          type: boolean
          description: >-
            Present and `true` when this response replays an earlier request
            with the same Idempotency-Key
          example: true
      required:
        - appointment
    CreatedAppointmentResponse:
      type: object
      properties:
        id:
          type: string
          example: apt_e5f6a7b8c9d04e1f2a3b4c5d6e7f8a9b
        externalId:
          type: string
          description: >-
            The EHR's own appointment identifier. **Always `null` on this
            endpoint's response**, because the EHR write is asynchronous and the
            vendor has not issued an id yet. It arrives on the
            `appointment.confirmed` webhook, and on any `GET /appointments` once
            the write has landed.
          nullable: true
          example: null
        patientId:
          type: string
          example: pat_c56103bcd39c46d39f3138dd2b5e05f6
        providerId:
          type: string
          example: prv_35103413aaaa4bbbccccddddeeeeffff
        facilityId:
          type: string
          example: fac_2706aaaa4bbbccccddddeeeeffff1111
        appointmentTypeId:
          type: string
          example: atp_1c0f8a2b4d5e4f6a8b9c0d1e2f3a4b5c
        visitReasonId:
          type: string
          nullable: true
        startsAt:
          type: string
          example: '2026-08-03T08:00:00-04:00'
        durationMinutes:
          type: number
          example: 15
        status:
          type: string
          example: scheduled
        syncStatus:
          type: string
          description: >-
            Always `pending` on a `202` from THIS endpoint. The EHR write is
            ASYNCHRONOUS: the slot is reserved and the appointment is durably
            recorded, but it is not on the practice's calendar yet. You SHOULD
            build a state machine over this — subscribe to
            `appointment.confirmed` (it landed, `externalId` is now known),
            `appointment.rejected` (the slot went to someone else), and
            `appointment.write_failed` (it could not be written after retries).
            Polling `GET /appointments/{id}` works too. Do not tell a patient
            they are booked on this response alone. `appointment.rejected`
            carries `suggestedSlots` — up to five nearby bookable starts for the
            same provider, facility and appointment type, computed the same way
            `GET /schedule/slots` computes them. Offer them directly rather than
            sending the patient back to the start: a lost slot is rare, and it
            is recoverable in one tap if your UI is ready for it. An empty
            `suggestedSlots` means nothing is free within a week, which is worth
            saying plainly instead of showing a spinner.
          example: pending
          enum:
            - pending
            - syncing
            - synced
            - failed
        newPatient:
          type: boolean
          example: true
        notes:
          type: string
          nullable: true
        source:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/AppointmentSourceResponse'
        createdAt:
          type: string
          example: '2026-07-26T13:00:00.000Z'
      required:
        - id
        - externalId
        - patientId
        - providerId
        - facilityId
        - appointmentTypeId
        - visitReasonId
        - startsAt
        - durationMinutes
        - status
        - syncStatus
        - newPatient
        - notes
        - source
        - createdAt
    AppointmentSourceResponse:
      type: object
      properties:
        system:
          type: string
          example: dcc-online-booking
        bookingReference:
          type: string
          nullable: true
          example: BK-2026-000123
        campaignId:
          type: string
          nullable: true
          example: google-gbp-flint
        url:
          type: string
          nullable: true
          example: https://book.dccderm.com/l/flint?utm_source=gbp
      required:
        - system
        - bookingReference
        - campaignId
        - url
  securitySchemes:
    api-key:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: Marketplace API key

````