Loopwise Docs
Admin APIMutations

Consulting Mutations

Mutation operations for managing consulting services and their time-slot meetings in your Loopwise school

The consulting mutations let you programmatically manage consulting services (1-on-1 / group coaching offerings) and the time-slot meetings booked under them.

Endpoint and authentication. All admin mutations live on POST /admin/graphqlnot the /graphql endpoint, which is the public school API. Authenticate with an sk_live_… key in the X-Teachify-API-Key header. Service mutations require the courses:write scope; meeting-enrollment mutations require students:write (or members:write).

Service entity mutations (require courses:write):

  • createConsultingService — Create a new consulting service under a course
  • updateConsultingService — Partial update of an existing service
  • deleteConsultingService — Soft-delete a service

Meeting (time-slot) mutations (require courses:write):

  • bulkCreateConsultingMeetings — Create multiple time slots under a service in one round trip
  • updateConsultingMeeting — Partial update of a single meeting
  • cancelConsultingMeeting — Cancel a single meeting
  • bulkCancelConsultingMeetings — Cancel multiple meetings in one round trip

Meeting enrollment mutations (require students:write or members:write):

  • enrollStudentToConsultingMeeting — Add a student to a meeting
  • removeStudentFromConsultingMeeting — Remove a student from a meeting
  • rescheduleStudentConsultingMeeting — Atomically move a student from one meeting to another

All mutations return an errors field with any validation or processing failures. Field names use camelCase on the wire; type names in this document use the GraphQL schema name (e.g. AdminConsultingServiceInput).


Create a Consulting Service

The createConsultingService mutation creates a new consulting service under an existing course.

Input Parameters

FieldTypeDescription
inputAdminConsultingServiceInput!Input object

AdminConsultingServiceInput Fields

FieldTypeRequiredDescription
nameString!YesDisplay name of the service
courseIdString!YesParent course ID. Course must belong to the same school
slugStringNoURL slug (lowercase letters, numbers, hyphens). If omitted, derived from name by friendly_id
descriptionStringNoService description
lecturerIdStringNoAssigned lecturer ID. Must belong to the same school
publishedBooleanNoWhen true, marks the service as published and stamps publishedAt to the current time
tags[String!]NoTag list stored in settings JSON. Replaces the entire list — pass all tags every time
ratingFormIdStringNoLinked feedback form ID
backgroundColorStringNoUI background color (hex)

Return Type

type AdminConsultingServiceCreatePayload {
  consultingService: AdminConsultingService
  errors: [String!]
}

Example

mutation CreateConsultingService {
  createConsultingService(input: {
    name: "1-on-1 Career Coaching"
    courseId: "ab6ee332-614d-475d-93b5-abc5ebb1fc84"
    slug: "career-coaching"
    description: "30-minute career coaching session"
    lecturerId: "f1b2c3d4-5678-90ef-abcd-ef1234567890"
    published: true
    tags: ["career", "coaching"]
  }) {
    consultingService {
      id
      name
      slug
      published
      publishedAt
      effectiveTimezone
    }
    errors
  }
}

Sample Response

{
  "data": {
    "createConsultingService": {
      "consultingService": {
        "id": "35a72dba-c3cc-4a06-b7eb-d784d010386e",
        "name": "1-on-1 Career Coaching",
        "slug": "career-coaching",
        "published": true,
        "publishedAt": 1747397869,
        "effectiveTimezone": "Asia/Taipei"
      },
      "errors": null
    }
  }
}

Common Errors

Error codeMessage
CONSULTING-002Parent course not found or not in this school
CONSULTING-003Slug must only contain lowercase letters, numbers, and hyphens
CONSULTING-005Lecturer not found or not in this school
CONSULTING-006Rating form not found or not in this school

Update a Consulting Service

The updateConsultingService mutation applies a partial update. Only provided keys are changed; explicit null clears nullable fields (lecturerId, ratingFormId, backgroundColor).

Not updatable: courseId is intentionally read-only. Reparenting a service across courses would orphan its meetings and enrollments — create a new service instead.

Publish-state semantics: setting published: false does not clear publishedAt; the historical first-publish timestamp is preserved.

Lecturer reassignment: changes the service-level lecturer only. Existing meetings keep the lecturer they were created with — update each meeting individually if you need to reassign past or scheduled slots.

Input Parameters

