Consulting Meeting Mutations
A consulting meeting is a single time slot under a consulting service. These mutations create and cancel slots, move students between slots, and record a TA’s rating of an attendee after the session.
Endpoint and authentication. All admin mutations are sent to
POST /admin/graphql— not/graphql, which is the public school API. Authenticate with ansk_live_…API key in theX-Teachify-API-Keyheader. See the API Overview for details.
The eight mutations fall into two scope groups:
- Slot management (
createBulk,update,cancel,bulkCancel) requirescourses:write. - Student operations (
enroll,remove,reschedule,rate) requiresstudents:writeormembers:write.
Each subsection notes its scope explicitly.
Timestamps and timezone
Section titled “Timestamps and timezone”All startedAt / endedAt values are Unix timestamps. Convert local datetimes using the parent service’s effectiveTimezone field (an IANA string such as Asia/Taipei) before sending them.
Shared payload shape
Section titled “Shared payload shape”Single-meeting mutations return a meeting (AdminConsultingMeeting) plus an errors list. Bulk mutations return a results list — results[i] always corresponds to inputs[i] / ids[i] — plus an allSucceeded flag and a top-level errors. Each AdminConsultingMeetingBulkResult has meeting (null when that row failed) and errors (null when it succeeded).
Recoverable failures populate errors in the payload. The coded errors documented below are raised into the top-level GraphQL errors array (code in extensions.error.code); any unexpected internal failure surfaces as GENERAL-001.
The AdminConsultingMeeting type exposes: id, title, description, state, startedAt, endedAt, serviceId, lecturerId, hostUserId, hostEmail, hostingType, hostingId, joinUrl, maxAttendeeCapacity, price, currency, attendeeCount, attendees, ratingForm, attendeeRatings, discardedAt, createdAt, and updatedAt.
Three distinct person fields.
lecturerIdis the marketing profile for the meeting page (does not control who runs the session).hostUserIdis the school owner or teaching assistant who actually hosts.hostEmailis the Zoom license email, only meaningful whenhostingType: zoom. To change who runs a meeting, sethostUserId, notlecturerId.
hostingType accepts the AdminMeetingHostingType enum: zoom, live_session, or custom.
bulkCreateConsultingMeetings
Section titled “bulkCreateConsultingMeetings”Scope: courses:write
Creates multiple consulting meetings under a single service in one round trip. Set atomic: true to roll back every row when any row fails; the default (false) processes each row independently.
atomic: true must not be combined with any row that uses hostingType: zoom — Zoom provisioning calls an external API per row and cannot participate in a single transaction (raises MEETING-010).
Signature
Section titled “Signature”bulkCreateConsultingMeetings( serviceId: String! # Required. Parent consulting service ID. inputs: [AdminConsultingMeetingBulkInput!]! # Required. One row per time slot. atomic: Boolean = false # Roll back all rows if any row fails.): AdminBulkCreateConsultingMeetingsPayload!
input AdminConsultingMeetingBulkInput { startedAt: Int! # Required. Slot start (Unix timestamp). endedAt: Int! # Required. Slot end (Unix timestamp). lecturerId: String # Marketing profile; inherits the service-level lecturer if null. hostUserId: String # Owner or TA who hosts; defaults to the school owner on create. hostingType: AdminMeetingHostingType hostingId: String # External platform meeting ID (e.g. Zoom meeting ID). hostEmail: String # Zoom license email (hostingType: zoom). joinUrl: String maxAttendeeCapacity: Int # 0 or null means unlimited. price: Float title: String # Falls back to the service name when omitted. description: String}
type AdminBulkCreateConsultingMeetingsPayload { results: [AdminConsultingMeetingBulkResult!] # Same length and order as inputs. allSucceeded: Boolean # True iff every row produced a meeting. errors: [String!]}Notes on host resolution
Section titled “Notes on host resolution”hostUserIdmust be the school owner’s user id or a user with theteaching_assistantrole on this school, otherwise the row fails withMEETING-012. The error string carries a machine-parseablevalid_options=user_id_1,user_id_2suffix. When omitted, it defaults to the school owner.hostEmail(Zoom only) must match one of the school’s configured Zoom licenses. When omitted and exactly one license exists, that license is used; otherwise the row fails withMEETING-011(also carrying avalid_options=email1,email2suffix). Requires an active Zoom integration (MEETING-009).- A
hostingType: customrow requires ajoinUrl(MEETING-008).
valid_options=is capped at 20 entries. Both theMEETING-011andMEETING-012suffixes list at most 20 values. When the pool is larger, the list is truncated and the message ends with(truncated; call meetingHosts / zoomHosts for the full list). Treat the suffix as a recovery hint, not an exhaustive pool — querymeetingHosts/zoomHostsup-front to enumerate every valid value.
Zoom rows are provisioned synchronously. For a
hostingType: zoomrow, the Zoom meeting is created against the Zoom API before the mutation responds, someeting.hostingId(the Zoom meeting ID) andmeeting.joinUrlare already populated inresults[i].meeting— no follow-up query is needed. If Zoom provisioning fails, the local row is rolled back and that row returns aZoom API: …error instead of a meeting.
Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
CONSULTING-001 | Parent consulting service not found in this school. |
MEETING-009 | A row uses hostingType: zoom but the school has no active Zoom integration. |
MEETING-010 | atomic: true combined with a hostingType: zoom row. |
MEETING-008 | (per row) hostingType: custom without a joinUrl. |
MEETING-011 | (per row) Zoom host email missing or not among the school’s licenses. |
MEETING-012 | (per row) hostUserId is not the owner or a teaching assistant. |
GENERAL-001 | Unexpected internal error. |
Example
Section titled “Example”mutation { bulkCreateConsultingMeetings( serviceId: "svc-100" inputs: [ { startedAt: 1768435200, endedAt: 1768438800, hostingType: live_session } { startedAt: 1768521600, endedAt: 1768525200, hostingType: live_session } ] ) { results { meeting { id startedAt state } errors } allSucceeded }}{ "data": { "bulkCreateConsultingMeetings": { "results": [ { "meeting": { "id": "mtg-1", "startedAt": 1768435200, "state": "available" }, "errors": null }, { "meeting": { "id": "mtg-2", "startedAt": 1768521600, "state": "available" }, "errors": null } ], "allSucceeded": true } }}updateConsultingMeeting
Section titled “updateConsultingMeeting”Scope: courses:write
Partially updates a single consulting meeting. Only provided fields are changed. Explicit null values are treated as “no change”, not “clear” — there is currently no API path to clear an already-assigned lecturer or host.
Time fields (startedAt, endedAt) are rejected when the meeting has enrolled students (MEETING-003); to reschedule, remove students first or cancel and recreate the slot. All other fields remain editable on scheduled meetings.
Signature
Section titled “Signature”updateConsultingMeeting( id: String! # Required. Meeting ID. input: AdminConsultingMeetingUpdateInput!): AdminUpdateConsultingMeetingPayload!
input AdminConsultingMeetingUpdateInput { startedAt: Int # Unix timestamp. Rejected when the meeting has enrolled students. endedAt: Int lecturerId: String hostUserId: String # Owner or TA; null is a no-op. hostingType: AdminMeetingHostingType hostingId: String hostEmail: String # Zoom license email (hostingType: zoom). joinUrl: String maxAttendeeCapacity: Int price: Float title: String description: String}
type AdminUpdateConsultingMeetingPayload { meeting: AdminConsultingMeeting errors: [String!]}Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
MEETING-001 | Consulting meeting not found in this school. |
MEETING-007 | The meeting is canceled and cannot be updated. |
MEETING-003 | Time change attempted on a meeting that has enrolled students. |
MEETING-008 | Resulting hostingType: custom without a joinUrl. |
MEETING-009 | Resulting hostingType: zoom but no active Zoom integration. |
MEETING-011 | Zoom host email missing or not among the school’s licenses. |
MEETING-012 | hostUserId is not the owner or a teaching assistant. |
GENERAL-001 | Unexpected internal error. |
Example
Section titled “Example”mutation { updateConsultingMeeting( id: "mtg-1" input: { title: "Rescheduled slot", maxAttendeeCapacity: 3 } ) { meeting { id title maxAttendeeCapacity } errors }}{ "data": { "updateConsultingMeeting": { "meeting": { "id": "mtg-1", "title": "Rescheduled slot", "maxAttendeeCapacity": 3 }, "errors": null } }}cancelConsultingMeeting
Section titled “cancelConsultingMeeting”Scope: courses:write
Cancels a single consulting meeting via its AASM lifecycle. Works from both the available and scheduled states. When the meeting was hosted on Zoom, the Zoom-side meeting is deleted while the local row keeps its hosting metadata for history.
Signature
Section titled “Signature”cancelConsultingMeeting( id: String! # Required. Meeting ID.): AdminCancelConsultingMeetingPayload!
type AdminCancelConsultingMeetingPayload { meeting: AdminConsultingMeeting # state is now "canceled". errors: [String!]}Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
MEETING-001 | Consulting meeting not found in this school. |
MEETING-007 | The meeting is already canceled. |
GENERAL-001 | Unexpected internal error. |
An invalid AASM transition is returned as a message in the payload errors list rather than as a coded error.
Example
Section titled “Example”mutation { cancelConsultingMeeting(id: "mtg-1") { meeting { id state } errors }}{ "data": { "cancelConsultingMeeting": { "meeting": { "id": "mtg-1", "state": "canceled" }, "errors": null } }}bulkCancelConsultingMeetings
Section titled “bulkCancelConsultingMeetings”Scope: courses:write
Cancels multiple meetings in one round trip. results[i] corresponds to ids[i]. Set atomic: true to roll back all rows when any row fails. Rows for already-canceled meetings return MEETING-007 in their errors list.
atomic: true cannot be combined with any Zoom-hosted meeting in the batch (raises MEETING-010).
Signature
Section titled “Signature”bulkCancelConsultingMeetings( ids: [String!]! # Required. Meeting IDs; order preserved in results. atomic: Boolean = false # Roll back all rows if any row fails.): AdminBulkCancelConsultingMeetingsPayload!
type AdminBulkCancelConsultingMeetingsPayload { results: [AdminConsultingMeetingBulkResult!] # Same length and order as ids. allSucceeded: Boolean errors: [String!]}Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
MEETING-010 | atomic: true combined with a Zoom-hosted meeting in the batch. |
MEETING-001 | (per row) Meeting not found in this school. |
MEETING-007 | (per row) Meeting is already canceled. |
GENERAL-001 | Unexpected internal error. |
Example
Section titled “Example”mutation { bulkCancelConsultingMeetings(ids: ["mtg-1", "mtg-2"]) { results { meeting { id state } errors } allSucceeded }}{ "data": { "bulkCancelConsultingMeetings": { "results": [ { "meeting": { "id": "mtg-1", "state": "canceled" }, "errors": null }, { "meeting": null, "errors": ["This meeting is already canceled."] } ], "allSucceeded": false } }}enrollStudentToConsultingMeeting
Section titled “enrollStudentToConsultingMeeting”Scope: students:write or members:write
Enrolls a student into a consulting meeting. Provide either userId or email — one is required. When email is supplied and no matching student exists, a school-scoped student is created (with an optional name). Enrolling is idempotent: re-enrolling an already-enrolled student is a no-op that returns the existing meeting. The first enrollment auto-transitions an available meeting to scheduled.
Signature
Section titled “Signature”enrollStudentToConsultingMeeting( meetingId: String! # Required. userId: String # Existing student ID. Mutually exclusive with email. email: String # Student email; creates a school-scoped student if no match. name: String # Required when creating a new user by email.): AdminEnrollStudentToConsultingMeetingPayload!
type AdminEnrollStudentToConsultingMeetingPayload { meeting: AdminConsultingMeeting user: AdminUser errors: [String!]}Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
ENROLLMENT-001 | Neither userId nor email was provided. |
MEETING-001 | Consulting meeting not found in this school. |
MEETING-007 | The meeting is canceled. |
ENROLLMENT-002 | userId does not reference a student in this school. |
MEETING-004 | Meeting is at maximum attendee capacity. |
ENROLLMENT-005 | Failed to create the student (from an email that matched no existing user). |
ENROLLMENT-006 | email is missing or fails format validation. |
ENROLLMENT-007 | name is required when creating a new user by email. |
GENERAL-001 | Unexpected internal error. |
Example
Section titled “Example”mutation { enrollStudentToConsultingMeeting( meetingId: "mtg-1" email: "alice@example.com" name: "Alice" ) { meeting { id state attendeeCount } user { id email } errors }}{ "data": { "enrollStudentToConsultingMeeting": { "meeting": { "id": "mtg-1", "state": "scheduled", "attendeeCount": 1 }, "user": { "id": "usr-9", "email": "alice@example.com" }, "errors": null } }}removeStudentFromConsultingMeeting
Section titled “removeStudentFromConsultingMeeting”Scope: students:write or members:write
Removes a student from a consulting meeting. If this leaves a scheduled meeting with zero attendees, the state auto-reverts to available so the slot is discoverable again via state filters. Refuses to operate on a canceled meeting.
Signature
Section titled “Signature”removeStudentFromConsultingMeeting( meetingId: String! # Required. userId: String! # Required.): AdminRemoveStudentFromConsultingMeetingPayload!
type AdminRemoveStudentFromConsultingMeetingPayload { meeting: AdminConsultingMeeting errors: [String!]}Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
MEETING-001 | Consulting meeting not found in this school. |
MEETING-007 | The meeting is canceled. |
MEETING-006 | The student is not enrolled in this meeting. |
GENERAL-001 | Unexpected internal error. |
Example
Section titled “Example”mutation { removeStudentFromConsultingMeeting(meetingId: "mtg-1", userId: "usr-9") { meeting { id state attendeeCount } errors }}{ "data": { "removeStudentFromConsultingMeeting": { "meeting": { "id": "mtg-1", "state": "available", "attendeeCount": 0 }, "errors": null } }}rescheduleStudentConsultingMeeting
Section titled “rescheduleStudentConsultingMeeting”Scope: students:write or members:write
Atomically moves a student from one meeting to another in a single transaction — replacing the two-call remove + enroll pattern, where the enroll leg can fail on capacity and leave the student in neither slot. If the target meeting is full, the whole operation rolls back and the student remains in the source meeting.
Convergent/idempotent: if the student is already enrolled in the target (e.g. a retried call), the enroll step is skipped and the student is still removed from the source if present. Refuses to move a student out of a meeting that has already ended (MEETING-020), since destroying the source enrollment would cascade-destroy any rating recorded for that session.
The ended-meeting guard applies to the source only — moving a student into a meeting that has already ended is allowed, which is useful for back-filling attendance.
Cross-service moves are allowed: fromMeetingId and toMeetingId may belong to different consulting services, as long as both resolve to consulting meetings under this school.
Signature
Section titled “Signature”rescheduleStudentConsultingMeeting( userId: String! # Required. fromMeetingId: String! # Required. toMeetingId: String! # Required.): AdminRescheduleStudentConsultingMeetingPayload!
type AdminRescheduleStudentConsultingMeetingPayload { fromMeeting: AdminConsultingMeeting toMeeting: AdminConsultingMeeting errors: [String!]}Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
MEETING-001 | Source or target meeting not found in this school. |
MEETING-007 | Source or target meeting is canceled. |
MEETING-019 | Source and target are the same meeting. |
MEETING-006 | The student is enrolled in neither the source nor the target meeting. |
MEETING-020 | The source meeting has already ended (moving out would destroy its rating). |
MEETING-004 | The target meeting is at maximum attendee capacity. |
GENERAL-001 | Unexpected internal error. |
Example
Section titled “Example”mutation { rescheduleStudentConsultingMeeting( userId: "usr-9" fromMeetingId: "mtg-1" toMeetingId: "mtg-2" ) { fromMeeting { id state attendeeCount } toMeeting { id state attendeeCount } errors }}{ "data": { "rescheduleStudentConsultingMeeting": { "fromMeeting": { "id": "mtg-1", "state": "available", "attendeeCount": 0 }, "toMeeting": { "id": "mtg-2", "state": "scheduled", "attendeeCount": 1 }, "errors": null } }}rateConsultingMeeting
Section titled “rateConsultingMeeting”Scope: students:write or members:write
Records a TA’s rating of an already-enrolled student after the session — the integrator-side equivalent of the /teaching reservation UI. The rating mode is fixed by the service’s rating form (see AdminConsultingMeeting.ratingForm):
- default mode (no rating form) — provide
score(0.5–10 in 0.5 steps) and an optionalcomment. - custom mode (rating form configured) — provide
answers, one entry per answered field.
Provide either userId or email to identify the enrolled student (mutually exclusive). The rating is attributed to a grader: pass raterId (the school owner or a teaching assistant), or omit it to attribute to the meeting’s host user. Upsert semantics: calling again replaces the student’s entire existing rating — in custom mode, send the complete answer set each time. The student is notified by email. Address-type form fields are not supported.
Signature
Section titled “Signature”rateConsultingMeeting( meetingId: String! # Required. userId: String # Enrolled student ID. Mutually exclusive with email. email: String # Enrolled student email (resolved within this school). Mutually exclusive with userId. raterId: String # Grader (owner or TA). Defaults to the meeting host user when omitted. score: Float # Default mode only: 0.5-10 in 0.5 steps. comment: String # Default mode only: optional free-text comment. answers: [AnswerInput!] # Custom mode only: one entry per answered form field.): AdminRateConsultingMeetingPayload!
input AnswerInput { fieldId: String! value: String values: [String!]}
type AdminRateConsultingMeetingPayload { attendeeRating: AdminConsultingAttendeeRating errors: [String!]}The AdminConsultingAttendeeRating payload exposes userId, email, name, rate (default-mode score/comment), and submission (custom-mode form answers).
Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
ENROLLMENT-001 | Neither userId nor email was provided. |
MEETING-016 | Both userId and email were provided. |
MEETING-001 | Consulting meeting not found in this school. |
MEETING-007 | The meeting is canceled. |
ENROLLMENT-002 | The student was not found in this school. |
MEETING-006 | The student is not enrolled in this meeting. |
MEETING-017 | raterId is not a valid grader (owner or teaching assistant). |
MEETING-018 | No raterId given and the meeting host is not a valid grader. |
MEETING-013 | Input does not match the service’s rating mode (e.g. score in custom mode, or answers in default mode). |
MEETING-014 | score is missing or outside 0.5–10 in 0.5 steps (default mode). |
MEETING-015 | Answer validation failed — unknown/duplicate fieldId, both value and values set, missing required field, invalid option, or an unsupported (address-type) field. |
GENERAL-001 | Unexpected internal error. |
Example (default mode)
Section titled “Example (default mode)”mutation { rateConsultingMeeting( meetingId: "mtg-1" userId: "usr-9" score: 8.5 comment: "Great progress" ) { attendeeRating { userId rate { score comment } } errors }}{ "data": { "rateConsultingMeeting": { "attendeeRating": { "userId": "usr-9", "rate": { "score": 8.5, "comment": "Great progress" } }, "errors": null } }}Related Resources
Section titled “Related Resources”For more information about the Teachify Admin API, please refer to the API Overview.