openapi: 3.0.3
info:
  version: "1"
  title: MeetOne Scheduling API v1
  description: >
    The Scheduling API runs organizations and appointment scheduling: organizations,
    users, appointment resources, availability types, availabilities, free slots,
    appointments, and the treatment waitlist.


    ## Authentication


    Every request carries an API token in the `Authorization` header:
    `Authorization: Bearer <token>`. A missing, unknown or expired token answers `401`.


    There are two kinds of token:


    - **Instance token** (created by an administrator under Developer → API Tokens):
      acts across the whole installation. On installations with organizations it names
      the organization with `organization_id` or `organization_external_id`. Creating
      anything requires it; on lists it filters; on a single record it is optional and
      must match the record.

    - **Organization token** (shown to the organization owner under Integrations, or
      created by an administrator for the organization): the organization is implicit.
      Naming any other organization answers `403`, and records of other organizations
      answer `404` as if they did not exist.


    A token is either *read & write* or *read only*. A read-only token can call the
    `GET` endpoints only; everything else answers `403` with the code `read_only_token`.


    ## Identifiers


    Every record has an `id`, a UUID assigned by MeetOne, and an optional `external_id`
    you set to your own key. External IDs are unique within an organization (for
    organizations themselves: within the installation). Every reference in a request
    accepts either form: `doctor_id` **or** `doctor_external_id`, `patient_id` **or**
    `patient_external_id`, and so on. Sending both answers `422`. A reference to a
    record that does not exist, or that belongs to another organization, answers `404`.
    Every list can be filtered by `external_id`, except waitlist entries, which have
    none.


    ## Times


    Times are ISO 8601 with an offset, e.g. `2026-10-08T14:30:00+02:00`. A time without
    an offset is read in `Europe/Berlin`, the time zone every response uses.


    ## Lists, pagination and sync


    Lists answer `{ "data": [ ... ] }`, single records `{ "data": { ... } }`. Lists are
    paginated with `page` (from 1) and `per_page` (default 50, at most 100). The
    response headers carry `total-count`, `total-pages`, `current-page`, `page-limit`
    and an RFC 8288 `link` header.


    Every list accepts `updated_since`: it returns the records changed at or after that
    time, ordered by their last change. Such a sync pages by cursor instead of by number:
    follow the `next` URL of the `link` header until there is none. Its `page` is an
    opaque cursor, and there is no `total-count`. A record that changes while you page
    comes again further on, so store records by `id`. Next time, pass the `updated_at` of
    the last record you processed, or a minute earlier to be safe: what comes again is
    simply stored again.


    ## Errors


    Errors answer JSON in one format, with English messages:


    ```json

    { "errors": [{ "code": "taken", "attribute": "external_id", "message": "External ID has already been taken" }] }

    ```


    `code` is stable and meant for programs, `attribute` names the offending field (or is
    `null`), `message` is for humans and may change.


    | Status | Meaning |

    |---|---|

    | 400 | The body is not valid JSON (`invalid_json`). |

    | 401 | The token is missing, unknown or expired (`unauthorized`). |

    | 403 | The token may not do this: `read_only_token`, `instance_token_required`, `organization_token_required`, `organization_forbidden`, `feature_disabled`, `subscription_inactive` (booking with an organization token whose organization's subscription has run out), or a field only an instance token may set. |

    | 404 | The record does not exist, or belongs to another organization (`not_found`). |

    | 409 | The request conflicts with the current state, e.g. a taken slot or a blocked deletion. |

    | 422 | Validation failed; `code` is the validation (`blank`, `taken`, `invalid`, …). |

    | 429 | Too many requests (`rate_limited`), see Rate limits. |


    ## Feature toggles


    Availability types, availabilities, free slots and booking answer `403`
    (`feature_disabled`) when appointment booking is off for the organization;
    appointment resources answer `403` without in-person appointments; waitlist
    entries answer `403` when the treatment waitlist is off.


    ## Rate limits


    Organization tokens are limited to 50 requests per minute and 100 per five minutes.
    Beyond that the API answers `429` (`rate_limited`) in the error format above.

servers:
  - url: https://{tenant}.meetone.io
    description: Production server (tenant-specific)
    variables:
      tenant:
        default: api
        description: Your tenant subdomain

security:
  - bearerAuth: []

tags:
  - name: Organizations
    description: >
      Create and maintain organizations. Only on installations with organizations; elsewhere
      these endpoints answer `404`.
  - name: Users
    description: >
      Practitioners (`doctor`), medical assistants, admins and patients. Accounts have no
      password; who may sign in does so with a link sent by email.
  - name: Appointment resources
    description: >
      Places where appointments happen in person, e.g. a practice or a room. Requires
      in-person appointments for the organization (`403` otherwise). An appointment or
      availability type without one is an online video appointment.
  - name: Availability types
    description: >
      The kinds of appointment an organization offers, e.g. "First visit, 30 minutes, in the
      practice". Availabilities, free slots and bookings are always of one type. Requires
      appointment booking for the organization (`403` otherwise).
  - name: Availabilities
    description: >
      When a practitioner offers appointments of an availability type: one block of time on
      one day. Free slots are what is left of them after the booked appointments. Requires
      appointment booking for the organization (`403` otherwise).
  - name: Free slots
    description: >
      The times an availability type can be booked at, on the rules of online booking.
      Requires appointment booking for the organization (`403` otherwise).
  - name: Appointments
    description: Read, sync, book, reschedule, cancel and complete appointments.
  - name: Waitlist entries
    description: >
      The organization's treatment waitlist, read only: entries come from the public
      waitlist form alone, and only once the person has confirmed their email address.
      Requires an organization token (`403`, `organization_token_required`, for an
      instance token) and the treatment waitlist for the organization (`403`,
      `feature_disabled`, otherwise). Read-only tokens may call both endpoints. An entry
      that is deleted simply disappears, whether the organization deletes it, the person
      unsubscribes, or the retention rules remove it (unconfirmed after 7 days, archived
      after 30 days); a sync does not report it, so compare against a full list from
      time to time.

paths:
  /api/v1/organizations:
    get:
      tags: [Organizations]
      summary: List organizations
      operationId: listOrganizations
      description: An instance token lists every organization, an organization token only its own.
      parameters:
        - $ref: '#/components/parameters/ExternalIdFilter'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        "200":
          description: The organizations.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Organization'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
    post:
      tags: [Organizations]
      summary: Create an organization
      operationId: createOrganization
      description: >
        Instance tokens only. Without contract data the organization starts in a trial.
        Organization tokens cannot be created through the API; create one under
        Developer → API Tokens.


        With `owner`, the organization gets its first account, a practitioner who owns
        the organization. `owner.notification` decides how they hear about it: `none`
        (default), `invitation` (the invitation mail of the admin area) or `welcome` (the
        welcome mail of the website sign-up). Without `owner`, owners can be added later
        through the users API.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/OrganizationInput'
                - type: object
                  properties:
                    consent_accepted:
                      type: boolean
                      description: The organization accepted the privacy policy and terms; stamps `consent_accepted_at`.
                    owner:
                      $ref: '#/components/schemas/OrganizationOwnerInput'
      responses:
        "201":
          description: The new organization.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Organization'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/organizations/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The organization's `id`.
        schema:
          type: string
          format: uuid
    get:
      tags: [Organizations]
      summary: Show an organization
      operationId: getOrganization
      responses:
        "200":
          description: The organization.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Organization'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Organizations]
      summary: Update an organization
      operationId: updateOrganization
      description: >
        An organization token can update its own organization, but not the fields marked
        as instance token only: sending one answers `403` (`attribute_forbidden`) naming
        the field. A plan or seat change is billed like one made in the admin area; when
        the billing refuses it, the error code names the reason (e.g. `seats_below_used`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrganizationInput'
      responses:
        "200":
          description: The updated organization.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Organization'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'
    delete:
      tags: [Organizations]
      summary: Delete an organization with all of its data
      operationId: deleteOrganization
      description: >
        **Irreversible.** Deletes the organization and everything in it: its users,
        patients, appointments and video rooms, availabilities, questionnaires, messages,
        documents and its **clinical documentation** (medical records, observations,
        attachments, prescriptions). Nothing can be restored afterwards.


        **Legal retention obligations are yours.** Clinical documentation is usually
        subject to retention periods (in Germany at least ten years, § 630f BGB). Export
        and archive whatever you have to keep before you delete.


        Instance tokens only; an organization token cannot delete its organization (`403`).


        - An organization without any data (no users, appointments or other records) is
          deleted at once: `204`.

        - Otherwise the request answers `409` with the code `deletion_confirmation_required`
          until it is repeated with `confirm_permanent_deletion=true`.

        - Confirmed, it answers `202`: the organization is marked for deletion at once. From
          then on it answers `404`, its organization tokens stop working and its users can
          no longer sign in. The data is deleted in the background.


        MeetOne keeps a record of the deletion without personal data: the organization's
        `id`, `external_id` and name, the name of the API token that asked for it, the
        number of users and appointments, and when it was requested and completed.
      parameters:
        - name: confirm_permanent_deletion
          in: query
          description: "`true` confirms the permanent deletion of all data, and that you have met your retention obligations."
          schema:
            type: boolean
      responses:
        "202":
          description: The organization is marked for deletion; its data is being deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      deletion_requested_at:
                        type: string
                        format: date-time
        "204":
          description: The organization had no data and is deleted.
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'

  /api/v1/users:
    get:
      tags: [Users]
      summary: List users
      operationId: listUsers
      description: >
        Deactivated users stay in the list with `deleted_at`, so a sync learns about them.
        Service accounts behind API tokens are never listed.
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - $ref: '#/components/parameters/OrganizationExternalId'
        - name: role
          in: query
          description: Only users with this role.
          schema:
            $ref: '#/components/schemas/UserRole'
        - name: email
          in: query
          description: Only the user with this email address.
          schema:
            type: string
            format: email
        - name: login_enabled
          in: query
          description: Only users who can (`true`) or cannot (`false`) sign in.
          schema:
            type: boolean
        - $ref: '#/components/parameters/ExternalIdFilter'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        "200":
          description: The users.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'
    post:
      tags: [Users]
      summary: Create a user
      operationId: createUser
      description: >
        Instance tokens name the organization (`organization_id` or `organization_external_id`).


        - **Login:** a patient cannot sign in unless `login_enabled` is `true`; everybody
          else always can (`false` answers `422`). A patient without a login still gets the
          emails about their appointments, with the link to join the video call.

        - **Invitation:** with `send_invitation: true`, a user who can sign in gets an
          invitation email with a sign-in link.

        - **Email** is unique across the installation; one in use answers `422` with the
          code `taken`.

        - A practitioner takes a seat of the organization; without a free one the answer is
          `422` (`seats_limit_reached`).

        - Patients get their message thread where messaging is enabled.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/UserInput'
                - type: object
                  required: [email, role]
                  properties:
                    role:
                      $ref: '#/components/schemas/UserRole'
                    send_invitation:
                      type: boolean
                      default: false
      responses:
        "201":
          description: The new user.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/User'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/users/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The user's `id`.
        schema:
          type: string
          format: uuid
    get:
      tags: [Users]
      summary: Show a user
      operationId: getUser
      responses:
        "200":
          description: The user.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/User'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Users]
      summary: Update a user
      operationId: updateUser
      description: >
        The role cannot change (`422`, `immutable`). Switching `login_enabled` off signs the
        user out everywhere; switching it on together with `send_invitation: true` sends
        the invitation. A patient's new email takes their appointments along. Deactivated
        users cannot be changed (`404`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/UserInput'
                - type: object
                  properties:
                    send_invitation:
                      type: boolean
      responses:
        "200":
          description: The updated user.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/User'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'
    delete:
      tags: [Users]
      summary: Deactivate a user
      operationId: deactivateUser
      description: >
        Deactivates the account: it can no longer sign in and disappears from the app's
        lists. Its data is erased after the retention period (for patients the statutory
        retention of clinical documentation). A user with confirmed appointments still
        ahead answers `409` (`future_appointments`): cancel them first.
      responses:
        "204":
          description: The user is deactivated.
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'

  /api/v1/appointment_resources:
    get:
      tags: [Appointment resources]
      summary: List appointment resources
      operationId: listAppointmentResources
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - $ref: '#/components/parameters/OrganizationExternalId'
        - name: active
          in: query
          description: Only active (`true`) or archived (`false`) resources.
          schema:
            type: boolean
        - $ref: '#/components/parameters/ExternalIdFilter'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        "200":
          description: The resources.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AppointmentResource'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [Appointment resources]
      summary: Create an appointment resource
      operationId: createAppointmentResource
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/AppointmentResourceInput'
                - type: object
                  required: [name]
      responses:
        "201":
          description: The new resource.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AppointmentResource'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/appointment_resources/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The resource's `id`.
        schema:
          type: string
          format: uuid
    get:
      tags: [Appointment resources]
      summary: Show an appointment resource
      operationId: getAppointmentResource
      responses:
        "200":
          description: The resource.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AppointmentResource'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Appointment resources]
      summary: Update an appointment resource
      operationId: updateAppointmentResource
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentResourceInput'
      responses:
        "200":
          description: The updated resource.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AppointmentResource'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'
    delete:
      tags: [Appointment resources]
      summary: Archive an appointment resource
      operationId: archiveAppointmentResource
      description: >
        Resources are never deleted, only archived (`active: false`): appointments and
        availability types keep pointing at them. Set `active: true` to bring one back.
      responses:
        "204":
          description: The resource is archived.
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'

  /api/v1/availability_types:
    get:
      tags: [Availability types]
      summary: List availability types
      operationId: listAvailabilityTypes
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - $ref: '#/components/parameters/OrganizationExternalId'
        - $ref: '#/components/parameters/ExternalIdFilter'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        "200":
          description: The availability types.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AvailabilityType'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [Availability types]
      summary: Create an availability type
      operationId: createAvailabilityType
      description: >
        Without `identifier` it is derived from `display_name`; without `color` the next
        unused colour of the palette is picked.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/AvailabilityTypeInput'
                - type: object
                  required: [display_name, slot_duration]
      responses:
        "201":
          description: The new availability type.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AvailabilityType'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/availability_types/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The availability type's `id`.
        schema:
          type: string
          format: uuid
    get:
      tags: [Availability types]
      summary: Show an availability type
      operationId: getAvailabilityType
      responses:
        "200":
          description: The availability type.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AvailabilityType'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Availability types]
      summary: Update an availability type
      operationId: updateAvailabilityType
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AvailabilityTypeInput'
      responses:
        "200":
          description: The updated availability type.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AvailabilityType'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'
    delete:
      tags: [Availability types]
      summary: Delete an availability type
      operationId: deleteAvailabilityType
      description: >
        Answers `409` while appointments or availabilities still ahead use the type
        (`in_use`), while questionnaires use it, and for the reserved system type
        (`reserved`). Past appointments keep their data and lose the type.
      responses:
        "204":
          description: The availability type is deleted.
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'

  /api/v1/availabilities:
    get:
      tags: [Availabilities]
      summary: List availabilities
      operationId: listAvailabilities
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - $ref: '#/components/parameters/OrganizationExternalId'
        - name: from
          in: query
          required: true
          description: First day, e.g. `2026-10-12`.
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          description: Last day, at most 92 days after `from`.
          schema:
            type: string
            format: date
        - name: doctor_id
          in: query
          description: Only this practitioner's availabilities.
          schema:
            type: string
            format: uuid
        - name: doctor_external_id
          in: query
          description: Only this practitioner's availabilities, by external ID.
          schema:
            type: string
        - name: availability_type_id
          in: query
          description: Only availabilities of this type.
          schema:
            type: string
            format: uuid
        - name: availability_type_external_id
          in: query
          description: Only availabilities of this type, by external ID.
          schema:
            type: string
        - $ref: '#/components/parameters/ExternalIdFilter'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        "200":
          description: The availabilities, by day and start time.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Availability'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'
    post:
      tags: [Availabilities]
      summary: Create an availability
      operationId: createAvailability
      description: >
        The availability lies within one day (Europe/Berlin) and within the organization's
        working hours. It may not overlap another availability of the same type of the same
        practitioner; types may overlap each other.


        With `repeat`, the availability is copied to the following days or weeks; `count`
        includes the availability itself. Either the whole series is created or, if any copy
        would overlap a block of the same type, none of it (`422`, attribute `repeat`). The
        response is then the list of the series, the given availability first; only that one
        carries the `external_id`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/AvailabilityInput'
                - type: object
                  required: [starts_at, ends_at]
                  properties:
                    repeat:
                      type: object
                      required: [interval, count]
                      properties:
                        interval:
                          type: string
                          enum: [daily, weekly]
                        count:
                          type: integer
                          minimum: 1
                          maximum: 52
      responses:
        "201":
          description: The availability, or with `repeat` the series.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    oneOf:
                      - $ref: '#/components/schemas/Availability'
                      - type: array
                        items:
                          $ref: '#/components/schemas/Availability'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/availabilities/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The availability's `id`.
        schema:
          type: string
          format: uuid
    get:
      tags: [Availabilities]
      summary: Show an availability
      operationId: getAvailability
      responses:
        "200":
          description: The availability.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Availability'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Availabilities]
      summary: Update an availability
      operationId: updateAvailability
      description: To move it, send `starts_at` and `ends_at` together.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AvailabilityInput'
      responses:
        "200":
          description: The updated availability.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Availability'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'
    delete:
      tags: [Availabilities]
      summary: Delete an availability
      operationId: deleteAvailability
      description: Appointments already booked in it stay.
      responses:
        "204":
          description: The availability is deleted.
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'

  /api/v1/availability_slots:
    get:
      tags: [Free slots]
      summary: List free slots
      operationId: listAvailabilitySlots
      description: >
        Slots follow the type's `slot_duration` within the availabilities. A slot is free
        while the practitioner has no appointment of any type overlapping it, and only from
        the organization's minimum booking lead time on. Each slot names the practitioners
        free then. Not paginated: the response covers the whole range.
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - $ref: '#/components/parameters/OrganizationExternalId'
        - name: availability_type_id
          in: query
          description: The type to book (or `availability_type_external_id`). Required.
          schema:
            type: string
            format: uuid
        - name: availability_type_external_id
          in: query
          description: The type to book, by external ID.
          schema:
            type: string
        - name: from
          in: query
          required: true
          description: First day.
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          description: Last day; the range covers at most 31 days.
          schema:
            type: string
            format: date
        - name: doctor_id
          in: query
          description: Only this practitioner's slots.
          schema:
            type: string
            format: uuid
        - name: doctor_external_id
          in: query
          description: Only this practitioner's slots, by external ID.
          schema:
            type: string
      responses:
        "200":
          description: The free slots in time order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AvailabilitySlot'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/appointments:
    get:
      tags: [Appointments]
      summary: List appointments
      operationId: listAppointments
      description: >
        Cancelled appointments stay in the list. Together with `updated_since` the list
        is the way to keep an external system in sync. Without `updated_since` the list
        is ordered by start time, appointments without a time last.
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - $ref: '#/components/parameters/OrganizationExternalId'
        - name: from
          in: query
          description: Only appointments starting at or after this time.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          description: Only appointments starting before this time.
          schema:
            type: string
            format: date-time
        - name: status
          in: query
          description: Only appointments in this status.
          schema:
            $ref: '#/components/schemas/AppointmentStatus'
        - name: doctor_id
          in: query
          description: Only this practitioner's appointments.
          schema:
            type: string
            format: uuid
        - name: doctor_external_id
          in: query
          description: Only this practitioner's appointments, by external ID.
          schema:
            type: string
        - name: patient_id
          in: query
          description: Appointments booked for this patient account (matched by its email).
          schema:
            type: string
            format: uuid
        - name: patient_external_id
          in: query
          description: Appointments booked for this patient account, by external ID.
          schema:
            type: string
        - name: patient_email
          in: query
          description: Appointments booked for this email address, case-insensitive.
          schema:
            type: string
            format: email
        - name: availability_type_id
          in: query
          description: Only appointments of this type.
          schema:
            type: string
            format: uuid
        - name: availability_type_external_id
          in: query
          description: Only appointments of this type, by external ID.
          schema:
            type: string
        - name: appointment_resource_id
          in: query
          description: Only appointments at this resource.
          schema:
            type: string
            format: uuid
        - name: appointment_resource_external_id
          in: query
          description: Only appointments at this resource, by external ID.
          schema:
            type: string
        - $ref: '#/components/parameters/ExternalIdFilter'
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        "200":
          description: The appointments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Appointment'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

    post:
      tags: [Appointments]
      summary: Book an appointment
      operationId: bookAppointment
      description: >
        Books a **confirmed** appointment of an availability type: its length is the type's
        `slot_duration`, its place the type's resource (video types get their video call).


        - **Slot:** `starts_at` has to be a free slot (see `GET /api/v1/availability_slots`),
          otherwise `409` (`slot_unavailable`). Without `doctor_id` any practitioner free then
          is chosen. With `ignore_availability: true` and a `doctor_id`, the appointment is
          booked outside the availabilities, like a manually planned one; the practitioner
          still cannot be double-booked (`409`).

        - **Patient:** an existing one by `patient_id` / `patient_external_id`, or `patient`
          inline: looked up in the organization by `external_id`, then by `email`, and created
          otherwise, without a login unless `login_enabled` says so.

        - **Notifications:** like an online booking, the patient and the practitioner get the
          confirmation mail and the patient the type's questionnaire. `notify: false` sends no
          mail about this appointment to the patient, reminders included. The
          `appointment.created` webhook is sent either way.

        - Organization tokens can book at most 1000 appointments a month through the API
          (`403`, `monthly_limit_reached`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentBooking'
      responses:
        "201":
          description: The booked appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/appointments/{id}:
    get:
      tags: [Appointments]
      summary: Show an appointment
      operationId: getAppointment
      parameters:
        - $ref: '#/components/parameters/AppointmentId'
        - $ref: '#/components/parameters/OrganizationId'
        - $ref: '#/components/parameters/OrganizationExternalId'
      responses:
        "200":
          description: The appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'

    patch:
      tags: [Appointments]
      summary: Update an appointment
      operationId: updateAppointment
      description: Only the fields that do not touch the time; to move it, reschedule.
      parameters:
        - $ref: '#/components/parameters/AppointmentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                external_id:
                  type: string
                message_to_patient:
                  type: string
                message_to_doctor:
                  type: string
      responses:
        "200":
          description: The updated appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/appointments/{id}/reschedule:
    post:
      tags: [Appointments]
      summary: Reschedule an appointment
      operationId: rescheduleAppointment
      description: >
        Moves the appointment, optionally to another practitioner. The new time has to be a
        free slot of its type, the appointment itself not counting as taken; otherwise `409`
        (`slot_unavailable`). `ignore_availability: true` skips that, as for booking. Only a
        scheduled appointment that is neither cancelled nor completed can be moved (`409`,
        `not_reschedulable`). Patient and practitioner are told unless `notify: false`, or
        the appointment was booked with `notify: false`: such an appointment stays silent.
      parameters:
        - $ref: '#/components/parameters/AppointmentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [starts_at]
              properties:
                starts_at:
                  type: string
                  format: date-time
                doctor_id:
                  type: string
                  format: uuid
                doctor_external_id:
                  type: string
                ignore_availability:
                  type: boolean
                  default: false
                notify:
                  type: boolean
                  default: true
      responses:
        "200":
          description: The rescheduled appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/appointments/{id}/cancel:
    post:
      tags: [Appointments]
      summary: Cancel an appointment
      operationId: cancelAppointment
      description: >
        Cancels the appointment and closes its video call. A cancelled or completed one
        answers `409` (`not_cancellable`). For an appointment still ahead, patient and
        practitioner are told unless `notify: false`, or the appointment was booked with
        `notify: false`. The appointment stays in the list.
      parameters:
        - $ref: '#/components/parameters/AppointmentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                notify:
                  type: boolean
                  default: true
      responses:
        "200":
          description: The cancelled appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/appointments/{id}/complete:
    post:
      tags: [Appointments]
      summary: Complete an appointment
      operationId: completeAppointment
      description: >
        Records how the appointment went and closes its video call. Only a scheduled
        appointment that is neither cancelled nor completed (`409`, `not_completable`).
      parameters:
        - $ref: '#/components/parameters/AppointmentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status, reason]
              properties:
                status:
                  type: string
                  enum: [success, no_success]
                reason:
                  type: string
      responses:
        "200":
          description: The completed appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/waitlist_entries:
    get:
      tags: [Waitlist entries]
      summary: List waitlist entries
      operationId: listWaitlistEntries
      description: >
        The confirmed entries of the token's organization, whatever their status. Without
        `updated_since` the list is ordered by submission, oldest first. With it, the list
        is the way to keep an external system in sync.
      parameters:
        - name: status
          in: query
          description: Only entries in this status.
          schema:
            $ref: '#/components/schemas/WaitlistEntryStatus'
        - name: doctor_id
          in: query
          description: Only entries that prefer this practitioner.
          schema:
            type: string
            format: uuid
        - name: doctor_external_id
          in: query
          description: Only entries that prefer this practitioner, by external ID.
          schema:
            type: string
        - $ref: '#/components/parameters/UpdatedSince'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        "200":
          description: The waitlist entries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WaitlistEntry'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          $ref: '#/components/responses/UnprocessableContent'

  /api/v1/waitlist_entries/{id}:
    get:
      tags: [Waitlist entries]
      summary: Show a waitlist entry
      operationId: getWaitlistEntry
      parameters:
        - name: id
          in: path
          required: true
          description: The waitlist entry's `id`.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The waitlist entry.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/WaitlistEntry'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

  parameters:
    AppointmentId:
      name: id
      in: path
      required: true
      description: The appointment's `id`.
      schema:
        type: string
        format: uuid
    OrganizationId:
      name: organization_id
      in: query
      required: false
      description: >
        Instance tokens: the organization to act in (its `id`). Required to create
        anything; a filter on lists. Organization tokens may only name their own.
      schema:
        type: string
        format: uuid
    OrganizationExternalId:
      name: organization_external_id
      in: query
      required: false
      description: The organization to act in, by its `external_id`. Alternative to `organization_id`.
      schema:
        type: string
    ExternalIdFilter:
      name: external_id
      in: query
      required: false
      description: Only the record with this external ID.
      schema:
        type: string
    UpdatedSince:
      name: updated_since
      in: query
      required: false
      description: Only records changed at or after this time, ordered by `updated_at`. Pages by cursor; follow the `next` link.
      schema:
        type: string
        format: date-time
        example: "2026-10-08T14:30:00+02:00"
    Page:
      name: page
      in: query
      required: false
      description: The page to return, from 1. With `updated_since`, the opaque cursor from the `next` link instead.
      schema:
        oneOf:
          - type: integer
            minimum: 1
          - type: string
    PerPage:
      name: per_page
      in: query
      required: false
      description: Records per page, at most 100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50

  responses:
    BadRequest:
      description: The body is not valid JSON.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
    Unauthorized:
      description: The API token is missing, invalid or expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
    Forbidden:
      description: The token may not do this.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
    NotFound:
      description: The record does not exist or belongs to another organization.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
    Conflict:
      description: The request conflicts with the current state.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
    UnprocessableContent:
      description: Validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'

  schemas:
    AvailabilitySlot:
      type: object
      properties:
        starts_at:
          type: string
          format: date-time
          example: "2026-10-12T09:15:00+02:00"
        ends_at:
          type: string
          format: date-time
          example: "2026-10-12T09:45:00+02:00"
        doctors:
          type: array
          description: The practitioners free at that time.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              external_id:
                type: string
                nullable: true
    AppointmentBooking:
      type: object
      required: [starts_at]
      description: >
        The type by `availability_type_id` or `availability_type_external_id` (required), the
        patient by `patient_id`, `patient_external_id` or `patient`.
      properties:
        availability_type_id:
          type: string
          format: uuid
        availability_type_external_id:
          type: string
        starts_at:
          type: string
          format: date-time
        doctor_id:
          type: string
          format: uuid
        doctor_external_id:
          type: string
        patient_id:
          type: string
          format: uuid
        patient_external_id:
          type: string
        patient:
          type: object
          description: A patient to look up or create; see `POST /api/v1/users` for the fields.
          properties:
            email:
              type: string
              format: email
            external_id:
              type: string
            login_enabled:
              type: boolean
              default: false
            locale:
              type: string
            profile:
              $ref: '#/components/schemas/UserProfile'
        external_id:
          type: string
        message_to_patient:
          type: string
        notify:
          type: boolean
          default: true
        ignore_availability:
          type: boolean
          default: false
          description: Book outside the availabilities; requires `doctor_id`.
    Availability:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        doctor:
          $ref: '#/components/schemas/PersonReference'
        availability_type:
          $ref: '#/components/schemas/AvailabilityTypeReference'
        starts_at:
          type: string
          format: date-time
          example: "2026-10-12T09:00:00+02:00"
        ends_at:
          type: string
          format: date-time
          example: "2026-10-12T12:00:00+02:00"
        date:
          type: string
          format: date
          example: "2026-10-12"
        start_time:
          type: string
          example: "09:00"
        end_time:
          type: string
          example: "12:00"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AvailabilityInput:
      type: object
      description: >
        The practitioner by `doctor_id` or `doctor_external_id` (a medical assistant where
        medical assistants may plan), the type by `availability_type_id` or
        `availability_type_external_id`; both required to create.
      properties:
        doctor_id:
          type: string
          format: uuid
        doctor_external_id:
          type: string
        availability_type_id:
          type: string
          format: uuid
        availability_type_external_id:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
          description: On the same day as `starts_at` (Europe/Berlin).
        external_id:
          type: string
    AppointmentResource:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        name:
          type: string
          example: Praxis 1
        address:
          type: string
          nullable: true
          example: Hauptstraße 1, 12345 Musterstadt
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AppointmentResourceInput:
      type: object
      properties:
        name:
          type: string
        address:
          type: string
          nullable: true
        active:
          type: boolean
        external_id:
          type: string
    AvailabilityType:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        display_name:
          type: string
          example: First visit
        identifier:
          type: string
          example: first-visit
        slot_duration:
          type: integer
          description: Minutes; the length of every slot and booked appointment of this type.
          example: 30
        color:
          type: string
          example: "#22C55E"
        bookable:
          type: boolean
          description: Offered on the online booking page.
        description:
          type: string
          nullable: true
        mailer_instructions:
          type: string
          nullable: true
          description: Added to the appointment confirmation mail.
        requires_consent:
          type: boolean
        send_questionnaire:
          type: boolean
        appointment_resource:
          nullable: true
          description: Where it takes place; `null` is an online video appointment.
          allOf:
            - $ref: '#/components/schemas/AppointmentResourceReference'
        reserved:
          type: boolean
          description: A system type that cannot be deleted.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AvailabilityTypeInput:
      type: object
      properties:
        display_name:
          type: string
        identifier:
          type: string
          pattern: "^[a-z0-9_-]+$"
        external_id:
          type: string
        slot_duration:
          type: integer
          minimum: 1
        color:
          type: string
          pattern: "^#[0-9A-Fa-f]{6}$"
        bookable:
          type: boolean
        description:
          type: string
        mailer_instructions:
          type: string
        requires_consent:
          type: boolean
        send_questionnaire:
          type: boolean
        appointment_resource_id:
          type: string
          format: uuid
          nullable: true
          description: "`null` makes it a video type. Requires in-person appointments."
        appointment_resource_external_id:
          type: string
    UserRole:
      type: string
      enum: [doctor, medical_assistant, patient, admin]
    UserProfile:
      type: object
      description: >
        One profile for every role. Not every role has every field: `medical_assistant` has
        no address, `admin` only the names, and only patients have the insurance fields.
        Sending a field the role does not have answers `422` (`unsupported_for_role`); reading
        it answers `null`. `first_name` and `last_name` are required.
      properties:
        name_title:
          type: string
          nullable: true
        first_name:
          type: string
        last_name:
          type: string
        gender:
          type: string
          nullable: true
          enum: [male, female, null]
        date_of_birth:
          type: string
          format: date
          nullable: true
        mobile_number:
          type: string
          nullable: true
        land_line_number:
          type: string
          nullable: true
        address_street:
          type: string
          nullable: true
        address_street_number:
          type: string
          nullable: true
        address_zipcode:
          type: string
          nullable: true
        address_city:
          type: string
          nullable: true
        address_country:
          type: string
          nullable: true
        insurance_company:
          type: string
          nullable: true
        insurance_number:
          type: string
          nullable: true
        insurance_type:
          type: string
          nullable: true
          enum: [gkv, pkv, self_pay, bg, null]
    UserInput:
      type: object
      properties:
        email:
          type: string
          format: email
        external_id:
          type: string
        login_enabled:
          type: boolean
          description: Patients only; defaults to `false` for them. Everybody else always has a login.
        organization_owner:
          type: boolean
          description: Practitioners and admins only.
        locale:
          type: string
          example: de
        profile:
          $ref: '#/components/schemas/UserProfile'
    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        email:
          type: string
        role:
          $ref: '#/components/schemas/UserRole'
        login_enabled:
          type: boolean
        organization_owner:
          type: boolean
        locale:
          type: string
          nullable: true
        organization:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            external_id:
              type: string
              nullable: true
        profile:
          $ref: '#/components/schemas/UserProfile'
        deleted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Organization:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        first_name:
          type: string
          nullable: true
          description: Contact person.
        last_name:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        address_street:
          type: string
          nullable: true
        address_street_number:
          type: string
          nullable: true
        address_zipcode:
          type: string
          nullable: true
        address_city:
          type: string
          nullable: true
        address_country:
          type: string
          nullable: true
          description: ISO 3166-1 alpha-2, e.g. `DE`.
        billing_company:
          type: string
          nullable: true
        billing_company_addition:
          type: string
          nullable: true
        billing_first_name:
          type: string
          nullable: true
        billing_last_name:
          type: string
          nullable: true
        billing_email:
          type: string
          nullable: true
        billing_address_street:
          type: string
          nullable: true
        billing_address_street_number:
          type: string
          nullable: true
        billing_zipcode:
          type: string
          nullable: true
        billing_city:
          type: string
          nullable: true
        billing_country:
          type: string
          nullable: true
        billing_vat_id:
          type: string
          nullable: true
        owners:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              external_id:
                type: string
                nullable: true
              email:
                type: string
        settings:
          $ref: '#/components/schemas/OrganizationSettings'
        plan_handle:
          type: string
        seats:
          type: integer
        seats_used:
          type: integer
          description: Practitioners using a seat.
        subscription_active:
          type: boolean
          description: Whether the organization may use the platform (trial, contract or subscription).
        trial_active:
          type: boolean
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        contract_active:
          type: boolean
        contract_started_at:
          type: string
          format: date-time
          nullable: true
        contract_ends_at:
          type: string
          format: date-time
          nullable: true
        features:
          type: array
          items:
            type: string
          example: [appointment_booking, appointment_resources]
        custom_domain:
          type: string
          nullable: true
        theme:
          type: string
          nullable: true
        consent_accepted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    OrganizationInput:
      type: object
      description: >
        Fields marked *instance token only* answer `403` from an organization token. The
        billing contact defaults to the contact person.
      properties:
        name:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
          format: email
        address_street:
          type: string
        address_street_number:
          type: string
        address_zipcode:
          type: string
        address_city:
          type: string
        address_country:
          type: string
        billing_company:
          type: string
        billing_company_addition:
          type: string
        billing_first_name:
          type: string
        billing_last_name:
          type: string
        billing_email:
          type: string
          format: email
        billing_address_street:
          type: string
        billing_address_street_number:
          type: string
        billing_zipcode:
          type: string
        billing_city:
          type: string
        billing_country:
          type: string
        billing_vat_id:
          type: string
        settings:
          $ref: '#/components/schemas/OrganizationSettings'
        external_id:
          type: string
          description: Instance token only.
        plan_handle:
          type: string
          description: Instance token only. A plan the installation offers.
        seats:
          type: integer
          description: Instance token only.
        trial_active:
          type: boolean
          description: Instance token only.
        trial_ends_at:
          type: string
          format: date-time
          description: Instance token only.
        contract_active:
          type: boolean
          description: Instance token only.
        contract_started_at:
          type: string
          format: date-time
          description: Instance token only.
        contract_ends_at:
          type: string
          format: date-time
          description: Instance token only.
        features:
          type: array
          items:
            type: string
          description: Instance token only. Features the installation lets organizations have.
        custom_domain:
          type: string
          description: Instance token only.
        theme:
          type: string
          description: Instance token only.
    OrganizationOwnerInput:
      type: object
      required: [email, first_name, last_name]
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        external_id:
          type: string
        notification:
          type: string
          enum: [none, invitation, welcome]
          default: none
    OrganizationSettings:
      type: object
      description: >
        Send only the settings to change; `null` resets one to the installation's default.
      properties:
        planner_day_start_hour:
          type: integer
          nullable: true
          minimum: 0
          maximum: 23
          description: Start of the working day; availabilities must lie within it.
        planner_day_end_hour:
          type: integer
          nullable: true
          minimum: 0
          maximum: 23
          description: End of the working day; later than its start.
        booking_min_lead_time_hours:
          type: integer
          nullable: true
          minimum: 0
          description: How many hours ahead a slot has to be booked online.
        appointment_booking_show_avatar:
          type: boolean
        appointment_booking_show_personal_message:
          type: boolean
        appointment_booking_personal_message:
          type: string
          nullable: true
        appointment_booking_personal_message_style:
          type: string
          enum: [info, warning, error]
        role_terminology:
          type: string
          nullable: true
          description: The words used for the two roles of an appointment; one of the presets the installation offers.
    AppointmentStatus:
      type: string
      description: >
        Where the appointment stands: `requested` (no time yet), `unconfirmed`, `confirmed`,
        `completed`, or `cancelled`.
      enum: [requested, unconfirmed, confirmed, completed, cancelled]
    Appointment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        status:
          $ref: '#/components/schemas/AppointmentStatus'
        scheduled_at:
          type: string
          format: date-time
          nullable: true
          example: "2026-10-12T09:00:00+02:00"
        ends_at:
          type: string
          format: date-time
          nullable: true
          example: "2026-10-12T09:30:00+02:00"
        duration:
          type: integer
          nullable: true
          description: Minutes.
          example: 30
        confirmed_at:
          type: string
          format: date-time
          nullable: true
        cancelled_at:
          type: string
          format: date-time
          nullable: true
        cancelled_reason:
          type: string
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        completed_status:
          type: string
          nullable: true
          enum: [success, no_success, null]
        availability_type:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/AvailabilityTypeReference'
        appointment_resource:
          nullable: true
          description: Where the appointment takes place; `null` is an online video appointment.
          allOf:
            - $ref: '#/components/schemas/AppointmentResourceReference'
        doctor:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PersonReference'
        patient:
          nullable: true
          description: The patient account the appointment is linked to by its email, if there is one.
          allOf:
            - $ref: '#/components/schemas/PatientReference'
        patient_name:
          type: string
          nullable: true
        patient_email:
          type: string
          nullable: true
        video:
          type: object
          nullable: true
          description: Join links of the video call; `null` for in-person appointments.
          properties:
            host_join_url:
              type: string
              format: uri
            guest_join_url:
              type: string
              format: uri
        message_to_patient:
          type: string
          nullable: true
        message_to_doctor:
          type: string
          nullable: true
        source:
          type: string
          enum: [manual, api]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AvailabilityTypeReference:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        display_name:
          type: string
    AppointmentResourceReference:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        name:
          type: string
        address:
          type: string
          nullable: true
    PersonReference:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
    PatientReference:
      allOf:
        - $ref: '#/components/schemas/PersonReference'
        - type: object
          properties:
            email:
              type: string
              format: email
    WaitlistEntryStatus:
      type: string
      description: >
        Where the entry stands: `waiting` (on the list), `invited` (sent an invitation to
        book), `booked`, `archived` (taken off the list by the practice, or after an
        unanswered "still interested?" mail, and deleted 30 days later), or `withdrawn`.
        `withdrawn` is not used: someone who leaves the list is deleted, so the entry
        disappears. The value stays in the enum for compatibility, as clients may filter by it.
      enum: [waiting, invited, booked, withdrawn, archived]
    WaitlistEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/WaitlistEntryStatus'
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
          format: email
        phone_number:
          type: string
          nullable: true
        date_of_birth:
          type: string
          format: date
          nullable: true
        address_street:
          type: string
          nullable: true
        address_zipcode:
          type: string
          nullable: true
        address_place:
          type: string
          nullable: true
        gp_name:
          type: string
          nullable: true
          description: The person's general practitioner.
        insurance_type:
          type: string
          enum: [gkv, pkv, self_pay]
          nullable: true
          description: Statutory (`gkv`) or private (`pkv`) health insurance, or self-paying.
        preferred_time:
          type: string
          enum: [morning, afternoon, all_day]
          nullable: true
        consultation_requested:
          type: boolean
          description: Whether the person asked for an initial consultation.
        message:
          type: string
          nullable: true
        internal_note:
          type: string
          nullable: true
          description: The practice's own note; the person never sees it.
        locale:
          type: string
          nullable: true
          description: The language the person signed up in, e.g. `de`.
        submitted_at:
          type: string
          format: date-time
        email_confirmed_at:
          type: string
          format: date-time
          nullable: true
        consent_accepted_at:
          type: string
          format: date-time
          nullable: true
        invited_at:
          type: string
          format: date-time
          nullable: true
        last_reconfirmed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        doctor:
          description: The practitioner the person prefers, if any.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PersonReference'
        invitation:
          type: object
          nullable: true
          description: The invitation to book the person was sent last, if any.
          properties:
            doctor:
              nullable: true
              allOf:
                - $ref: '#/components/schemas/PersonReference'
            availability_type:
              nullable: true
              allOf:
                - $ref: '#/components/schemas/AvailabilityTypeReference'
            appointment_id:
              type: string
              format: uuid
              nullable: true
              description: The appointment booked through the invitation, once there is one.
            created_at:
              type: string
              format: date-time
        answers:
          type: array
          description: >
            The answers to the organization's waitlist questions, in question order and in
            the language they were given in.
          items:
            $ref: '#/components/schemas/WaitlistAnswer'
    WaitlistAnswer:
      type: object
      properties:
        question:
          type: string
        question_type:
          type: string
          enum: [free_text, dropdown, number, single_choice, multiple_choice]
        text:
          type: string
          nullable: true
        number:
          type: number
          nullable: true
        selected_options:
          type: array
          items:
            type: object
            properties:
              label:
                type: string
              text_input:
                type: string
                nullable: true
    Errors:
      type: object
      properties:
        errors:
          type: array
          items:
            $ref: '#/components/schemas/Error'
    Error:
      type: object
      properties:
        code:
          type: string
          example: taken
        attribute:
          type: string
          nullable: true
          example: external_id
        message:
          type: string
          example: External ID has already been taken