FieldTypeDescription
idString!ID of the consulting service to update
inputAdminConsultingServiceUpdateInput!Partial-update fields

AdminConsultingServiceUpdateInput Fields

FieldTypeDescription
nameStringDisplay name
slugStringURL slug
descriptionStringService description
lecturerIdStringLecturer ID; pass null to remove
publishedBooleanSet to true to publish (auto-stamps publishedAt on first publish)
tags[String!]Tag list. Replaces the entire list
ratingFormIdStringLinked form ID; pass null to remove
backgroundColorStringUI background color (hex); pass null to remove

Example

mutation UpdateConsultingService {
  updateConsultingService(
    id: "35a72dba-c3cc-4a06-b7eb-d784d010386e"
    input: {
      description: "Updated 45-minute career coaching session"
      tags: ["career", "coaching", "premium"]
    }
  ) {
    consultingService {
      id
      name
      description
      tags
    }
    errors
  }
}

Common Errors

Error codeMessage
CONSULTING-001Consulting service not found
CONSULTING-003Slug must only contain lowercase letters, numbers, and hyphens
CONSULTING-005Lecturer not found or not in this school
CONSULTING-006Rating form not found or not in this school

Delete a Consulting Service

The deleteConsultingService mutation soft-deletes (discards) a consulting service. Refuses to delete when the service still has undiscarded, non-canceled future meetings — cancel them via bulkCancelConsultingMeetings (or wait for them to end) first.

Input Parameters

FieldTypeDescription
idString!ID of the consulting service to delete

Example

mutation DeleteConsultingService {
  deleteConsultingService(id: "35a72dba-c3cc-4a06-b7eb-d784d010386e") {
    consultingService {
      id
      discardedAt
    }
    errors
  }
}

Common Errors

Error codeMessage
CONSULTING-001Consulting service not found
CONSULTING-004Cannot delete a consulting service that still has undiscarded, non-canceled future meetings. Cancel them first

Bulk Create Consulting Meetings

The bulkCreateConsultingMeetings mutation creates multiple time slots under a single consulting service in one request. results[i] always corresponds to inputs[i].

⚠️ atomic: true + hostingType: zoom is rejected. Zoom rows provision against the Zoom API per-row, which makes a clean rollback impossible. The mutation top-level-fails with MEETING-010 if you mix them. Leave atomic: false (the default) whenever any row is Zoom-hosted — even an agent that defaults to atomic: true "for safety" will trip this on the first Zoom batch.

Time and Timezone

startedAt and endedAt are Unix timestamps (seconds since epoch). Convert from local datetimes using the parent service's effectiveTimezone field (IANA name, e.g. Asia/Taipei) before sending.

Input Parameters

FieldTypeDescription
serviceIdString!Parent consulting service ID
inputs[AdminConsultingMeetingBulkInput!]!Time-slot rows
atomicBooleanWhen true, all rows roll back if any row fails. Default false (per-row independent)

AdminConsultingMeetingBulkInput Fields

Meeting host model — three independent concepts. A consulting meeting carries three different "host-ish" identifiers, and they are not interchangeable:

  • lecturerId — the public-facing Lecturer profile (slug, URL routing, slot-overlap detection). Managed in the school admin's Lecturers section. List via the lecturers query.
  • hostUserId — the user (school owner or teaching assistant) who actually runs the session. Surfaced to enrolled students. This is the "host" the school admin UI exposes on the meeting edit form. List valid hosts up-front via meetingHosts to avoid MEETING-012.
  • hostEmail — the Zoom license email used to provision the Zoom room when hostingType: zoom. A Zoom seat, not a person record. List valid seats up-front via zoomHosts to avoid MEETING-011.

For a Zoom meeting you typically set all three: lecturerId (whose profile owns the slot), hostUserId (who clicks "join" on the day), and hostEmail (which Zoom seat hosts the room). They can intentionally be three different people/seats.

