Appointments are what your integration ultimately cares about. This article covers reading them, keeping your own system in sync, and changing them after they are booked. How they are created is described in Free slots and booking.

Endpoint Purpose
GET /api/v1/appointments List and filter appointments; the interface for syncing.
GET /api/v1/appointments/{id} Show one appointment.
POST /api/v1/appointments Book an appointment. See Free slots and booking.
PATCH /api/v1/appointments/{id} Update the fields that do not touch the time.
POST /api/v1/appointments/{id}/reschedule Move an appointment.
POST /api/v1/appointments/{id}/cancel Cancel an appointment.
POST /api/v1/appointments/{id}/complete Record how an appointment went.

Read-only tokens can call the two GET endpoints only. Instance tokens may name the organization with organization_id or organization_external_id; on a single appointment it is optional and must match the appointment's organization. Organization tokens get 404 for appointments of other organizations.

The appointment object

Field Type Description
id string (uuid) MeetOne's ID of the appointment.
external_id string, nullable Your key for the appointment. Unique within the organization. See Identifiers.
status string (enum) requested, unconfirmed, confirmed, completed or cancelled. See Statuses.
scheduled_at string (date-time), nullable The start, in Europe/Berlin. null for a requested appointment.
ends_at string (date-time), nullable The end: scheduled_at plus duration.
duration integer, nullable Minutes.
confirmed_at, cancelled_at, completed_at string (date-time), nullable When the appointment reached that state.
cancelled_reason string, nullable The reason given when it was cancelled.
completed_status string, nullable success or no_success once completed.
availability_type object, nullable The type: id, external_id, display_name.
appointment_resource object, nullable The place: id, external_id, name, address. null is an online video appointment.
doctor object, nullable The practitioner: id, external_id, first_name, last_name.
patient object, nullable The patient account: id, external_id, email, first_name, last_name. Appointments name their patient by email, so this is null when no patient account of the organization has that email, even if an account with that email exists in another organization. patient_email is shown either way.
patient_name, patient_email string, nullable The name and email the appointment was booked for.
video object, nullable host_join_url and guest_join_url of the video call. null for in-person appointments.
message_to_patient, message_to_doctor string, nullable Messages attached to the appointment.
source string (enum) api for appointments booked through this API, manual for all others.
created_at, updated_at string (date-time) Timestamps. updated_at drives syncing.

Medical history, private notes of the practitioner, and bank and tax data are never part of the object.

video.host_join_url lets whoever opens it join the call as the host. Treat it like a credential: keep it on your server and hand it only to the practitioner. The patient gets guest_join_url.

{
  "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,
    "confirmed_at": "2026-10-08T10:20:00+02:00",
    "cancelled_at": null,
    "cancelled_reason": null,
    "completed_at": null,
    "completed_status": null,
    "availability_type": {
      "id": "5d9a2f77-c3e1-4b08-8a64-7e1f0c92d3b6",
      "external_id": "first-visit",
      "display_name": "First visit"
    },
    "appointment_resource": null,
    "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"
    },
    "patient_name": "Mary Major",
    "patient_email": "mary.major@example.com",
    "video": {
      "host_join_url": "https://acme.meetone.io/r/7bbb22b73d4e4732",
      "guest_join_url": "https://acme.meetone.io/r/3d4e4722b7327bbb"
    },
    "message_to_patient": "See you on Monday.",
    "message_to_doctor": null,
    "source": "api",
    "created_at": "2026-10-08T10:20:00+02:00",
    "updated_at": "2026-10-08T10:20:00+02:00"
  }
}

Statuses

status Meaning
requested An appointment request without a time yet (scheduled_at is null). Appointments booked through this API are never requested.
unconfirmed Has a time, but is not confirmed yet.
confirmed Has a time and is confirmed. Everything you book through the API starts here.
completed Has been completed. completed_status is success or no_success.
cancelled Has been cancelled. cancelled_at and cancelled_reason are set. A cancelled appointment stays in the list.

cancelled takes precedence over completed, and both over the other statuses.

List appointments

GET /api/v1/appointments

