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

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

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

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

On this page