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
403with the codefeature_disabled. - Instance tokens name the organization with
organization_idororganization_external_idon both requests; without it the answer is422(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_idthat practitioner has to be free atstarts_at. If they are not, the answer is409, even if another practitioner is free. - Without a practitioner any practitioner who is free at
starts_atis 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: falseneither of them receives the confirmation email, and the patient receives no reminders for this appointment. - Webhook. The
appointment.createdwebhook 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
403with the codemonthly_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, codesubscription_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
Questions? Contact us at support@meetone.io.