Query parameter Type Description
from string (date-time) Only appointments starting at or after this time.
to string (date-time) Only appointments starting before this time.
status string (enum) Only appointments in this status. An unknown status returns 422.
doctor_id, doctor_external_id string Only this practitioner's appointments.
patient_id, patient_external_id string Only appointments booked for this patient account (matched by its email).
patient_email string Only appointments booked for this email address, case-insensitive.
availability_type_id, availability_type_external_id string Only appointments of this type.
appointment_resource_id, appointment_resource_external_id string Only appointments at this place.
external_id string Only the appointment with this external ID.
organization_id, organization_external_id string Instance tokens: only this organization's appointments.
updated_since string (date-time) Only appointments changed at or after this time. See below.
page, per_page integer Pagination. per_page defaults to 50, at most 100. With updated_since, page is an opaque cursor taken from the next link instead of a page number.

Times without an offset are read in Europe/Berlin. Filters combine: all that you send must match.

Without updated_since, the list is ordered by start time, with appointments that have no time last. With updated_since, it is ordered by updated_at.

curl "https://acme.meetone.io/api/v1/appointments?from=2026-10-12T00:00:00%2B02:00&to=2026-10-13T00:00:00%2B02:00&status=confirmed&doctor_external_id=staff-12" \
  -H "Authorization: Bearer $TOKEN"

In a query string, a + stands for a space. Encode the offset of a time as %2B (...T09:00:00%2B02:00), or use Z for UTC, or let your HTTP client build the query.

Show an appointment

GET /api/v1/appointments/{id} returns the appointment in the object shown above, or 404 if it does not exist or belongs to another organization.

Keep your system in sync

The list is the interface for keeping an external system in sync: cancelled appointments stay in it, so a cancellation is just another change you read, and updated_since returns only what changed.

  • updated_since=<timestamp> returns the appointments whose updated_at is at or after that time, ordered by updated_at, oldest change first.
  • Such a sync pages by cursor, not by page number. Follow the next URL of the link response header until there is none. Its page value is an opaque cursor: do not build it or count it up. A response to a sync has no total-count or total-pages header, so the missing next link is the only signal that you reached the end. A cursor that is malformed returns 422 with the code invalid and the attribute page.
  • An appointment that changes while you page comes again further on, after the ones you have not read yet. Store appointments by id (upsert), so reading one twice does no harm.
  • Next time, send the updated_at of the last appointment you processed as updated_since, or a minute earlier to be safe. What comes again is simply stored again.
  • For the first run, send an updated_since far in the past, for example 2000-01-01T00:00:00+00:00. That reads every appointment, in the same order as later runs, and the last updated_at becomes your first cursor.
import requests
from datetime import datetime, timedelta

BASE = "https://acme.meetone.io/api/v1"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}

def sync(last_updated_at):
    """Store every appointment changed since the last run; return the new last updated_at."""
    # A minute earlier than the last one we stored, to be safe.
    since = (datetime.fromisoformat(last_updated_at) - timedelta(minutes=1)).isoformat()
    url, params = f"{BASE}/appointments", {"updated_since": since, "per_page": 100}
    while url:
        response = requests.get(url, headers=HEADERS, params=params)
        response.raise_for_status()
        for appointment in response.json()["data"]:
            upsert(appointment)                              # idempotent, keyed by appointment["id"]
            last_updated_at = appointment["updated_at"]      # oldest change first: the last one is the newest
        url = response.links.get("next", {}).get("url")      # the cursor is inside this URL
        params = None                                        # ...which already carries every parameter
    return last_updated_at

Run it on a schedule, for example every few minutes, and persist the returned value after each run. An Organization API Token is rate limited, so poll at a moderate rate; see Rate limits. If you need to react faster, subscribe to webhooks and use the list as the safety net that catches anything a webhook missed.

The same updated_since filter, with the same cursor paging, works on every other list of the Scheduling API, for example users and availabilities.

Update an appointment

PATCH /api/v1/appointments/{id}

Updates only the fields that do not touch the time. To move an appointment, use reschedule.

Field Type Description
external_id string Your key for the appointment.
message_to_patient string A message for the patient.
message_to_doctor string A message for the practitioner.
curl -X PATCH "https://acme.meetone.io/api/v1/appointments/c6e0b3a9-72d4-4f1b-9a58-0d3f8e2c6b17" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "message_to_doctor": "Patient asks for a sick note." }'

