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.
At a glance
Section titled “At a glance”- 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
studentrole for the current school. It does not enroll them in any course or membership plan.
OAuth scope
Section titled “OAuth scope”Requires one of:
students:writemembers:write
Signature
Section titled “Signature”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>Payloadfrom the mutation’sgraphql_name. The mutation declaresgraphql_name "AdminCreateStudent", so the payload isAdminCreateStudentPayload.
Behavior
Section titled “Behavior”Idempotency
Section titled “Idempotency”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 -> createsmutation { createStudent(email: "alice@example.com", name: "Alice") { student { id } created } }# => { student: { id: "1" }, created: true }
# Second call (same email) -> returns existingmutation { 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].
Multi-tenant isolation
Section titled “Multi-tenant isolation”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.
No-email guarantee
Section titled “No-email guarantee”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 fallback for new users
Section titled “Name fallback for new users”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.
Opportunistic name fill
Section titled “Opportunistic name fill”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.
Phone number on new users
Section titled “Phone number on new users”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.
Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
STUDENT-001 | Catch-all create failure. Includes the underlying validation message (e.g. invalid phone number format). |
STUDENT-002 | Email is missing or fails format validation. |
STUDENT-003 | Reserved. No longer raised — name is optional and falls back to the email. Kept for API stability. |
STUDENT-004 | User 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.
Example: create a new student
Section titled “Example: create a new student”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 } }}Example: idempotent re-run
Section titled “Example: idempotent re-run”mutation { createStudent(email: "alice@example.com", name: "Alice Liddell") { student { id } created }}{ "data": { "createStudent": { "student": { "id": "abc-123" }, "created": false } }}Out of scope
Section titled “Out of scope”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.
Related Resources
Section titled “Related Resources”For more information about the Teachify Admin API, please refer to the API Overview.