Booking takes two requests: ask which times of an availability type are free, then book one. This article covers both, and what happens when two systems want the same slot.

Requirements

  • Appointment booking must be enabled for the organization. Without it, free slots and booking return 403 with the code feature_disabled.
  • Instance tokens name the organization with organization_id or organization_external_id on both requests; without it the answer is 422 (organization_required).
  • A read-only token can list free slots but not book.

List free slots

GET /api/v1/availability_slots

Query parameter Type Description
availability_type_id or availability_type_external_id string The type to book. Required.
from string (date) First day, e.g. 2026-10-12. Required.
to string (date) Last day, inclusive. Required. The range covers at most 31 days.
doctor_id or doctor_external_id string Only this practitioner's slots.
organization_id, organization_external_id string Instance tokens: the organization.
curl "https://acme.meetone.io/api/v1/availability_slots?availability_type_external_id=first-visit&from=2026-10-12&to=2026-10-16" \
  -H "Authorization: Bearer $TOKEN"
{
  "data": [
    {
      "starts_at": "2026-10-12T09:00:00+02:00",
      "ends_at": "2026-10-12T09:30:00+02:00",
      "doctors": [
        { "id": "b7d2e9c4-1a3f-4c58-8e66-2f9a0d7c5b13", "external_id": "staff-12" },
        { "id": "f2a6d8c0-5e91-4b37-9d14-8c7e3a0b6f25", "external_id": null }
      ]
    },
    {
      "starts_at": "2026-10-12T09:30:00+02:00",
      "ends_at": "2026-10-12T10:00:00+02:00",
      "doctors": [
        { "id": "b7d2e9c4-1a3f-4c58-8e66-2f9a0d7c5b13", "external_id": "staff-12" }
      ]
    }
  ]
}

The response is a list of slots in time order, each with the practitioners who are free then. It is not paginated: one response covers the whole range. A range longer than 31 days, or an end before the start, returns 422 with the code invalid and the attribute to.

How slots are calculated

Free slots follow the same rules as the online booking page:

  • Grid. Slots lie on a grid that starts at the beginning of the working day and advances by the type's slot_duration. Each slot is exactly as long as the type. A slot is only offered when it fits completely inside an availability of that type; its start does not have to coincide with the start of the availability.
  • Availabilities. Only practitioners who have an availability of this type at that time are offered. See Availabilities endpoints.
  • Existing appointments block. A practitioner is not free for a slot while they have an appointment that overlaps it, whatever its type. Cancelled appointments do not block. This is why two types that are offered at the same hours do not lead to double bookings.
  • Lead time. A slot is only offered if it starts later than the organization's minimum booking lead time from now. The organization sets it with booking_min_lead_time_hours, see Organizations endpoints.
  • 31 days at most per request. For a longer horizon, ask in several requests.

A slot list is a snapshot. Another system, or a patient on the online booking page, can take a slot a moment after you read it. Treat a slot as free only when the booking succeeds.

Book an appointment

POST /api/v1/appointments

The appointment is created confirmed. It takes its length (slot_duration) and its place from the availability type: a video type opens a video call and returns its join links, an in-person type uses the type's appointment resource.

Field Type Required Description
availability_type_id or availability_type_external_id string Yes The type to book.
starts_at string (date-time) Yes The start. A time without an offset is read in Europe/Berlin.
patient_id or patient_external_id string One of the patient options An existing patient.
patient object One of the patient options A patient to look up or create. See The patient.
doctor_id or doctor_external_id string No The practitioner. Without it, any practitioner who is free then is chosen at random.
ignore_availability boolean No Book outside the availabilities. Default false. Requires a practitioner.
external_id string No Your key for the appointment.
message_to_patient string No A message for the patient.
notify boolean No Send the confirmation emails. Default true.
organization_id or organization_external_id string Instance tokens The organization.
curl -X POST "https://acme.meetone.io/api/v1/appointments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "availability_type_external_id": "first-visit",
    "starts_at": "2026-10-12T09:00:00+02:00",
    "external_id": "booking-8812",
    "message_to_patient": "See you on Monday.",
    "patient": {
      "external_id": "pat-1001",
      "email": "mary.major@example.com",
      "profile": { "first_name": "Mary", "last_name": "Major" }
    }
  }'

A successful request returns 201 Created with the appointment (abbreviated here; see The appointment object for all fields):

