Skip to content

Create Student

The createStudent mutation creates a new student in the current school, or returns the existing user if the email already matches one.

This mutation is the standalone way to provision a student without granting any course or subscription access. It is intended for CRM sync, bulk import, and similar flows that need user records before deciding what access to grant.

  • Idempotent by email within the school. Re-running with the same email returns the existing user with created: false.
  • Silent: no welcome / invitation / confirmation email is sent.
  • Multi-tenant safe: a matching email in another school never leaks across boundaries.
  • Decoupled from access: this only creates / finds the user and assigns the student role for the current school. It does not enroll them in any course or membership plan.

Requires one of:

  • students:write
  • members:write
createStudent(
email: String! # Required. Idempotency key within the school.
name: String # Optional. When omitted or blank, the email is used as the display name. Mostly ignored for existing users (see "Opportunistic name fill").
phoneNumber: String # Optional. Ignored if the user already exists.
): AdminCreateStudentPayload!
type AdminCreateStudentPayload {
student: User!
created: Boolean! # true: a new user was created. false: email matched an existing user.
}

The payload type name is auto-generated as <MutationName>Payload from the mutation’s graphql_name. The mutation declares graphql_name "AdminCreateStudent", so the payload is AdminCreateStudentPayload.

The mutation is keyed on email within the current school. Bulk importers may safely retry the same payload — duplicate calls return the existing user instead of creating a second one or raising an error.

# First call -> creates
mutation { createStudent(email: "alice@example.com", name: "Alice") { student { id } created } }
# => { student: { id: "1" }, created: true }
# Second call (same email) -> returns existing
mutation { createStudent(email: "alice@example.com", name: "Alice") { student { id } created } }
# => { student: { id: "1" }, created: false }

The mutation is also race-safe at the database level: if two concurrent calls both pass the existence check and one wins the unique-key constraint, the loser recovers via lookup and returns [existing, false].

Email addresses are not globally unique across schools. The mutation looks up existing users scoped to the current school only. A user with the same email in a different school will never be returned, and a fresh user will be created in the current school instead.

createStudent does not send any email — no welcome, no invitation, no Devise confirmation email. The new user is created with the confirmation flow bypassed.

This is intentional: source systems own their own outreach. If you need an invitation flow, send it separately after calling this mutation.

name is optional. When the caller omits name or sends a blank value, the new user is created with the email as the display name. Provide a real name whenever the source system has it — the email fallback is a safety net for sources that genuinely do not know the student’s name at provisioning time.

Note: a user created via the email fallback is not treated as a placeholder by “Opportunistic name fill” below. To later swap the email-as-name for a real name, use a dedicated user-update flow rather than relying on a follow-up createStudent or enrollStudentToCourse to overwrite it.

For an existing user, the supplied name is ignored unless the existing name is one of:

  • blank
  • the placeholder string "Student"

In those cases the existing user’s name is updated to the supplied name. A real name the user (or someone else) filled in is never overwritten. An email-as-name created via the fallback above is treated as a real name and is not overwritten.

phoneNumber on existing users is always ignored.

When phoneNumber is supplied and a new user is created, it is written via update!. If validation or normalization fails, the mutation raises STUDENT-001 with the validation message — it does not silently drop the value.

CodeMeaning
STUDENT-001Catch-all create failure. Includes the underlying validation message (e.g. invalid phone number format).
STUDENT-002Email is missing or fails format validation.
STUDENT-003Reserved. No longer raised — name is optional and falls back to the email. Kept for API stability.
STUDENT-004User row could not be saved (validation failure on User.save). The race-recovery path raises this only if recovery itself failed (e.g. extreme replication lag).

Errors are returned in the standard GraphQL errors array. The error code is in extensions.error.code.

mutation {
createStudent(
email: "alice@example.com"
name: "Alice Liddell"
phoneNumber: "+886912345678"
) {
student {
id
email
name
phoneNumber
}
created
}
}
{
"data": {
"createStudent": {
"student": {
"id": "abc-123",
"email": "alice@example.com",
"name": "Alice Liddell",
"phoneNumber": "+886912345678"
},
"created": true
}
}
}
mutation {
createStudent(email: "alice@example.com", name: "Alice Liddell") {
student { id }
created
}
}
{
"data": {
"createStudent": {
"student": { "id": "abc-123" },
"created": false
}
}
}

The following are intentionally not part of this mutation. Use the dedicated mutations / flows for these:

  • Course enrollment — use enrollStudentToCourse.
  • Subscription / membership plan — use createSubscription.
  • Roles other than student (teacher, admin) — not supported.
  • Profile fields beyond email / name / phoneNumber (tags, external_id, custom metadata) — not supported.
  • Batch / bulk creation — call the mutation once per student. The idempotency guarantee makes retry-driven bulk imports safe.

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