---
title: "Provision Users"
url: https://docs.growamal.com/en/guides/provision-users/
language: en
---

# Provision Users

Every user after the bootstrap admin is created through `POST /api/users`, whose body is a
**discriminated union** keyed on `primaryPersona`. The discriminator (`CreateUserBody`) maps each
persona to its own body schema:

```
primaryPersona  →  body schema
STUDENT         →  CreateStudentUserBody
TEACHER         →  CreateTeacherUserBody
PARENT          →  CreateParentUserBody
PRINCIPAL       →  CreatePrincipalUserBody
MANAGER         →  CreateManagerUserBody
ADMIN           →  CreateAdminUserBody
```

Set `primaryPersona` and the server validates the rest of the body against that persona's schema.

## Per-persona fields

| Persona | Required (beyond common) | Optional |
|---|---|---|
| **common** | `email`, `password` (8–72 chars), `fullNameAr`, `primaryPersona` | `fullNameEn`, usually `organizationId` |
| `STUDENT` | `gradeLevel` (1–4) | `homeDialect` (`MSA` \| `LEV`, default `MSA`), `classId` |
| `TEACHER` | — | `tier` (`STANDARD` \| `SENIOR` \| `HEAD`), `arabicLiteracyTraining` |
| `PARENT` | — | `phoneE164`, `preferredLanguage` (`ar` \| `en`) |
| `PRINCIPAL` | `schoolId` | `tier` (`STANDARD` \| `HEAD`) |
| `MANAGER` | — | `scopedSchoolIds[]` (`[]` = org-wide) |
| `ADMIN` | `scope` (`SUPER` \| `ORG` \| `SCHOOL`) | `specialistRole` |

Example: create a Grade-2 student already enrolled in a class:

```bash
curl -X POST https://localhost:3000/api/users \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{
    "primaryPersona": "STUDENT",
    "email": "student@school.edu.jo",
    "password": "student-strong-password",
    "fullNameAr": "ليان حسن",
    "organizationId": "clx4z2k0u0000xyz1234abcde",
    "gradeLevel": 2,
    "classId": "clx4z2k0u0000xyz1234abcde"
  }'
```

> **Note:** **Enrolling a student into a class.** Set `classId` on the student body at creation. There is no
> separate enroll endpoint in `1.0.0-phase0`; the student then appears in
> `GET /api/classes/{classId}/students`. See [Manage Classes & Rosters](https://docs.growamal.com/en/guides/classes-and-rosters/).

## Who can call it

`POST /api/users` requires the `MANAGE_ORG` permission, which only admins hold. To create a principal
or manager when acting as a Manager, use the dedicated `POST /api/principals` (requires
`MANAGE_PRINCIPALS`) and `POST /api/managers` (requires `MANAGE_MANAGERS`) endpoints instead.
Managers cannot call `POST /api/users` directly.

## What you get back

A `201 UserCreatedResponse` (`id`, `email`, `primaryPersona`, `organizationId`, `createdAt`). The
matching persona profile row is created in the **same transaction**.

## Read profiles and lists

- `GET /api/users/me/profile` → the caller's persona-typed `PersonaProfileResponse` (discriminated).
- `GET /api/users` → users in your scope, paginated with a `nextCursor`.
- `GET /api/users/me/preferences` → `UserPreferencesResponse` (create-on-read with defaults).
  `PATCH /api/users/me/preferences` → partial upsert. Fields: `highContrast`, `captionsDefault`,
  `transcriptOpensInPanel`, `reducedMotionOverride`, `fontScalePreference` (`100` | `125` | `150` |
  `200`), `dyslexiaFontEnabled`. No permission gate beyond authentication.

## Errors

| Status | When |
|---|---|
| `422` | Validation: short password, bad persona/field combo (`VALIDATION_ERROR`) |
| `403` | Missing the required `MANAGE_*` permission (`PERMISSION_DENIED`) |
| `409` | Conflict: e.g. a quickCode collision (retried up to 10 times automatically, then surfaced as `QUICKCODE_COLLISION_EXHAUSTED`) |

See [Errors](https://docs.growamal.com/en/errors/) for the full catalog.