The response is 200 OK with the updated appointment. An external_id that is already in use returns 422 with the code taken.

Reschedule an appointment

POST /api/v1/appointments/{id}/reschedule

Moves the appointment to a new time, and optionally to another practitioner.

Field Type Required Description
starts_at string (date-time) Yes The new start.
doctor_id or doctor_external_id string No Move it to another practitioner. Default: the current one. Required for an appointment that has no practitioner (422, code blank, attribute doctor_id).
ignore_availability boolean No Skip the slot check, as when booking. Default false.
notify boolean No Tell the patient and the practitioner by email. Default true.
curl -X POST "https://acme.meetone.io/api/v1/appointments/c6e0b3a9-72d4-4f1b-9a58-0d3f8e2c6b17/reschedule" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "starts_at": "2026-10-14T10:30:00+02:00" }'
  • The new time has to be a free slot of the appointment's type, with the appointment itself not counting as taken, so you can move it by a few minutes into its own slot. Otherwise the response is 409 with the code slot_unavailable. With ignore_availability: true the slot check is skipped, but the practitioner still cannot be double-booked.
  • Only a confirmed or unconfirmed appointment can be moved. A cancelled or completed one, or one without a time, returns 409 with the code not_reschedulable.
  • Rescheduling needs appointment booking to be enabled for the organization (403, feature_disabled).
  • The slot is checked and the appointment moved under the same lock as when booking, so concurrent requests cannot take the same slot.
  • An appointment that was booked with notify: false stays silent: it sends no emails when it is rescheduled either.

The response is 200 OK with the rescheduled appointment.

Cancel an appointment

POST /api/v1/appointments/{id}/cancel

Field Type Required Description
reason string Yes Why it was cancelled. Stored as cancelled_reason.
notify boolean No Tell the patient and the practitioner by email. Default true.
curl -X POST "https://acme.meetone.io/api/v1/appointments/c6e0b3a9-72d4-4f1b-9a58-0d3f8e2c6b17/cancel" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Practitioner is ill.", "notify": true }'

The response is 200 OK with the appointment, now with status cancelled. Cancelling closes its video call. The patient and the practitioner are told by email only when the appointment still lies ahead and notify is not false; an appointment that was booked with notify: false stays silent. A missing reason returns 422 with the code blank; an appointment that is already cancelled or completed returns 409 with the code not_cancellable.

The cancelled appointment stays in the list and in your sync.

Complete an appointment

POST /api/v1/appointments/{id}/complete

Records how the appointment went and closes its video call.

Field Type Required Description
status string (enum) Yes success or no_success, for example when the patient did not show up. Stored as completed_status.
reason string Yes A short explanation. It is stored with the appointment and not returned.
curl -X POST "https://acme.meetone.io/api/v1/appointments/c6e0b3a9-72d4-4f1b-9a58-0d3f8e2c6b17/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": "no_success", "reason": "Patient did not show up." }'

The response is 200 OK with the appointment, now with status completed. An unknown status or a missing reason returns 422. Only a scheduled appointment that is neither cancelled nor completed can be completed; anything else returns 409 with the code not_completable.

Webhooks and notifications

Booking, rescheduling, cancelling and completing through the API do exactly what the same steps do in the app: they send the emails, and they send the webhooks appointment.created, appointment.reschedule, appointment.cancel and appointment.complete to the endpoints that subscribed to them. notify: false holds back the emails only, never the webhooks. See Webhook events reference.

Webhook payloads use their own, older representation, not the objects of this API. Their id, where they have one, is not the UUID id you see here. The UUID is the payload's public_id, so match a webhook to the appointment in this API by public_id. If you give every appointment you book an external_id, that is part of the payload too and matches the webhook to the appointment in your own system. To read the current state, fetch the appointment through the API.

Errors at a glance

Status Code Meaning
403 read_only_token A read-only token called an endpoint that changes data.
403 feature_disabled Rescheduling needs appointment booking to be enabled.
404 not_found The appointment, or a record you referenced, does not exist or belongs to another organization.
409 slot_unavailable The new time is not free.
409 not_reschedulable, not_cancellable, not_completable The appointment is not in a state that allows this.
422 blank, inclusion, taken, invalid A required field is missing, a value is not allowed or a time cannot be read.

Questions? Contact us at support@meetone.io.