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
- 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).
- Use the appointment group's slot IDs (appointment_group.time_slots / appointment groups API) rather than arbitrary calendar event IDs.
- Refresh the client's data if the appointment group was recently deleted or modified so stale IDs aren't reused.
- 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
- Reserve only via appointment group slot IDs from the appointments API
- Check context_type == 'AppointmentGroup' before calling reserve_for
- Refresh client state after appointment groups are edited/deleted
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
- ineligible participant
- invalid participant
- all slots filled
- cannot cancel past reservation
- Failed updating an event in the series, update not saved
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)