FieldTypeRequiredDescription
startedAtInt!YesSlot start, Unix timestamp
endedAtInt!YesSlot end, Unix timestamp
lecturerIdStringNoLecturer profile (slug, URL routing, slot-overlap detection). NOT the person who hosts the session — use hostUserId for that. Inherits the service-level lecturer when omitted
hostUserIdStringNoThe user who actually hosts the meeting. Must be the school owner's user id OR a user with the teaching_assistant role in this school; otherwise MEETING-012. Defaults to the school owner when omitted
hostingTypeMeetingHostingTypeNozoom, live_session, or custom
hostingIdStringNoExternal hosting platform meeting ID (e.g. Zoom meeting ID)
hostEmailStringNoZoom license email for hostingType: zoom — see Zoom host resolution below. Independent of hostUserId
joinUrlStringNoPre-set join URL. Required when hostingType: custom
maxAttendeeCapacityIntNo0 or null means unlimited
priceFloatNoPer-slot price; falls back to service-level pricing if omitted
titleStringNoFalls back to the service name when omitted
descriptionStringNo

Zoom host resolution

When hostingType: zoom:

  1. If hostEmail is provided, it must match one of the school's configured Zoom integration licenses (Settings → Integrations → Zoom). Prefer listing them via zoomHosts up-front.
  2. If omitted and the school has exactly one Zoom license, that license is used automatically.
  3. Otherwise, the row fails with MEETING-011. The error string carries a machine-parseable valid_options=email1,email2 suffix so agents can recover without parsing prose.

On successful return for Zoom rows: the mutation provisions the Zoom meeting synchronously before responding, so meeting.hostingId (Zoom meeting ID) and meeting.joinUrl are populated by the time you read them in results[i].meeting. No follow-up query needed.

Return Type

type AdminBulkCreateConsultingMeetingsPayload {
  results: [AdminConsultingMeetingBulkResult!]
  allSucceeded: Boolean
  errors: [String!]
}

type AdminConsultingMeetingBulkResult {
  meeting: AdminConsultingMeeting   # null when the row failed
  errors: [String!]                  # null when the row succeeded
}

Example

mutation BulkCreateConsultingMeetings {
  bulkCreateConsultingMeetings(
    serviceId: "35a72dba-c3cc-4a06-b7eb-d784d010386e"
    atomic: false
    inputs: [
      {
        startedAt: 1748390400
        endedAt: 1748392200
        hostingType: live_session
        maxAttendeeCapacity: 1
      }
      {
        startedAt: 1748394000
        endedAt: 1748395800
        hostingType: zoom
        hostEmail: "host@example.com"          # Zoom license seat
        hostUserId: "9b8a7c65-1234-5678-90ab-cdef01234567"  # the human who runs it (owner or TA)
        maxAttendeeCapacity: 1
      }
    ]
  ) {
    allSucceeded
    results {
      meeting {
        id
        title
        state
        startedAt
        endedAt
        hostingType
        joinUrl
      }
      errors
    }
    errors
  }
}

Common Errors (per row or top-level)

Error codeMessage
CONSULTING-001Consulting service not found (top-level — bad serviceId)
MEETING-008Custom hosting type requires joinUrl
MEETING-009School has no active Zoom integration; configure Zoom under integrations first
MEETING-010Zoom hosting type cannot be combined with atomic: true; use atomic: false to allow per-row Zoom provisioning
MEETING-011hostEmail is required or invalid — error carries valid_options=email1,email2 suffix (capped at 20 entries; call zoomHosts for the full list)
MEETING-012hostUserId must be the school owner or a teaching assistant — error carries valid_options=user_id_1,user_id_2 suffix (capped at 20 entries; call meetingHosts for the full list)

Update a Consulting Meeting

The updateConsultingMeeting mutation applies a partial update. Time fields (startedAt, endedAt) are blocked when the meeting has enrolled students (see MEETING-003); remove students first, or cancel and recreate the slot, to reschedule. (To move an individual student to a different slot instead, see rescheduleStudentConsultingMeeting.) Other fields — including hostUserId, lecturerId, hostingType, title, maxAttendeeCapacity, etc. — are NOT blocked by MEETING-003 and can be changed on a scheduled meeting (e.g. to reassign hosts when staff turn over).

Explicit-null semantics: explicit null is treated as no change, not "clear". Sending { lecturerId: null } keeps the existing lecturer. There is currently no API path to clear an already-assigned lecturer.

Input Parameters

FieldTypeDescription
idString!Meeting ID
inputAdminConsultingMeetingUpdateInput!Partial-update fields

AdminConsultingMeetingUpdateInput Fields

The three independent host concepts (lecturerId / hostUserId / hostEmail) are explained in the bulk-create section above.

