instructure/canvas-lms · error · CalendarEvent::ReservationError

not an appointment

Error message

not an appointment

What it means

Raised by CalendarEvent#reserve_for when someone tries to make a reservation on a calendar event whose context_type is not 'AppointmentGroup'. Only appointment-group time slots can be reserved; regular course/user/account calendar events cannot. It is a Canvas ReservationError, typically surfaced through the appointments API.

Solutions

  1. Verify the event is a time slot of an AppointmentGroup before reserving: check event.context_type == 'AppointmentGroup' (or fetch the slot via the appointment group's time slots API).
  2. Use the appointment group's slot IDs (appointment_group.time_slots / appointment groups API) rather than arbitrary calendar event IDs.
  3. Refresh the client's data if the appointment group was recently deleted or modified so stale IDs aren't reused.
  4. Catch ReservationError in the calling code and return a 400-style response to the user.

Example fix

# before
slot = CalendarEvent.find(params[:event_id])
slot.reserve_for(participant, user)
# after
slot = CalendarEvent.find(params[:event_id])
raise ReservationError, "not an appointment" unless slot.context_type == "AppointmentGroup"
slot.reserve_for(participant, user)
Defensive patterns

Strategy: validation

Validate before calling

slot = CalendarEvent.find(event_id)
raise ReservationError, "not an appointment" unless slot.context_type == "AppointmentGroup"

Try / catch

begin
  slot.reserve_for(participant, user)
rescue CalendarEvent::ReservationError => e
  render json: { error: e.message }, status: :bad_request
end

Prevention

When it happens

Trigger: An API client or controller calls event.reserve_for(participant, user) on a CalendarEvent loaded by ID where context_type is 'Course', 'User', or 'Account' instead of 'AppointmentGroup' — e.g. the client passed a normal calendar event's ID to the appointment reservation endpoint.

Common situations: Frontend cached an event ID from an appointment group that was later converted/deleted and replaced by a regular event; API consumer confuses calendar event IDs with appointment group slot IDs; importing/copying courses turns appointment slots into plain events.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/6ce9861d68f8fbdf. Report an issue: GitHub.

Appendix: source

Thrown at app/models/calendar_event.rb:592

    context_type == "AppointmentGroup" || parent_event.try(:context_type) == "AppointmentGroup"
  end

  def appointment_group
    if parent_event.try(:context).is_a?(AppointmentGroup)
      parent_event.context
    elsif context_type == "AppointmentGroup"
      context
    end
  end

  def account
    (context_type == "Account") ? context : nil
  end

  class ReservationError < StandardError; end

  def reserve_for(participant, user, options = {})
    raise ReservationError, "not an appointment" unless context_type == "AppointmentGroup"
    raise ReservationError, "ineligible participant" unless context.eligible_participant?(participant)

    transaction do
      lock! # in case two people two participants try to grab the same slot
      participant.lock! # in case two people try to make a reservation for the same participant

      if options[:cancel_existing]
        context.reservations_for(participant).lock.each do |reservation|
          raise ReservationError, "cannot cancel past reservation" if reservation.end_at < Time.now.utc

          reservation.updating_user = user
          reservation.destroy
        end
      end

      raise ReservationError, "participant has met per-participant limit" if context.max_appointments_per_participant && context.reservations_for(participant).size >= context.max_appointments_per_participant
      raise ReservationError, "all slots filled" if participants_per_appointment && child_events.size >= participants_per_appointment
      raise ReservationError, "participant has already reserved this appointment" if child_events_for(participant).present?

View on GitHub (pinned to 1c9f0bb801)