{
  "data": {
    "id": "c6e0b3a9-72d4-4f1b-9a58-0d3f8e2c6b17",
    "external_id": "booking-8812",
    "status": "confirmed",
    "scheduled_at": "2026-10-12T09:00:00+02:00",
    "ends_at": "2026-10-12T09:30:00+02:00",
    "duration": 30,
    "doctor": { "id": "b7d2e9c4-1a3f-4c58-8e66-2f9a0d7c5b13", "external_id": "staff-12", "first_name": "Maria", "last_name": "Muster" },
    "patient": { "id": "e41c8a0f-6b9d-4d27-a3f5-90c2b7d1e8a4", "external_id": "pat-1001", "email": "mary.major@example.com", "first_name": "Mary", "last_name": "Major" },
    "video": {
      "host_join_url": "https://acme.meetone.io/r/7bbb22b73d4e4732",
      "guest_join_url": "https://acme.meetone.io/r/3d4e4722b7327bbb"
    },
    "source": "api"
  }
}

The slot must be free

By default starts_at has to be a free slot of the type, one of the starts_at values that GET /api/v1/availability_slots returns. Otherwise the request returns 409 Conflict with the code slot_unavailable, and nothing is created:

{
  "errors": [
    { "code": "slot_unavailable", "attribute": null, "message": "This time is not, or no longer, free." }
  ]
}

A time that is not on the slot grid, outside the availabilities, too soon after now, or already taken, all return the same 409.

Choosing the practitioner

  • With doctor_id/doctor_external_id that practitioner has to be free at starts_at. If they are not, the answer is 409, even if another practitioner is free.
  • Without a practitioner any practitioner who is free at starts_at is chosen at random.

Booking outside the availabilities

With ignore_availability: true and a practitioner, the appointment is booked at any time, like a manually planned appointment in the app: the grid, the availabilities and the lead time are not checked. The practitioner still cannot be double-booked; an appointment that overlaps another one of theirs returns 409 (slot_unavailable). Without a practitioner it returns 422 with the code blank and the attribute doctor_id.

The patient

Name the patient with patient_id or patient_external_id, or give a patient object. Without any of them the request returns 422 with the code blank and the attribute patient_id. A reference to a patient who does not exist in the organization, or has been deactivated, returns 404.

The inline patient object looks the patient up in the organization, by external_id first and then by email (case-insensitive), and creates the patient if none is found. An existing patient is used as they are; fields in patient do not update them. Use PATCH /api/v1/users/{id} for that.

Field Type Description
email string Required to create a patient. Unique across the installation.
external_id string Your key for the patient.
profile object first_name and last_name are required to create a patient; the other profile fields are those of a patient, see Users endpoints.
login_enabled boolean Default false. A patient created this way cannot sign in unless you set true. They still receive the emails about their appointments, with the video link.
locale string The patient's language, e.g. de.

Errors in a patient that is created are reported under patient., for example patient.profile.first_name with the code blank, or patient.email with the code taken. Nothing is created if either the patient or the appointment is invalid.

Notifications and webhooks

  • Emails. As with an online booking, the patient and the practitioner receive the confirmation email, and the patient receives the questionnaire of the type if it has one. With notify: false neither of them receives the confirmation email, and the patient receives no reminders for this appointment.
  • Webhook. The appointment.created webhook is sent in either case, if you have subscribed to it. See Webhook events reference.

Limits

  • Monthly limit for organization tokens. An organization token can book at most 1,000 appointments per calendar month through the API. After that, booking returns 403 with the code monthly_limit_reached. Instance tokens are not limited. Reading, rescheduling, cancelling and completing do not count.
  • Subscription. Where the installation uses trials and contracts, an organization token whose organization has no running trial or contract cannot book (403, code subscription_inactive).

Concurrency

Two requests for the same slot can never both succeed. MeetOne checks the slot and creates the appointment while holding a lock on the practitioner's calendar, so exactly one request wins and the other receives 409 with the code slot_unavailable. After a 409, fetch the free slots again and book another one.

Errors at a glance

Status Code Meaning
403 feature_disabled Appointment booking is not enabled for the organization.
403 monthly_limit_reached The monthly booking limit of an organization token is reached.
403 subscription_inactive The organization's subscription is inactive.
404 not_found The type, practitioner or patient does not exist in the organization.
409 slot_unavailable The time is not, or no longer, free.
422 blank, taken, invalid, ... A required field is missing or a value is invalid, including the fields of an inline patient.

Questions? Contact us at support@meetone.io.