FieldTypeDescription
startedAtIntSlot start, Unix timestamp
endedAtIntSlot end, Unix timestamp
lecturerIdStringReassign Lecturer profile (not the actual host — see hostUserId)
hostUserIdStringReassign the user who runs the meeting (school owner or teaching assistant); MEETING-012 otherwise
hostingTypeMeetingHostingTypezoom, live_session, or custom
hostingIdString
hostEmailStringZoom license email — same resolution rules as bulk-create
joinUrlString
maxAttendeeCapacityInt
priceFloat
titleString
descriptionString

Return Type

type AdminUpdateConsultingMeetingPayload {
  meeting: AdminConsultingMeeting
  errors: [String!]
}

Example

mutation UpdateConsultingMeeting {
  updateConsultingMeeting(
    id: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1"
    input: {
      title: "Rescheduled coaching session"
      maxAttendeeCapacity: 2
    }
  ) {
    meeting {
      id
      title
      maxAttendeeCapacity
      state
    }
    errors
  }
}

Common Errors

Error codeMessage
MEETING-001Consulting meeting not found
MEETING-003Cannot reschedule meeting with enrolled students
MEETING-007Meeting is already canceled
MEETING-008Custom hosting type requires joinUrl
MEETING-009School has no active Zoom integration
MEETING-011hostEmail is required or invalid — error carries valid_options=email1,email2 suffix (capped at 20 entries; call zoomHosts for the full list)
MEETING-012hostUserId must be the school owner or a teaching assistant — error carries valid_options=user_id_1,user_id_2 suffix (capped at 20 entries; call meetingHosts for the full list)

Cancel a Consulting Meeting

The cancelConsultingMeeting mutation cancels a single meeting via the underlying AASM state machine. Works from both available and scheduled states. For Zoom-hosted meetings, the remote Zoom meeting is also deleted; the local row keeps its hosting metadata for history.

Input Parameters

FieldTypeDescription
idString!Meeting ID

Example

mutation CancelConsultingMeeting {
  cancelConsultingMeeting(id: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1") {
    meeting {
      id
      state
    }
    errors
  }
}

Common Errors

Error codeMessage
MEETING-001Consulting meeting not found
MEETING-007Meeting is already canceled

Bulk Cancel Consulting Meetings

The bulkCancelConsultingMeetings mutation cancels multiple meetings in one round trip. results[i] always corresponds to ids[i]. Already-canceled meetings produce MEETING-007 in their per-row errors. For Zoom-hosted meetings, the remote Zoom meeting is also deleted (same behavior as the single cancelConsultingMeeting); the local rows keep their hosting metadata for history.

Input Parameters

FieldTypeDescription
ids[String!]!Meeting IDs; order preserved in results
atomicBooleanWhen true, all rows roll back if any row fails. Default false

Example

mutation BulkCancelConsultingMeetings {
  bulkCancelConsultingMeetings(
    ids: ["meeting-id-a", "meeting-id-b"]
    atomic: false
  ) {
    allSucceeded
    results {
      meeting {
        id
        state
      }
      errors
    }
  }
}

Common Errors

Same per-row codes as the single cancel: MEETING-001, MEETING-007. Top-level MEETING-010 if Zoom-hosted meetings are present and atomic: true.


Enroll a Student to a Consulting Meeting

The enrollStudentToConsultingMeeting mutation adds a student to a meeting. Provide either userId or email — one is required. Re-enrolling an already-enrolled student is a no-op and returns the existing meeting. An available meeting auto-transitions to scheduled on the first enrollment. If the student is being moved from another slot, use rescheduleStudentConsultingMeeting instead of remove + enroll.

Scope: this mutation requires students:write (or members:write), not courses:write.

Input Parameters

FieldTypeRequiredDescription
meetingIdString!YesTarget meeting ID
userIdStringNo*Existing student ID. Mutually exclusive with email
emailStringNo*Student email; creates a school-scoped student if no match
nameStringNoRequired when creating a new user by email

*Either userId or email must be provided.

Example

mutation EnrollStudent {
  enrollStudentToConsultingMeeting(
    meetingId: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1"
    email: "student@example.com"
    name: "John Doe"
  ) {
    meeting {
      id
      state
      attendeeCount
    }
    user {
      id
      email
      name
    }
    errors
  }
}

Common Errors

