# Free slots and booking

> How to list the bookable times of an availability type and book a confirmed appointment: slot rules, choosing the practitioner, patients created inline, notifications, limits and conflicts.

Source: https://help.meetone.io/developers/scheduling-api/availability-slots-and-booking
Last updated: 2026-10-08

---

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. |

```bash
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"
```

```json
{
  "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](https://help.meetone.io/developers/scheduling-api/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](https://help.meetone.io/developers/scheduling-api/organizations-endpoints#settings).
- **31 days at most** per request. For a longer horizon, ask in several requests.

> **Note**
>
> 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](https://help.meetone.io/developers/scheduling-api/availability-slots-and-booking#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. |

```bash
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](https://help.meetone.io/developers/scheduling-api/appointments-endpoints#the-appointment-object) for all fields):

```json
{
  "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:

```json
{
  "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](https://help.meetone.io/developers/scheduling-api/users-endpoints#the-profile). |
| `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](https://help.meetone.io/developers/webhooks/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. |

## Related

- [Appointments endpoints](https://help.meetone.io/developers/scheduling-api/appointments-endpoints)
- [Availabilities endpoints](https://help.meetone.io/developers/scheduling-api/availabilities-endpoints)
- [Users endpoints](https://help.meetone.io/developers/scheduling-api/users-endpoints)
- [Errors & pagination](https://help.meetone.io/developers/getting-started/errors-and-pagination)

Questions? Contact us at [support@meetone.io](mailto:support@meetone.io).
