Skip to content

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/graphqlnot /graphql, which is the public school API. Authenticate with an sk_live_… API key in the X-Teachify-API-Key header. See the API Overview for details.

The eight mutations fall into two scope groups:

  • Slot management (createBulk, update, cancel, bulkCancel) requires courses:write.
  • Student operations (enroll, remove, reschedule, rate) requires students:write or members:write.

Each subsection notes its scope explicitly.

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.

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. lecturerId is the marketing profile for the meeting page (does not control who runs the session). hostUserId is the school owner or teaching assistant who actually hosts. hostEmail is the Zoom license email, only meaningful when hostingType: zoom. To change who runs a meeting, set hostUserId, not lecturerId.

hostingType accepts the AdminMeetingHostingType enum: zoom, live_session, or custom.


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).

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!]
}
  • hostUserId must be the school owner’s user id or a user with the teaching_assistant role on this school, otherwise the row fails with MEETING-012. The error string carries a machine-parseable valid_options=user_id_1,user_id_2 suffix. 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 with MEETING-011 (also carrying a valid_options=email1,email2 suffix). Requires an active Zoom integration (MEETING-009).
  • A hostingType: custom row requires a joinUrl (MEETING-008).

valid_options= is capped at 20 entries. Both the MEETING-011 and MEETING-012 suffixes 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 — query meetingHosts / zoomHosts up-front to enumerate every valid value.

Zoom rows are provisioned synchronously. For a hostingType: zoom row, the Zoom meeting is created against the Zoom API before the mutation responds, so meeting.hostingId (the Zoom meeting ID) and meeting.joinUrl are already populated in results[i].meeting — no follow-up query is needed. If Zoom provisioning fails, the local row is rolled back and that row returns a Zoom API: … error instead of a meeting.

CodeMeaning
CONSULTING-001Parent consulting service not found in this school.
MEETING-009A row uses hostingType: zoom but the school has no active Zoom integration.
MEETING-010atomic: 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-001Unexpected internal error.
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
}
}
}

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.

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!]
}
CodeMeaning
MEETING-001Consulting meeting not found in this school.
MEETING-007The meeting is canceled and cannot be updated.
MEETING-003Time change attempted on a meeting that has enrolled students.
MEETING-008Resulting hostingType: custom without a joinUrl.
MEETING-009Resulting hostingType: zoom but no active Zoom integration.
MEETING-011Zoom host email missing or not among the school’s licenses.
MEETING-012hostUserId is not the owner or a teaching assistant.
GENERAL-001Unexpected internal error.
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
}
}
}

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.

cancelConsultingMeeting(
id: String! # Required. Meeting ID.
): AdminCancelConsultingMeetingPayload!
type AdminCancelConsultingMeetingPayload {
meeting: AdminConsultingMeeting # state is now "canceled".
errors: [String!]
}
CodeMeaning
MEETING-001Consulting meeting not found in this school.
MEETING-007The meeting is already canceled.
GENERAL-001Unexpected internal error.

An invalid AASM transition is returned as a message in the payload errors list rather than as a coded error.

mutation {
cancelConsultingMeeting(id: "mtg-1") {
meeting { id state }
errors
}
}
{
"data": {
"cancelConsultingMeeting": {
"meeting": { "id": "mtg-1", "state": "canceled" },
"errors": null
}
}
}

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).

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!]
}
CodeMeaning
MEETING-010atomic: 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-001Unexpected internal error.
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
}
}
}

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.

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!]
}
CodeMeaning
ENROLLMENT-001Neither userId nor email was provided.
MEETING-001Consulting meeting not found in this school.
MEETING-007The meeting is canceled.
ENROLLMENT-002userId does not reference a student in this school.
MEETING-004Meeting is at maximum attendee capacity.
ENROLLMENT-005Failed to create the student (from an email that matched no existing user).
ENROLLMENT-006email is missing or fails format validation.
ENROLLMENT-007name is required when creating a new user by email.
GENERAL-001Unexpected internal error.
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
}
}
}

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.

removeStudentFromConsultingMeeting(
meetingId: String! # Required.
userId: String! # Required.
): AdminRemoveStudentFromConsultingMeetingPayload!
type AdminRemoveStudentFromConsultingMeetingPayload {
meeting: AdminConsultingMeeting
errors: [String!]
}
CodeMeaning
MEETING-001Consulting meeting not found in this school.
MEETING-007The meeting is canceled.
MEETING-006The student is not enrolled in this meeting.
GENERAL-001Unexpected internal error.
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
}
}
}

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.

rescheduleStudentConsultingMeeting(
userId: String! # Required.
fromMeetingId: String! # Required.
toMeetingId: String! # Required.
): AdminRescheduleStudentConsultingMeetingPayload!
type AdminRescheduleStudentConsultingMeetingPayload {
fromMeeting: AdminConsultingMeeting
toMeeting: AdminConsultingMeeting
errors: [String!]
}
CodeMeaning
MEETING-001Source or target meeting not found in this school.
MEETING-007Source or target meeting is canceled.
MEETING-019Source and target are the same meeting.
MEETING-006The student is enrolled in neither the source nor the target meeting.
MEETING-020The source meeting has already ended (moving out would destroy its rating).
MEETING-004The target meeting is at maximum attendee capacity.
GENERAL-001Unexpected internal error.
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
}
}
}

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 optional comment.
  • 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.

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).

CodeMeaning
ENROLLMENT-001Neither userId nor email was provided.
MEETING-016Both userId and email were provided.
MEETING-001Consulting meeting not found in this school.
MEETING-007The meeting is canceled.
ENROLLMENT-002The student was not found in this school.
MEETING-006The student is not enrolled in this meeting.
MEETING-017raterId is not a valid grader (owner or teaching assistant).
MEETING-018No raterId given and the meeting host is not a valid grader.
MEETING-013Input does not match the service’s rating mode (e.g. score in custom mode, or answers in default mode).
MEETING-014score is missing or outside 0.5–10 in 0.5 steps (default mode).
MEETING-015Answer validation failed — unknown/duplicate fieldId, both value and values set, missing required field, invalid option, or an unsupported (address-type) field.
GENERAL-001Unexpected internal error.
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
}
}
}

For more information about the Teachify Admin API, please refer to the API Overview.