Error codeMessage
MEETING-001Consulting meeting not found
MEETING-004Meeting has reached its maximum attendee capacity
MEETING-007Meeting is already canceled
ENROLLMENT-001Either userId or email must be provided
ENROLLMENT-002Student not found
ENROLLMENT-005Failed to create student
ENROLLMENT-006Invalid email
ENROLLMENT-007Name is required when creating a new user

Remove a Student from a Consulting Meeting

The removeStudentFromConsultingMeeting mutation removes a student's enrollment. 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 (MEETING-007). To move a student to a different slot, do not pair this with an enroll call — use rescheduleStudentConsultingMeeting, which is atomic.

Scope: this mutation requires students:write (or members:write).

Input Parameters

FieldTypeDescription
meetingIdString!Target meeting ID
userIdString!Student to remove

Example

mutation RemoveStudent {
  removeStudentFromConsultingMeeting(
    meetingId: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1"
    userId: "387b0c87-0f85-44cc-833e-7306c1b5d3e6"
  ) {
    meeting {
      id
      state
      attendeeCount
    }
    errors
  }
}

Common Errors

Error codeMessage
MEETING-001Consulting meeting not found
MEETING-006Student is not enrolled in this meeting
MEETING-007Meeting is already canceled

Reschedule a Student's Consulting Meeting

The rescheduleStudentConsultingMeeting mutation atomically moves a student from one meeting to another in a single transaction.

Why not remove + enroll? Calling removeStudentFromConsultingMeeting followed by enrollStudentToConsultingMeeting is two independent, non-atomic calls — if the enroll leg fails (e.g. the target filled up between your two calls), the student ends up in neither slot. rescheduleStudentConsultingMeeting does both steps inside one DB transaction: if the target is at capacity, the whole operation rolls back and the student remains enrolled in the source meeting.

Ended-meeting guard: refuses to reschedule out of a meeting that has already ended (MEETING-020). Destroying the source enrollment cascades to destroy any rating already recorded for that session, so once a meeting has happened its attendees are locked in place. Moving a student into a meeting that has already ended is allowed (same behavior as enrollStudentToConsultingMeeting) — useful for back-filling attendance.

Idempotent / retry-safe: if the student is already enrolled in the target meeting (e.g. a retried call after a network timeout), the create step is skipped — the student is still removed from the source if present — and the mutation returns success.

Cross-service moves are allowed: fromMeetingId and toMeetingId can belong to different consulting services, as long as both resolve under this school.

Scope: this mutation requires students:write (or members:write), the same scope as the other enrollment mutations.

Input Parameters

FieldTypeRequiredDescription
userIdString!YesStudent to move
fromMeetingIdString!YesSource meeting ID
toMeetingIdString!YesTarget meeting ID

Return Type

type AdminRescheduleStudentConsultingMeetingPayload {
  fromMeeting: AdminConsultingMeeting
  toMeeting: AdminConsultingMeeting
  errors: [String!]
}

Example

mutation RescheduleStudent {
  rescheduleStudentConsultingMeeting(
    userId: "387b0c87-0f85-44cc-833e-7306c1b5d3e6"
    fromMeetingId: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1"
    toMeetingId: "6f6f8b2f-6e0f-4a3f-9a3d-5f4c9c1b2e3a"
  ) {
    fromMeeting {
      id
      state
      attendeeCount
    }
    toMeeting {
      id
      state
      attendeeCount
    }
    errors
  }
}

Sample Response

{
  "data": {
    "rescheduleStudentConsultingMeeting": {
      "fromMeeting": {
        "id": "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1",
        "state": "available",
        "attendeeCount": 0
      },
      "toMeeting": {
        "id": "6f6f8b2f-6e0f-4a3f-9a3d-5f4c9c1b2e3a",
        "state": "scheduled",
        "attendeeCount": 1
      },
      "errors": null
    }
  }
}

Common Errors

Error codeMessage
MEETING-001Consulting meeting not found (either fromMeetingId or toMeetingId)
MEETING-004Target meeting has reached its maximum attendee capacity — the whole operation rolls back, the student stays enrolled in the source meeting
MEETING-006Student is not enrolled in the source meeting (and not already in the target)
MEETING-007Either meeting is already canceled
MEETING-019fromMeetingId and toMeetingId are identical
MEETING-020Cannot reschedule from a meeting that has already ended

On this page