{
 "openapi": "3.1.0",
 "info": {
  "title": "saMas School API",
  "version": "1.0.0-draft",
  "description": "The School API is for software acting on behalf of one school: a payment gateway or bank feed posting payments, an accounting or ERP system reading invoices, a WhatsApp provider sending reminders, the school's own website listing announcements, an HR system reading staff.\n\nAuthentication: a per-school API key in X-Api-Key. Each key carries scopes; a request outside its scopes is refused (403, code SCOPE_MISSING). The school is implied by the key, so there is no school id in these paths.\n\nEvery write is audited under the key's name in the school's change history, and can be replayed safely with Idempotency-Key. Webhooks push events to the school's systems; deliveries are signed and retried.",
  "contact": {
   "name": "saMas School",
   "url": "https://samas.school"
  },
  "x-audience": "public"
 },
 "servers": [
  {
   "url": "https://api.samas.school/v1",
   "description": "Production"
  },
  {
   "url": "https://api-beta.samas.school/v1",
   "description": "Beta (demo schools only)"
  }
 ],
 "security": [
  {
   "schoolKey": []
  }
 ],
 "tags": [
  {
   "name": "School",
   "description": "The school the key belongs to."
  },
  {
   "name": "Structure",
   "description": "Classes and sections."
  },
  {
   "name": "Students",
   "description": "Students and guardians."
  },
  {
   "name": "Attendance",
   "description": "Registers, read and write."
  },
  {
   "name": "Grades",
   "description": "Published marks."
  },
  {
   "name": "Timetable",
   "description": "Published timetables."
  },
  {
   "name": "People",
   "description": "Staff and guardian directory."
  },
  {
   "name": "Announcements",
   "description": "Notices and calendar."
  },
  {
   "name": "Fees",
   "description": "Structure, families, invoices, dues."
  },
  {
   "name": "Payments",
   "description": "Receipts."
  },
  {
   "name": "Payment proofs",
   "description": "Slips uploaded by parents."
  },
  {
   "name": "Webhooks",
   "description": "Push events to the school's systems."
  },
  {
   "name": "Branding",
   "description": "The school's look, readable without credentials."
  },
  {
   "name": "Learn",
   "description": "Shared study guides: the public page of a scholar's study guide, reached by its magic link, without credentials."
  },
  {
   "name": "Site",
   "description": "samas.school itself: the home page's data, the public roadmap, asking for a demo school, wishing for a feature. No credentials."
  }
 ],
 "paths": {
  "/school": {
   "get": {
    "tags": [
     "School"
    ],
    "summary": "This school",
    "description": "Name, code and public settings for the key's school.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...).\n\n**Response 200:** `{ data, meta }` where `data` is one School.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `code` | string | Always present. |\n| `name` | string | Always present. |\n| `is_active` | boolean | May be omitted. |\n| `settings` | object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/School"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "code": "DEMO",
          "name": "saMas School Demo",
          "is_active": true,
          "settings": {
           "directory_visibility": "staff_only",
           "timezone": "Asia/Karachi",
           "locale": "en-PK",
           "fee_reply_to": "parent@example.pk",
           "admission_no_prefix": "string",
           "self_attendance": "on"
          }
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    }
   }
  },
  "/school/academic-years": {
   "get": {
    "tags": [
     "School"
    ],
    "summary": "Academic years",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/AcademicYear"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "2026-27",
           "starts_on": "2026-09-01",
           "ends_on": "2026-09-01",
           "is_current": true
          }
         ],
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "description": "Academic years.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...).\n\n**Response 200:** `{ data, meta }` where `data` is a list of AcademicYear.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `name` | string | Always present. |\n| `starts_on` | string | Always present; date YYYY-MM-DD. |\n| `ends_on` | string | Always present; date YYYY-MM-DD. |\n| `is_current` | boolean | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/classes": {
   "get": {
    "tags": [
     "Structure"
    ],
    "summary": "Classes with sections",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/ClassWithSections"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "Grade 5",
           "sort_order": 1,
           "min_age": 1,
           "max_age": 1,
           "sections": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "class_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "class_name": "Grade 5",
             "name": "A",
             "capacity": 1,
             "room": "Room 105",
             "class_teacher": {
              "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "full_name": "string"
             },
             "enrolled": 1,
             "kind": "normal",
             "label": "string",
             "shift_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
            }
           ]
          }
         ],
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:students"
    ],
    "description": "Classes with sections.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:students`.\n\n**Response 200:** `{ data, meta }` where `data` is a list of ClassWithSections.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `name` | string | Always present. |\n| `sort_order` | integer | May be omitted. |\n| `min_age` | integer | May be omitted; may be null. |\n| `max_age` | integer | May be omitted; may be null. |\n| `sections` | array of Section | Always present. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/sections/{section_id}/students": {
   "get": {
    "tags": [
     "Structure"
    ],
    "summary": "Students in a section",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/section_id"
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Student"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "admission_no": "DS-2026-001",
           "full_name": "Amna Khan",
           "date_of_birth": "2026-09-01",
           "gender": "female",
           "status": "active",
           "section": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "class_name": "string",
            "room": "string"
           },
           "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "phone": "string",
           "email": "string",
           "roll_no": 1,
           "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "guardians": [
            {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "phone": "string",
             "email": "string",
             "relation": "mother",
             "is_primary": true
            }
           ]
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:students"
    ],
    "description": "Students in a section.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:students`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `section_id` | path | string | Required; UUID; at most 40 characters. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of Student.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `admission_no` | string | Always present. |\n| `full_name` | string | Always present. |\n| `date_of_birth` | string | Always present; date YYYY-MM-DD. |\n| `gender` | string | May be omitted; may be null; one of `female`, `male`, `other`, `None`. |\n| `status` | string | Always present; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `section` | object | May be omitted. |\n| `section_id` | string | May be omitted; may be null; UUID; Same as section.id; handy for filtering. |\n| `phone` | string | May be omitted; may be null. |\n| `email` | string | May be omitted; may be null. |\n| `roll_no` | integer | May be omitted; may be null. |\n| `family_account_id` | string | May be omitted; may be null; UUID. |\n| `user_id` | string | May be omitted; may be null; UUID; The student's own sign-in, when they have one. On the list since 2026-10-15 (it was only on StudentDetail). |\n| `guardians` | array of Guardian | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/students": {
   "get": {
    "tags": [
     "Students"
    ],
    "summary": "List students",
    "description": "Filter by class, section, status, or search by name or admission number.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:students`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `class_id` | query | string | Optional; UUID; at most 40 characters. |\n| `section_id` | query | string | Optional; UUID; at most 40 characters. |\n| `status` | query | string | Optional; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `q` | query | string | Optional; at most 200 characters; Name or admission number. |\n| `updated_since` | query | string | Optional; timestamp with offset or Z (RFC 3339); at most 40 characters; Only students changed after this time, for incremental sync. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of Student.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `admission_no` | string | Always present. |\n| `full_name` | string | Always present. |\n| `date_of_birth` | string | Always present; date YYYY-MM-DD. |\n| `gender` | string | May be omitted; may be null; one of `female`, `male`, `other`, `None`. |\n| `status` | string | Always present; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `section` | object | May be omitted. |\n| `section_id` | string | May be omitted; may be null; UUID; Same as section.id; handy for filtering. |\n| `phone` | string | May be omitted; may be null. |\n| `email` | string | May be omitted; may be null. |\n| `roll_no` | integer | May be omitted; may be null. |\n| `family_account_id` | string | May be omitted; may be null; UUID. |\n| `user_id` | string | May be omitted; may be null; UUID; The student's own sign-in, when they have one. On the list since 2026-10-15 (it was only on StudentDetail). |\n| `guardians` | array of Guardian | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "class_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "section_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "status",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "active",
        "inactive",
        "graduated",
        "withdrawn"
       ]
      }
     },
     {
      "name": "q",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "Name or admission number",
       "maxLength": 200
      }
     },
     {
      "name": "updated_since",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "Only students changed after this time, for incremental sync.",
       "format": "date-time",
       "maxLength": 40
      }
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Student"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "admission_no": "DS-2026-001",
           "full_name": "Amna Khan",
           "date_of_birth": "2026-09-01",
           "gender": "female",
           "status": "active",
           "section": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "class_name": "string",
            "room": "string"
           },
           "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "phone": "string",
           "email": "string",
           "roll_no": 1,
           "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "guardians": [
            {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "phone": "string",
             "email": "string",
             "relation": "mother",
             "is_primary": true
            }
           ]
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:students"
    ]
   },
   "post": {
    "tags": [
     "Students"
    ],
    "summary": "Enrol a student",
    "description": "Same rules as the office screen: admission number unique, age within the class range, section capacity enforced (SECTION_FULL).\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:students`.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `admission_no` | string | Required; pattern `^[A-Za-z0-9-]{2,20}$`; at most 80 characters. |\n| `full_name` | string | Required; pattern `^[A-Za-z'\\- ]{2,80}$`; at most 80 characters. |\n| `date_of_birth` | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `gender` | string | Optional; may be null; one of `female`, `male`, `other`, `None`. |\n| `section_id` | string | Optional; may be null; UUID; at most 40 characters. |\n| `roll_no` | integer | Optional; may be null; from 1 to 999. |\n| `status` | string | Optional; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `phone` | string | Optional; may be null; at most 200 characters. |\n| `email` | string | Optional; may be null; email address; at most 254 characters. |\n| `address` | string | Optional; may be null; at most 200 characters. |\n| `previous_school` | string | Optional; may be null; at most 120 characters. |\n| `notes` | string | Optional; may be null; at most 500 characters. |\n| `user_id` | string | Optional; may be null; UUID; at most 40 characters; Link the student's own sign-in (a person with the student role). |\n\n**Response 201:** `{ data, meta }` where `data` is one Student.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `admission_no` | string | Always present. |\n| `full_name` | string | Always present. |\n| `date_of_birth` | string | Always present; date YYYY-MM-DD. |\n| `gender` | string | May be omitted; may be null; one of `female`, `male`, `other`, `None`. |\n| `status` | string | Always present; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `section` | object | May be omitted. |\n| `section_id` | string | May be omitted; may be null; UUID; Same as section.id; handy for filtering. |\n| `phone` | string | May be omitted; may be null. |\n| `email` | string | May be omitted; may be null. |\n| `roll_no` | integer | May be omitted; may be null. |\n| `family_account_id` | string | May be omitted; may be null; UUID. |\n| `user_id` | string | May be omitted; may be null; UUID; The student's own sign-in, when they have one. On the list since 2026-10-15 (it was only on StudentDetail). |\n| `guardians` | array of Guardian | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/StudentWrite"
       },
       "example": {
        "admission_no": "string",
        "full_name": "string",
        "date_of_birth": "2026-09-01",
        "gender": "female",
        "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "roll_no": 1,
        "status": "active",
        "phone": "string",
        "email": "parent@example.pk",
        "address": "string",
        "previous_school": "string",
        "notes": "string",
        "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Student"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "admission_no": "DS-2026-001",
          "full_name": "Amna Khan",
          "date_of_birth": "2026-09-01",
          "gender": "female",
          "status": "active",
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "class_name": "string",
           "room": "string"
          },
          "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "phone": "string",
          "email": "string",
          "roll_no": 1,
          "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guardians": [
           {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "phone": "string",
            "email": "string",
            "relation": "mother",
            "is_primary": true
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:students"
    ]
   }
  },
  "/school/students/{student_id}": {
   "get": {
    "tags": [
     "Students"
    ],
    "summary": "One student",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/student_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Student"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "admission_no": "DS-2026-001",
          "full_name": "Amna Khan",
          "date_of_birth": "2026-09-01",
          "gender": "female",
          "status": "active",
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "class_name": "string",
           "room": "string"
          },
          "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "phone": "string",
          "email": "string",
          "roll_no": 1,
          "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guardians": [
           {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "phone": "string",
            "email": "string",
            "relation": "mother",
            "is_primary": true
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:students"
    ],
    "description": "One student.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:students`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one Student.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `admission_no` | string | Always present. |\n| `full_name` | string | Always present. |\n| `date_of_birth` | string | Always present; date YYYY-MM-DD. |\n| `gender` | string | May be omitted; may be null; one of `female`, `male`, `other`, `None`. |\n| `status` | string | Always present; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `section` | object | May be omitted. |\n| `section_id` | string | May be omitted; may be null; UUID; Same as section.id; handy for filtering. |\n| `phone` | string | May be omitted; may be null. |\n| `email` | string | May be omitted; may be null. |\n| `roll_no` | integer | May be omitted; may be null. |\n| `family_account_id` | string | May be omitted; may be null; UUID. |\n| `user_id` | string | May be omitted; may be null; UUID; The student's own sign-in, when they have one. On the list since 2026-10-15 (it was only on StudentDetail). |\n| `guardians` | array of Guardian | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   },
   "patch": {
    "tags": [
     "Students"
    ],
    "summary": "Update a student",
    "description": "Partial update; the admission number cannot change.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:students`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `admission_no` | string | Required; pattern `^[A-Za-z0-9-]{2,20}$`; at most 80 characters. |\n| `full_name` | string | Required; pattern `^[A-Za-z'\\- ]{2,80}$`; at most 80 characters. |\n| `date_of_birth` | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `gender` | string | Optional; may be null; one of `female`, `male`, `other`, `None`. |\n| `section_id` | string | Optional; may be null; UUID; at most 40 characters. |\n| `roll_no` | integer | Optional; may be null; from 1 to 999. |\n| `status` | string | Optional; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `phone` | string | Optional; may be null; at most 200 characters. |\n| `email` | string | Optional; may be null; email address; at most 254 characters. |\n| `address` | string | Optional; may be null; at most 200 characters. |\n| `previous_school` | string | Optional; may be null; at most 120 characters. |\n| `notes` | string | Optional; may be null; at most 500 characters. |\n| `user_id` | string | Optional; may be null; UUID; at most 40 characters; Link the student's own sign-in (a person with the student role). |\n\n**Response 200:** `{ data, meta }` where `data` is one Student.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `admission_no` | string | Always present. |\n| `full_name` | string | Always present. |\n| `date_of_birth` | string | Always present; date YYYY-MM-DD. |\n| `gender` | string | May be omitted; may be null; one of `female`, `male`, `other`, `None`. |\n| `status` | string | Always present; one of `active`, `inactive`, `graduated`, `withdrawn`. |\n| `section` | object | May be omitted. |\n| `section_id` | string | May be omitted; may be null; UUID; Same as section.id; handy for filtering. |\n| `phone` | string | May be omitted; may be null. |\n| `email` | string | May be omitted; may be null. |\n| `roll_no` | integer | May be omitted; may be null. |\n| `family_account_id` | string | May be omitted; may be null; UUID. |\n| `user_id` | string | May be omitted; may be null; UUID; The student's own sign-in, when they have one. On the list since 2026-10-15 (it was only on StudentDetail). |\n| `guardians` | array of Guardian | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/student_id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/StudentWrite"
       },
       "example": {
        "admission_no": "string",
        "full_name": "string",
        "date_of_birth": "2026-09-01",
        "gender": "female",
        "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "roll_no": 1,
        "status": "active",
        "phone": "string",
        "email": "parent@example.pk",
        "address": "string",
        "previous_school": "string",
        "notes": "string",
        "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Student"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "admission_no": "DS-2026-001",
          "full_name": "Amna Khan",
          "date_of_birth": "2026-09-01",
          "gender": "female",
          "status": "active",
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "class_name": "string",
           "room": "string"
          },
          "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "phone": "string",
          "email": "string",
          "roll_no": 1,
          "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guardians": [
           {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "phone": "string",
            "email": "string",
            "relation": "mother",
            "is_primary": true
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:students"
    ]
   }
  },
  "/school/students/{student_id}/guardians": {
   "post": {
    "tags": [
     "Students"
    ],
    "summary": "Link a guardian",
    "description": "The guardian must already be a person with the parent role at this school; see People.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:students`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `user_id` | string | Required; UUID; at most 40 characters. |\n| `relation` | string | Required; one of `mother`, `father`, `guardian`, `other`. |\n| `is_primary` | boolean | Optional. |\n\n**Response 201:** `{ data, meta }` where `data` is one Guardian.\n\n| Field | Type | Notes |\n|---|---|---|\n| `user_id` | string | Always present; may be null; UUID. |\n| `full_name` | string | Always present; may be null. |\n| `phone` | string | May be omitted; may be null. |\n| `email` | string | May be omitted; may be null. |\n| `relation` | string | Always present; one of `mother`, `father`, `guardian`, `other`. |\n| `is_primary` | boolean | Always present. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/student_id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid",
          "maxLength": 40
         },
         "relation": {
          "type": "string",
          "enum": [
           "mother",
           "father",
           "guardian",
           "other"
          ]
         },
         "is_primary": {
          "type": "boolean"
         }
        },
        "required": [
         "user_id",
         "relation"
        ],
        "additionalProperties": false
       },
       "example": {
        "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "relation": "mother",
        "is_primary": true
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Guardian"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "full_name": "string",
          "phone": "string",
          "email": "string",
          "relation": "mother",
          "is_primary": true
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:students"
    ]
   }
  },
  "/school/students/{student_id}/attendance": {
   "get": {
    "tags": [
     "Attendance"
    ],
    "summary": "Attendance for a student",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/student_id"
     },
     {
      "name": "from",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "name": "to",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/AttendanceRecord"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "on_date": "2026-09-01",
           "period_no": 1,
           "status": "present",
           "note": "string",
           "marked_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "marked_at": "2026-09-22T09:30:00Z"
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:attendance"
    ],
    "description": "Attendance for a student.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:attendance`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `from` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n| `to` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of AttendanceRecord.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student_id` | string | Always present; UUID. |\n| `section_id` | string | Always present; UUID. |\n| `on_date` | string | Always present; date YYYY-MM-DD. |\n| `period_no` | integer | May be omitted; 0 means the whole day. |\n| `status` | AttendanceStatus | Always present; one of `present`, `absent`, `late`, `excused`. |\n| `note` | string | May be omitted; may be null. |\n| `marked_by` | PersonRef | May be omitted. |\n| `marked_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/sections/{section_id}/attendance": {
   "get": {
    "tags": [
     "Attendance"
    ],
    "summary": "Attendance register for a section and date",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/section_id"
     },
     {
      "name": "on_date",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "name": "period_no",
      "in": "query",
      "schema": {
       "type": "integer",
       "default": 0
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/AttendanceRecord"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "on_date": "2026-09-01",
           "period_no": 1,
           "status": "present",
           "note": "string",
           "marked_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "marked_at": "2026-09-22T09:30:00Z"
          }
         ],
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:attendance"
    ],
    "description": "Attendance register for a section and date.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:attendance`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `section_id` | path | string | Required; UUID; at most 40 characters. |\n| `on_date` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n| `period_no` | query | integer | Optional. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of AttendanceRecord.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student_id` | string | Always present; UUID. |\n| `section_id` | string | Always present; UUID. |\n| `on_date` | string | Always present; date YYYY-MM-DD. |\n| `period_no` | integer | May be omitted; 0 means the whole day. |\n| `status` | AttendanceStatus | Always present; one of `present`, `absent`, `late`, `excused`. |\n| `note` | string | May be omitted; may be null. |\n| `marked_by` | PersonRef | May be omitted. |\n| `marked_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   },
   "put": {
    "tags": [
     "Attendance"
    ],
    "summary": "Save a register",
    "description": "Whole register for the section, date and period (an RFID gate or a biometric device posts here). Every student needs a status; excused needs a note; future dates are refused.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:attendance`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `section_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `on_date` | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `period_no` | integer | Optional. |\n| `rows` | array of AttendanceRow | Required; at most 500 items. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `saved` | integer | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/section_id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "on_date": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01",
          "maxLength": 40
         },
         "period_no": {
          "type": "integer",
          "default": 0
         },
         "rows": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/AttendanceRow"
          },
          "maxItems": 500
         }
        },
        "required": [
         "on_date",
         "rows"
        ],
        "additionalProperties": false
       },
       "example": {
        "on_date": "2026-09-01",
        "period_no": 0,
        "rows": [
         {
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "status": "present",
          "note": "string"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "saved": {
             "type": "integer"
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "saved": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:attendance"
    ]
   }
  },
  "/school/students/{student_id}/grades": {
   "get": {
    "tags": [
     "Grades"
    ],
    "summary": "Published marks for a student",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/student_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "assessment": {
              "$ref": "#/components/schemas/Assessment"
             },
             "mark": {
              "$ref": "#/components/schemas/Mark"
             }
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "assessment": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "subject": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "Mathematics",
             "code": "MATH"
            },
            "title": "Unit test 1",
            "kind": "quiz",
            "max_marks": 1,
            "weight": 1.5,
            "held_on": "2026-09-22",
            "published_at": "2026-09-22T09:30:00Z",
            "exam_paper_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
           },
           "mark": {
            "assessment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "marks": 1,
            "absent": true,
            "exempt": true,
            "feedback": "string"
           }
          }
         ],
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:grades"
    ],
    "description": "Published marks for a student.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:grades`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `assessment` | Assessment | May be omitted. |\n| `mark` | Mark | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/sections/{section_id}/timetable": {
   "get": {
    "tags": [
     "Timetable"
    ],
    "summary": "Published timetable for a section",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/section_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "periods": {
             "type": "array",
             "items": {
              "$ref": "#/components/schemas/Period"
             }
            },
            "slots": {
             "type": "array",
             "items": {
              "$ref": "#/components/schemas/TimetableSlot"
             }
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "periods": [
           {
            "period_no": 1,
            "label": "P1",
            "starts_at": "08:00",
            "ends_at": "08:40",
            "is_break": true,
            "block_type": "period",
            "weekday": 1,
            "shift_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "version_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
           }
          ],
          "slots": [
           {
            "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "weekday": 1,
            "period_no": 1,
            "subject": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "Mathematics",
             "code": "MATH"
            },
            "teacher": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "room": "string",
            "span": 1,
            "combined_key": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "room_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "live_url": "https://erp.example.pk/samas/hook"
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:timetable"
    ],
    "description": "Published timetable for a section.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:timetable`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `section_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `periods` | array of Period | May be omitted. |\n| `slots` | array of TimetableSlot | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/people": {
   "get": {
    "tags": [
     "People"
    ],
    "summary": "Staff and guardians",
    "description": "Directory of people at the school by role. Identity numbers are never included.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:people`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `role` | query | Role | Optional. |\n| `q` | query | string | Optional; at most 200 characters. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of Person.\n\n| Field | Type | Notes |\n|---|---|---|\n| `user_id` | string | Always present; UUID; Internal id, stable across sign-in providers. |\n| `full_name` | string | Always present; may be null. |\n| `email` | string | May be omitted; may be null; email address. |\n| `phone` | string | May be omitted; may be null. |\n| `role` | any | Always present; Default role. super_admin for platform staff; null for an account with no school yet. |\n| `is_active` | boolean | May be omitted. |\n| `signed_in` | boolean | May be omitted; False while the person is invited and has not signed in yet. Present on GET /admin/people. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "role",
      "in": "query",
      "schema": {
       "$ref": "#/components/schemas/Role"
      }
     },
     {
      "name": "q",
      "in": "query",
      "schema": {
       "type": "string",
       "maxLength": 200
      }
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Person"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "Ahmed Khan",
           "email": "parent@example.pk",
           "phone": "+923001234567",
           "role": null,
           "is_active": true,
           "signed_in": true
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:people"
    ]
   }
  },
  "/school/announcements": {
   "get": {
    "tags": [
     "Announcements"
    ],
    "summary": "Announcements",
    "description": "For the school website or a display board. Only announcements addressed to everyone unless the key has read:people.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of Announcement.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `created_by` | string | May be omitted; may be null; UUID; user_id of the author. |\n| `author` | PersonRef | May be omitted. |\n| `title` | string | Always present. |\n| `body` | string | Always present. |\n| `audience_roles` | array of Role | May be omitted. |\n| `section_ids` | array of string | May be omitted. |\n| `publish_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `expires_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `pinned` | boolean | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Announcement"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "created_by": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "author": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "title": "string",
           "body": "string",
           "audience_roles": [
            "school_admin"
           ],
           "section_ids": [
            "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
           ],
           "publish_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "pinned": true
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    }
   },
   "post": {
    "tags": [
     "Announcements"
    ],
    "summary": "Post an announcement",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/AnnouncementWrite"
       },
       "example": {
        "title": "string",
        "body": "string",
        "audience_roles": [
         "school_admin"
        ],
        "section_ids": [
         "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
        ],
        "publish_at": "2026-09-22T09:30:00Z",
        "expires_at": "2026-09-22T09:30:00Z",
        "pinned": true
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Announcement"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "created_by": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "author": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "title": "string",
          "body": "string",
          "audience_roles": [
           "school_admin"
          ],
          "section_ids": [
           "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
          ],
          "publish_at": "2026-09-22T09:30:00Z",
          "expires_at": "2026-09-22T09:30:00Z",
          "pinned": true
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:announcements"
    ],
    "description": "Post an announcement.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:announcements`.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `title` | string | Required; 3 to 120 characters. |\n| `body` | string | Required; 10 to 4000 characters. |\n| `audience_roles` | array of Role | Optional; at most 500 items. |\n| `section_ids` | array of string | Optional; at most 500 items. |\n| `publish_at` | string | Optional; timestamp with offset or Z (RFC 3339); at most 40 characters. |\n| `expires_at` | string | Optional; timestamp with offset or Z (RFC 3339); at most 40 characters. |\n| `pinned` | boolean | Optional. |\n\n**Response 201:** `{ data, meta }` where `data` is one Announcement.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `created_by` | string | May be omitted; may be null; UUID; user_id of the author. |\n| `author` | PersonRef | May be omitted. |\n| `title` | string | Always present. |\n| `body` | string | Always present. |\n| `audience_roles` | array of Role | May be omitted. |\n| `section_ids` | array of string | May be omitted. |\n| `publish_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `expires_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `pinned` | boolean | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/calendar": {
   "get": {
    "tags": [
     "Announcements"
    ],
    "summary": "Calendar events",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "from",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "name": "to",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/CalendarEvent"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "kind": "holiday",
           "starts_at": "2026-09-22T09:30:00Z",
           "ends_at": "2026-09-22T09:30:00Z",
           "all_day": true,
           "description": "string"
          }
         ],
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "description": "Calendar events.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `from` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n| `to` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of CalendarEvent.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `title` | string | Always present. |\n| `kind` | string | Always present; one of `holiday`, `exam`, `ptm`, `event`, `deadline`. |\n| `starts_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `ends_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `all_day` | boolean | May be omitted. |\n| `description` | string | May be omitted; may be null. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/fees/structure": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "Fee structure",
    "description": "Heads, lines per class and the school's fee settings for an academic year.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `academic_year_id` | query | string | Optional; UUID; at most 40 characters; Defaults to the current year. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `heads` | array of FeeHead | May be omitted. |\n| `lines` | array of FeeStructureLine | May be omitted. |\n| `settings` | FeeSettings | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "academic_year_id",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "Defaults to the current year.",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "heads": {
             "type": "array",
             "items": {
              "$ref": "#/components/schemas/FeeHead"
             }
            },
            "lines": {
             "type": "array",
             "items": {
              "$ref": "#/components/schemas/FeeStructureLine"
             }
            },
            "settings": {
             "$ref": "#/components/schemas/FeeSettings"
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "heads": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "Tuition",
            "code": "TUITION",
            "kind": "recurring",
            "is_active": true,
            "sort_order": 1
           }
          ],
          "lines": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "academic_year_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "class_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "fee_head_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "amount": "12500.00",
            "frequency": "monthly"
           }
          ],
          "settings": {
           "currency": "PKR",
           "due_day": 1,
           "late_fee_amount": "12500.00",
           "late_fee_after_days": 1,
           "second_approval_percent": 1.5,
           "refund_approval_amount": "12500.00"
          }
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ]
   }
  },
  "/school/families": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "Family accounts",
    "description": "The payer records. Search by name, phone or a child's admission number.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `q` | query | string | Optional; at most 200 characters. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of FamilyAccount.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `name` | string | Always present. |\n| `primary_guardian` | PersonRef | May be omitted. |\n| `phone` | string | May be omitted; may be null. |\n| `students` | array of object | May be omitted. |\n| `outstanding` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `is_locked` | boolean | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "q",
      "in": "query",
      "schema": {
       "type": "string",
       "maxLength": 200
      }
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/FamilyAccount"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "Ahmed Khan",
           "primary_guardian": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "phone": "string",
           "students": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "admission_no": "string",
             "status": "active",
             "class_name": "string",
             "section_name": "string"
            }
           ],
           "outstanding": "12500.00",
           "is_locked": true
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ]
   }
  },
  "/school/families/{id}": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "One family account",
    "description": "With children, open invoices and outstanding balance.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `name` | string | Always present. |\n| `primary_guardian` | PersonRef | May be omitted. |\n| `phone` | string | May be omitted; may be null. |\n| `students` | array of object | May be omitted. |\n| `outstanding` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `is_locked` | boolean | May be omitted. |\n| `open_invoices` | array of Invoice | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/FamilyAccount"
            },
            {
             "type": "object",
             "properties": {
              "open_invoices": {
               "type": "array",
               "items": {
                "$ref": "#/components/schemas/Invoice"
               }
              }
             }
            }
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "name": "Ahmed Khan",
          "primary_guardian": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "phone": "string",
          "students": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "admission_no": "string",
            "status": "active",
            "class_name": "string",
            "section_name": "string"
           }
          ],
          "outstanding": "12500.00",
          "is_locked": true,
          "open_invoices": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "invoice_no": "INV-2026-000123",
            "student": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "admission_no": "string",
             "class_name": "string",
             "section_name": "string"
            },
            "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "family_name": "string",
            "family_phone": "string",
            "period_start": "2026-09-01",
            "period_end": "2026-09-01",
            "issue_date": "2026-09-01",
            "due_date": "2026-09-01",
            "status": "issued",
            "is_overdue": true,
            "currency": "PKR",
            "subtotal": "12500.00",
            "discount_total": "12500.00",
            "late_fee": "12500.00",
            "total": "12500.00",
            "paid_amount": "12500.00",
            "balance": "12500.00",
            "items": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "fee_head_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "description": "Tuition",
              "amount": "12500.00",
              "is_late_fee": true,
              "adjustment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
             }
            ],
            "notes": "string",
            "void_reason": "string",
            "voided_at": "2026-09-22T09:30:00Z",
            "is_locked": true,
            "on_hold": true,
            "payments": [
             {
              "payment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "receipt_no": "string",
              "paid_on": "2026-09-01",
              "method": "cash",
              "amount": "12500.00",
              "status": "confirmed"
             }
            ]
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ]
   }
  },
  "/school/invoices": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "Invoices",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "status",
      "in": "query",
      "schema": {
       "$ref": "#/components/schemas/InvoiceStatus"
      }
     },
     {
      "name": "overdue",
      "in": "query",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "name": "period",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "First day of the month",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "name": "class_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "family_account_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "student_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "updated_since",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date-time",
       "maxLength": 40
      }
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Invoice"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "invoice_no": "INV-2026-000123",
           "student": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "admission_no": "string",
            "class_name": "string",
            "section_name": "string"
           },
           "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "family_name": "string",
           "family_phone": "string",
           "period_start": "2026-09-01",
           "period_end": "2026-09-01",
           "issue_date": "2026-09-01",
           "due_date": "2026-09-01",
           "status": "issued",
           "is_overdue": true,
           "currency": "PKR",
           "subtotal": "12500.00",
           "discount_total": "12500.00",
           "late_fee": "12500.00",
           "total": "12500.00",
           "paid_amount": "12500.00",
           "balance": "12500.00",
           "items": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "fee_head_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "description": "Tuition",
             "amount": "12500.00",
             "is_late_fee": true,
             "adjustment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
            }
           ],
           "notes": "string",
           "void_reason": "string",
           "voided_at": "2026-09-22T09:30:00Z",
           "is_locked": true,
           "on_hold": true,
           "payments": [
            {
             "payment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "receipt_no": "string",
             "paid_on": "2026-09-01",
             "method": "cash",
             "amount": "12500.00",
             "status": "confirmed"
            }
           ]
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ],
    "description": "Invoices.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `status` | query | InvoiceStatus | Optional. |\n| `overdue` | query | boolean | Optional. |\n| `period` | query | string | Optional; date YYYY-MM-DD; at most 40 characters; First day of the month. |\n| `class_id` | query | string | Optional; UUID; at most 40 characters. |\n| `family_account_id` | query | string | Optional; UUID; at most 40 characters. |\n| `student_id` | query | string | Optional; UUID; at most 40 characters. |\n| `updated_since` | query | string | Optional; timestamp with offset or Z (RFC 3339); at most 40 characters. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of Invoice.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `invoice_no` | string | Always present. |\n| `student` | object | Always present. |\n| `family_account_id` | string | Always present; UUID. |\n| `family_name` | string | May be omitted; may be null. |\n| `family_phone` | string | May be omitted; may be null. |\n| `period_start` | string | Always present; date YYYY-MM-DD. |\n| `period_end` | string | May be omitted; date YYYY-MM-DD. |\n| `issue_date` | string | May be omitted; date YYYY-MM-DD. |\n| `due_date` | string | Always present; date YYYY-MM-DD. |\n| `status` | InvoiceStatus | Always present; one of `issued`, `part_paid`, `paid`, `void`; Overdue is not a status; it is derived: issued or part_paid and past due_date. |\n| `is_overdue` | boolean | May be omitted. |\n| `currency` | string | May be omitted. |\n| `subtotal` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `discount_total` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `late_fee` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `total` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `paid_amount` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `balance` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `items` | array of InvoiceItem | May be omitted. |\n| `notes` | string | May be omitted; may be null. |\n| `void_reason` | string | May be omitted; may be null. |\n| `voided_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `is_locked` | boolean | May be omitted. |\n| `on_hold` | boolean | May be omitted. |\n| `payments` | array of object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)"
   },
   "post": {
    "tags": [
     "Fees"
    ],
    "summary": "Generate invoices for a month",
    "description": "One invoice per active student for the month (skipping students already invoiced), from the fee structure and approved discounts. Returns how many were created. Idempotent by nature.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:invoices`.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `period_start` | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `class_id` | string | Optional; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `created` | integer | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "period_start": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01",
          "maxLength": 40
         },
         "class_id": {
          "type": "string",
          "format": "uuid",
          "maxLength": 40
         }
        },
        "required": [
         "period_start"
        ],
        "additionalProperties": false
       },
       "example": {
        "period_start": "2026-09-01",
        "class_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "created": {
             "type": "integer"
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "created": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:invoices"
    ]
   }
  },
  "/school/invoices/{id}": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "One invoice",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Invoice"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "invoice_no": "INV-2026-000123",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "admission_no": "string",
           "class_name": "string",
           "section_name": "string"
          },
          "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "family_name": "string",
          "family_phone": "string",
          "period_start": "2026-09-01",
          "period_end": "2026-09-01",
          "issue_date": "2026-09-01",
          "due_date": "2026-09-01",
          "status": "issued",
          "is_overdue": true,
          "currency": "PKR",
          "subtotal": "12500.00",
          "discount_total": "12500.00",
          "late_fee": "12500.00",
          "total": "12500.00",
          "paid_amount": "12500.00",
          "balance": "12500.00",
          "items": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "fee_head_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "description": "Tuition",
            "amount": "12500.00",
            "is_late_fee": true,
            "adjustment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
           }
          ],
          "notes": "string",
          "void_reason": "string",
          "voided_at": "2026-09-22T09:30:00Z",
          "is_locked": true,
          "on_hold": true,
          "payments": [
           {
            "payment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "receipt_no": "string",
            "paid_on": "2026-09-01",
            "method": "cash",
            "amount": "12500.00",
            "status": "confirmed"
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ],
    "description": "One invoice.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one Invoice.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `invoice_no` | string | Always present. |\n| `student` | object | Always present. |\n| `family_account_id` | string | Always present; UUID. |\n| `family_name` | string | May be omitted; may be null. |\n| `family_phone` | string | May be omitted; may be null. |\n| `period_start` | string | Always present; date YYYY-MM-DD. |\n| `period_end` | string | May be omitted; date YYYY-MM-DD. |\n| `issue_date` | string | May be omitted; date YYYY-MM-DD. |\n| `due_date` | string | Always present; date YYYY-MM-DD. |\n| `status` | InvoiceStatus | Always present; one of `issued`, `part_paid`, `paid`, `void`; Overdue is not a status; it is derived: issued or part_paid and past due_date. |\n| `is_overdue` | boolean | May be omitted. |\n| `currency` | string | May be omitted. |\n| `subtotal` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `discount_total` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `late_fee` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `total` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `paid_amount` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `balance` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `items` | array of InvoiceItem | May be omitted. |\n| `notes` | string | May be omitted; may be null. |\n| `void_reason` | string | May be omitted; may be null. |\n| `voided_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `is_locked` | boolean | May be omitted. |\n| `on_hold` | boolean | May be omitted. |\n| `payments` | array of object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/invoices/{id}/pdf": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "Invoice as PDF",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "url": {
             "type": "string",
             "format": "uri"
            },
            "expires_at": {
             "type": "string",
             "format": "date-time"
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "url": "https://erp.example.pk/samas/hook",
          "expires_at": "2026-09-22T09:30:00Z"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ],
    "description": "Invoice as PDF.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `url` | string | May be omitted; https URL. |\n| `expires_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/payments": {
   "get": {
    "tags": [
     "Payments"
    ],
    "summary": "Payments (receipts)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "from",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "name": "to",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "name": "method",
      "in": "query",
      "schema": {
       "$ref": "#/components/schemas/PaymentMethod"
      }
     },
     {
      "name": "family_account_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "status",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "confirmed",
        "reversed"
       ]
      }
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Payment"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "receipt_no": "RCP-2026-000045",
           "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "family_name": "string",
           "family_phone": "string",
           "amount": "12500.00",
           "currency": "string",
           "method": "cash",
           "paid_on": "2026-09-01",
           "reference": "string",
           "notes": "string",
           "status": "confirmed",
           "reversed_reason": "string",
           "reversed_at": "2026-09-22T09:30:00Z",
           "received_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "proof_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "allocations": [
            {
             "invoice_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "invoice_no": "string",
             "student_name": "string",
             "period_start": "2026-09-22",
             "amount": "12500.00"
            }
           ],
           "unallocated": "12500.00",
           "refunded": "12500.00",
           "refunds": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "refund_no": "string",
             "amount": "12500.00",
             "method": "cash",
             "status": "pending",
             "refunded_on": "2026-09-22"
            }
           ]
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ],
    "description": "Payments (receipts).\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `from` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n| `to` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n| `method` | query | PaymentMethod | Optional. |\n| `family_account_id` | query | string | Optional; UUID; at most 40 characters. |\n| `status` | query | string | Optional; one of `confirmed`, `reversed`. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of Payment.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `receipt_no` | string | Always present. |\n| `family_account_id` | string | Always present; UUID. |\n| `family_name` | string | May be omitted. |\n| `family_phone` | string | May be omitted; may be null. |\n| `amount` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `currency` | string | May be omitted. |\n| `method` | PaymentMethod | Always present; one of `cash`, `bank_transfer`, `cheque`, `wallet`. |\n| `paid_on` | string | Always present; date YYYY-MM-DD. |\n| `reference` | string | May be omitted; may be null. |\n| `notes` | string | May be omitted; may be null. |\n| `status` | string | Always present; one of `confirmed`, `reversed`. |\n| `reversed_reason` | string | May be omitted; may be null. |\n| `reversed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `received_by` | PersonRef | May be omitted. |\n| `proof_id` | string | May be omitted; may be null; UUID. |\n| `allocations` | array of object | May be omitted. |\n| `unallocated` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Part of the payment not applied to any invoice and not refunded (credit). |\n| `refunded` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Approved refunds taken out of this payment. |\n| `refunds` | array of object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)"
   },
   "post": {
    "tags": [
     "Payments"
    ],
    "summary": "Record a payment",
    "description": "What a gateway or bank feed calls when money arrived. A receipt number is issued and the amount allocated oldest due first (or to the invoices named). Use Idempotency-Key with the gateway's transaction id so a retry never double-posts. Idempotency-Key is required on this operation.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:payments`.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `family_account_id` | string | Required; UUID; at most 40 characters. |\n| `amount` | string | Required; pattern `^\\d+\\.\\d{2}$`; at most 80 characters; Amount with two decimals, as a string; never negative. |\n| `method` | PaymentMethod | Required; one of `cash`, `bank_transfer`, `cheque`, `wallet`. |\n| `paid_on` | string | Required; date YYYY-MM-DD; at most 40 characters; Today or earlier. |\n| `reference` | string | Optional; at most 80 characters; Cheque, transfer or wallet reference, kept on the receipt. On POST /school/payments an external_ref, when sent, is stored as the reference instead and this field is ignored. |\n| `notes` | string | Optional; at most 300 characters. |\n| `invoice_ids` | array of string | Optional; at most 500 items; Restrict allocation to these invoices. Omit to allocate oldest due first across the family's open invoices. Empty list keeps the whole amount unallocated. |\n| `external_ref` | string | Optional; at most 80 characters; Gateway or bank transaction id; stored as the receipt reference and unique per school. |\n\n**Response 201:** `{ data, meta }` where `data` is one Payment.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `receipt_no` | string | Always present. |\n| `family_account_id` | string | Always present; UUID. |\n| `family_name` | string | May be omitted. |\n| `family_phone` | string | May be omitted; may be null. |\n| `amount` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `currency` | string | May be omitted. |\n| `method` | PaymentMethod | Always present; one of `cash`, `bank_transfer`, `cheque`, `wallet`. |\n| `paid_on` | string | Always present; date YYYY-MM-DD. |\n| `reference` | string | May be omitted; may be null. |\n| `notes` | string | May be omitted; may be null. |\n| `status` | string | Always present; one of `confirmed`, `reversed`. |\n| `reversed_reason` | string | May be omitted; may be null. |\n| `reversed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `received_by` | PersonRef | May be omitted. |\n| `proof_id` | string | May be omitted; may be null; UUID. |\n| `allocations` | array of object | May be omitted. |\n| `unallocated` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Part of the payment not applied to any invoice and not refunded (credit). |\n| `refunded` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Approved refunds taken out of this payment. |\n| `refunds` | array of object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "allOf": [
         {
          "$ref": "#/components/schemas/PaymentWrite"
         },
         {
          "type": "object",
          "properties": {
           "external_ref": {
            "type": "string",
            "description": "Gateway or bank transaction id; stored as the receipt reference and unique per school.",
            "maxLength": 80
           }
          },
          "additionalProperties": false
         }
        ]
       },
       "example": {
        "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "amount": "12500.00",
        "method": "cash",
        "paid_on": "2026-09-01",
        "reference": "string",
        "notes": "string",
        "invoice_ids": [
         "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
        ],
        "external_ref": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Payment"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "receipt_no": "RCP-2026-000045",
          "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "family_name": "string",
          "family_phone": "string",
          "amount": "12500.00",
          "currency": "string",
          "method": "cash",
          "paid_on": "2026-09-01",
          "reference": "string",
          "notes": "string",
          "status": "confirmed",
          "reversed_reason": "string",
          "reversed_at": "2026-09-22T09:30:00Z",
          "received_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "proof_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "allocations": [
           {
            "invoice_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "invoice_no": "string",
            "student_name": "string",
            "period_start": "2026-09-22",
            "amount": "12500.00"
           }
          ],
          "unallocated": "12500.00",
          "refunded": "12500.00",
          "refunds": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "refund_no": "string",
            "amount": "12500.00",
            "method": "cash",
            "status": "pending",
            "refunded_on": "2026-09-22"
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:payments"
    ],
    "x-idempotency-required": true
   }
  },
  "/school/payments/{id}": {
   "get": {
    "tags": [
     "Payments"
    ],
    "summary": "One payment",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Payment"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "receipt_no": "RCP-2026-000045",
          "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "family_name": "string",
          "family_phone": "string",
          "amount": "12500.00",
          "currency": "string",
          "method": "cash",
          "paid_on": "2026-09-01",
          "reference": "string",
          "notes": "string",
          "status": "confirmed",
          "reversed_reason": "string",
          "reversed_at": "2026-09-22T09:30:00Z",
          "received_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "proof_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "allocations": [
           {
            "invoice_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "invoice_no": "string",
            "student_name": "string",
            "period_start": "2026-09-22",
            "amount": "12500.00"
           }
          ],
          "unallocated": "12500.00",
          "refunded": "12500.00",
          "refunds": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "refund_no": "string",
            "amount": "12500.00",
            "method": "cash",
            "status": "pending",
            "refunded_on": "2026-09-22"
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ],
    "description": "One payment.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one Payment.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `receipt_no` | string | Always present. |\n| `family_account_id` | string | Always present; UUID. |\n| `family_name` | string | May be omitted. |\n| `family_phone` | string | May be omitted; may be null. |\n| `amount` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `currency` | string | May be omitted. |\n| `method` | PaymentMethod | Always present; one of `cash`, `bank_transfer`, `cheque`, `wallet`. |\n| `paid_on` | string | Always present; date YYYY-MM-DD. |\n| `reference` | string | May be omitted; may be null. |\n| `notes` | string | May be omitted; may be null. |\n| `status` | string | Always present; one of `confirmed`, `reversed`. |\n| `reversed_reason` | string | May be omitted; may be null. |\n| `reversed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `received_by` | PersonRef | May be omitted. |\n| `proof_id` | string | May be omitted; may be null; UUID. |\n| `allocations` | array of object | May be omitted. |\n| `unallocated` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Part of the payment not applied to any invoice and not refunded (credit). |\n| `refunded` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Approved refunds taken out of this payment. |\n| `refunds` | array of object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/payments/{id}/receipt-pdf": {
   "get": {
    "tags": [
     "Payments"
    ],
    "summary": "Receipt as PDF",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "url": {
             "type": "string",
             "format": "uri"
            },
            "expires_at": {
             "type": "string",
             "format": "date-time"
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "url": "https://erp.example.pk/samas/hook",
          "expires_at": "2026-09-22T09:30:00Z"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ],
    "description": "Receipt as PDF.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `url` | string | May be omitted; https URL. |\n| `expires_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/dues": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "Dues by class",
    "description": "Outstanding per class in overdue buckets. For a reminder service or a dashboard.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:fees`.\n\n**Response 200:** `{ data, meta }` where `data` is a list of DuesRow.\n\n| Field | Type | Notes |\n|---|---|---|\n| `class_id` | string | May be omitted; UUID. |\n| `class_name` | string | May be omitted. |\n| `students_due` | integer | May be omitted. |\n| `outstanding` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `overdue_0_30` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `overdue_31_60` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `overdue_61_plus` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/DuesRow"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "class_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "class_name": "string",
           "students_due": 1,
           "outstanding": "12500.00",
           "overdue_0_30": "12500.00",
           "overdue_31_60": "12500.00",
           "overdue_61_plus": "12500.00"
          }
         ],
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:fees"
    ]
   }
  },
  "/school/proofs": {
   "get": {
    "tags": [
     "Payment proofs"
    ],
    "summary": "Payment proofs",
    "description": "Slips parents uploaded, for a reconciliation tool to match against a bank feed.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `read:proofs`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `status` | query | string | Optional; one of `waiting`, `matched`, `returned`, `rejected`. |\n| `fee_month` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of Proof.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `family_account_id` | string | Always present; UUID. |\n| `family_name` | string | May be omitted. |\n| `family_phone` | string | May be omitted; may be null. |\n| `student_ids` | array of string | Always present. |\n| `students` | array of object | May be omitted. |\n| `fee_month` | string | Always present; date YYYY-MM-DD; First day of the month. |\n| `amount` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `method` | PaymentMethod | Always present; one of `cash`, `bank_transfer`, `cheque`, `wallet`. |\n| `reference` | string | May be omitted; may be null. |\n| `file_url` | string | May be omitted; may be null; Short-lived signed URL (5 minutes) or null. |\n| `status` | string | Always present; one of `waiting`, `matched`, `returned`, `rejected`. |\n| `return_reason` | string | May be omitted; may be null. |\n| `duplicate_of` | string | May be omitted; may be null; UUID. |\n| `matched_payment_id` | string | May be omitted; may be null; UUID. |\n| `submitted_by` | PersonRef | May be omitted. |\n| `submitted_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `reviewed_by` | PersonRef | May be omitted. |\n| `reviewed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `kind` | string | Always present; one of `slip`, `claim`; claim: the parent said \"I have paid\" on one invoice (POST /me/invoices/{id}/paid), with or without a slip attached. slip: a proof sent by month (POST /me/proofs), with or without a file, tied to no invoice. |\n| `invoice_id` | string | May be omitted; may be null; UUID; The invoice the parent answered: set for a claim, null for a slip. |\n| `invoice_no` | string | May be omitted; may be null. |\n| `paid_on` | string | May be omitted; may be null; date YYYY-MM-DD; The day the parent says they paid, for a claim. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "status",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "waiting",
        "matched",
        "returned",
        "rejected"
       ]
      }
     },
     {
      "name": "fee_month",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      }
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Proof"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "family_name": "string",
           "family_phone": "string",
           "student_ids": [
            "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
           ],
           "students": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            }
           ],
           "fee_month": "2026-09-01",
           "amount": "12500.00",
           "method": "cash",
           "reference": "string",
           "file_url": "string",
           "status": "waiting",
           "return_reason": "string",
           "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "matched_payment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "submitted_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "submitted_at": "2026-09-22T09:30:00Z",
           "reviewed_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "reviewed_at": "2026-09-22T09:30:00Z",
           "kind": "slip",
           "invoice_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "invoice_no": "string",
           "paid_on": "2026-09-22"
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "read:proofs"
    ]
   }
  },
  "/school/proofs/{id}/match": {
   "post": {
    "tags": [
     "Payment proofs"
    ],
    "summary": "Match a proof to a payment",
    "description": "Confirms the payment on the family account and issues a receipt. Idempotency-Key is required on this operation.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `write:proofs`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `paid_on` | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `invoice_ids` | array of string | Optional; at most 500 items. |\n\n**Response 200:** `{ data, meta }` where `data` is one Payment.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `receipt_no` | string | Always present. |\n| `family_account_id` | string | Always present; UUID. |\n| `family_name` | string | May be omitted. |\n| `family_phone` | string | May be omitted; may be null. |\n| `amount` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `currency` | string | May be omitted. |\n| `method` | PaymentMethod | Always present; one of `cash`, `bank_transfer`, `cheque`, `wallet`. |\n| `paid_on` | string | Always present; date YYYY-MM-DD. |\n| `reference` | string | May be omitted; may be null. |\n| `notes` | string | May be omitted; may be null. |\n| `status` | string | Always present; one of `confirmed`, `reversed`. |\n| `reversed_reason` | string | May be omitted; may be null. |\n| `reversed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `received_by` | PersonRef | May be omitted. |\n| `proof_id` | string | May be omitted; may be null; UUID. |\n| `allocations` | array of object | May be omitted. |\n| `unallocated` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Part of the payment not applied to any invoice and not refunded (credit). |\n| `refunded` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Approved refunds taken out of this payment. |\n| `refunds` | array of object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "paid_on": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01",
          "maxLength": 40
         },
         "invoice_ids": {
          "type": "array",
          "items": {
           "type": "string",
           "format": "uuid",
           "maxLength": 40
          },
          "maxItems": 500
         }
        },
        "required": [
         "paid_on"
        ],
        "additionalProperties": false
       },
       "example": {
        "paid_on": "2026-09-01",
        "invoice_ids": [
         "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Payment"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "receipt_no": "RCP-2026-000045",
          "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "family_name": "string",
          "family_phone": "string",
          "amount": "12500.00",
          "currency": "string",
          "method": "cash",
          "paid_on": "2026-09-01",
          "reference": "string",
          "notes": "string",
          "status": "confirmed",
          "reversed_reason": "string",
          "reversed_at": "2026-09-22T09:30:00Z",
          "received_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "proof_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "allocations": [
           {
            "invoice_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "invoice_no": "string",
            "student_name": "string",
            "period_start": "2026-09-22",
            "amount": "12500.00"
           }
          ],
          "unallocated": "12500.00",
          "refunded": "12500.00",
          "refunds": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "refund_no": "string",
            "amount": "12500.00",
            "method": "cash",
            "status": "pending",
            "refunded_on": "2026-09-22"
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "write:proofs"
    ],
    "x-idempotency-required": true
   }
  },
  "/school/webhooks": {
   "get": {
    "tags": [
     "Webhooks"
    ],
    "summary": "Webhook endpoints",
    "description": "The school's own endpoints. A connected Quran reading app's endpoint is not listed; it belongs to the app (Settings, Quran reading apps).\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `webhooks`.\n\n**Response 200:** `{ data, meta }` where `data` is a list of WebhookEndpoint.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `url` | string | Always present; https URL. |\n| `events` | array of WebhookEvent | Always present. |\n| `is_active` | boolean | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `last_delivery_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `failing_since` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/WebhookEndpoint"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "url": "https://erp.example.pk/samas/hook",
           "events": [
            "invoice.issued"
           ],
           "is_active": true,
           "created_at": "2026-09-22T09:30:00Z",
           "last_delivery_at": "2026-09-22T09:30:00Z",
           "failing_since": "2026-09-22T09:30:00Z"
          }
         ],
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "webhooks"
    ]
   },
   "post": {
    "tags": [
     "Webhooks"
    ],
    "summary": "Create a webhook endpoint",
    "description": "The signing secret is returned once. Deliveries carry X-Samas-Signature (HMAC-SHA256 of the raw body) and X-Samas-Timestamp; reject anything older than 5 minutes.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `webhooks`.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `url` | string | Required; https URL; at most 2048 characters. |\n| `events` | array of WebhookEvent | Required; at most 500 items. |\n\n**Response 201:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `url` | string | Always present; https URL. |\n| `events` | array of WebhookEvent | Always present. |\n| `is_active` | boolean | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `last_delivery_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `failing_since` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `secret` | string | Always present. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "url": {
          "type": "string",
          "format": "uri",
          "maxLength": 2048
         },
         "events": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/WebhookEvent"
          },
          "maxItems": 500
         }
        },
        "required": [
         "url",
         "events"
        ],
        "additionalProperties": false
       },
       "example": {
        "url": "https://erp.example.pk/samas/hook",
        "events": [
         "invoice.issued"
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/WebhookEndpoint"
            },
            {
             "type": "object",
             "properties": {
              "secret": {
               "type": "string"
              }
             },
             "required": [
              "secret"
             ]
            }
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "url": "https://erp.example.pk/samas/hook",
          "events": [
           "invoice.issued"
          ],
          "is_active": true,
          "created_at": "2026-09-22T09:30:00Z",
          "last_delivery_at": "2026-09-22T09:30:00Z",
          "failing_since": "2026-09-22T09:30:00Z",
          "secret": "string"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "webhooks"
    ]
   }
  },
  "/school/webhooks/{id}": {
   "delete": {
    "tags": [
     "Webhooks"
    ],
    "summary": "Delete a webhook endpoint",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "204": {
      "description": "Deleted."
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "webhooks"
    ],
    "description": "Delete a webhook endpoint.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `webhooks`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 204:** no body.\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)"
   },
   "patch": {
    "tags": [
     "Webhooks"
    ],
    "summary": "Change the URL or events, or pause",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "url": {
          "type": "string",
          "format": "uri",
          "maxLength": 2048
         },
         "events": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/WebhookEvent"
          },
          "maxItems": 500
         },
         "is_active": {
          "type": "boolean"
         }
        },
        "additionalProperties": false
       },
       "example": {
        "url": "https://erp.example.pk/samas/hook",
        "events": [
         "invoice.issued"
        ],
        "is_active": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WebhookEndpoint"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "url": "https://erp.example.pk/samas/hook",
          "events": [
           "invoice.issued"
          ],
          "is_active": true,
          "created_at": "2026-09-22T09:30:00Z",
          "last_delivery_at": "2026-09-22T09:30:00Z",
          "failing_since": "2026-09-22T09:30:00Z"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "webhooks"
    ],
    "description": "Change the URL or events, or pause.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `webhooks`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `url` | string | Optional; https URL; at most 2048 characters. |\n| `events` | array of WebhookEvent | Optional; at most 500 items. |\n| `is_active` | boolean | Optional. |\n\n**Response 200:** `{ data, meta }` where `data` is one WebhookEndpoint.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `url` | string | Always present; https URL. |\n| `events` | array of WebhookEvent | Always present. |\n| `is_active` | boolean | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `last_delivery_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `failing_since` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/webhooks/{id}/deliveries": {
   "get": {
    "tags": [
     "Webhooks"
    ],
    "summary": "Recent deliveries",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/id"
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "$ref": "#/components/parameters/size"
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/WebhookDelivery"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "endpoint_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "event": "invoice.issued",
           "payload": {},
           "attempts": 1,
           "status": "pending",
           "last_status_code": 1,
           "next_attempt_at": "2026-09-22T09:30:00Z",
           "created_at": "2026-09-22T09:30:00Z"
          }
         ],
         "page": {
          "next": "string",
          "size": 1,
          "total": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "webhooks"
    ],
    "description": "Recent deliveries.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `webhooks`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n| `cursor` | query | string | Optional; at most 200 characters. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, page, meta }` where `data` is a page of WebhookDelivery.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | May be omitted; UUID. |\n| `endpoint_id` | string | May be omitted; UUID. |\n| `event` | string | May be omitted; one of `invoice.issued`, `invoice.voided`, `payment.recorded`, `payment.reversed`, `proof.submitted`, `proof.matched`, `student.enrolled`, `student.updated`, `attendance.saved`, `lesson.assigned`, `lesson.graded`, `juz.tested`, `link.approved`, `link.revoked`, `recitation.reviewed`, `live.session.started`, `live.session.ended`, `ping`; A WebhookEvent, or an event only a connected Quran reading app's endpoint receives (link.approved, link.revoked, recitation.reviewed, live.session.started, live.session.ended), or ping. |\n| `payload` | object | May be omitted. |\n| `attempts` | integer | May be omitted. |\n| `status` | string | May be omitted; one of `pending`, `delivered`, `failed`, `skipped_unlinked`; skipped_unlinked: a delivery to a Quran reading app dropped because the link ended before it was sent. |\n| `last_status_code` | integer | May be omitted; may be null. |\n| `next_attempt_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/webhooks/{id}/test": {
   "post": {
    "tags": [
     "Webhooks"
    ],
    "summary": "Send a test event",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WebhookDelivery"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "endpoint_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "event": "invoice.issued",
          "payload": {},
          "attempts": 1,
          "status": "pending",
          "last_status_code": 1,
          "next_attempt_at": "2026-09-22T09:30:00Z",
          "created_at": "2026-09-22T09:30:00Z"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-scopes": [
     "webhooks"
    ],
    "description": "Send a test event.\n\n**Who can call:** school API key (X-Api-Key: sk_school_...). API keys need scope `webhooks`.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one WebhookDelivery.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | May be omitted; UUID. |\n| `endpoint_id` | string | May be omitted; UUID. |\n| `event` | string | May be omitted; one of `invoice.issued`, `invoice.voided`, `payment.recorded`, `payment.reversed`, `proof.submitted`, `proof.matched`, `student.enrolled`, `student.updated`, `attendance.saved`, `lesson.assigned`, `lesson.graded`, `juz.tested`, `link.approved`, `link.revoked`, `recitation.reviewed`, `live.session.started`, `live.session.ended`, `ping`; A WebhookEvent, or an event only a connected Quran reading app's endpoint receives (link.approved, link.revoked, recitation.reviewed, live.session.started, live.session.ended), or ping. |\n| `payload` | object | May be omitted. |\n| `attempts` | integer | May be omitted. |\n| `status` | string | May be omitted; one of `pending`, `delivered`, `failed`, `skipped_unlinked`; skipped_unlinked: a delivery to a Quran reading app dropped because the link ended before it was sent. |\n| `last_status_code` | integer | May be omitted; may be null. |\n| `next_attempt_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n\n**Errors**\n\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/school/public/learn/n/{token}": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "A shared study guide",
    "description": "The public page of one study guide, without signing in (L63, L64): the school, the course, the date, the guide's text and pictures, her original photos, and whether a PDF and a ZIP can be downloaded. Picture links last 15 minutes. Never her surname, class or other notes; her first name only when she agreed. LEARN_LINK_INACTIVE (404) for an unknown, revoked or expired link, the same whatever the reason (L65). At most 60 reads a minute per link and 120 per address; answered noindex and no-store, with no cookie. Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** anyone.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `token` | path | string | Required; at most 64 characters. |\n| `version` | query | integer | Optional; from 1 to 200; One version of the guide (L106); default the current one. LEARN_LINK_INACTIVE for a version that does not exist. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnPublicGuide.\n\n| Field | Type | Notes |\n|---|---|---|\n| `school` | object | Always present. |\n| `course_name` | string | Always present; at most 120 characters. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `scholar_label` | string | Always present; at most 80 characters; Her first name when she agreed (L66), else \"A saMas scholar\". |\n| `guide` | object | Always present. |\n| `pictures` | array of LearnPublicPicture | Always present. |\n| `thumbnail` | LearnPublicPicture | May be omitted. |\n| `originals` | array of LearnPublicPicture | Always present. |\n| `downloads` | object | Always present. |\n| `version` | object | May be omitted. |\n| `versions` | array of object | May be omitted. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "maxLength": 64
      },
      "description": "22 URL-safe characters. Anything else is answered as an inactive link."
     },
     {
      "name": "version",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 200,
       "description": "One version of the guide (L106); default the current one. LEARN_LINK_INACTIVE for a version that does not exist."
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnPublicGuide"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "school": {
           "name": "string",
           "crest_url": "https://erp.example.pk/samas/hook"
          },
          "course_name": "string",
          "note_date": "2026-09-01",
          "scholar_label": "string",
          "guide": {
           "title": "string",
           "big_idea": "string",
           "quote": {
            "text": "string",
            "by": "string"
           },
           "subject": "string",
           "sections": [
            {
             "heading": "string",
             "emoji": "string",
             "hook": "string",
             "lines": [
              "string"
             ],
             "spot": "string"
            }
           ],
           "formulas": [
            {
             "label": "string",
             "expr": "string",
             "note": "string",
             "section": 1
            }
           ],
           "tables": [
            {
             "title": "string",
             "columns": [
              "string"
             ],
             "rows": [
              []
             ],
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "string",
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ]
          },
          "pictures": [
           {
            "ordinal": 1,
            "url": "https://erp.example.pk/samas/hook",
            "width": 1,
            "height": 1,
            "content_type": "string",
            "alt": "string"
           }
          ],
          "thumbnail": {
           "ordinal": 1,
           "url": "https://erp.example.pk/samas/hook",
           "width": 1,
           "height": 1,
           "content_type": "string",
           "alt": "string"
          },
          "originals": [
           {
            "ordinal": 1,
            "url": "https://erp.example.pk/samas/hook",
            "width": 1,
            "height": 1,
            "content_type": "string",
            "alt": "string"
           }
          ],
          "downloads": {
           "pdf": true,
           "zip": true
          },
          "version": {
           "number": 1,
           "current": 1,
           "text_kept": true
          },
          "versions": [
           {
            "number": 1,
            "change_kind": "string",
            "change_note": "string",
            "text_kept": true,
            "created_at": "2026-09-22T09:30:00Z"
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/learn/n/{token}/download/{kind}": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "Download a shared study guide",
    "description": "302 to a 15-minute link: kind pdf is the guide's pages, zip adds her original photos, the summary as text and the link (L68). LEARN_LINK_INACTIVE (404) as for the page; NOT_FOUND when that file was not made. Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** anyone.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `token` | path | string | Required; at most 64 characters. |\n| `kind` | path | string | Required; one of `pdf`, `zip`. |\n| `version` | query | integer | Optional; from 1 to 200; One version of the guide (L106); default the current one. LEARN_LINK_INACTIVE for a version that does not exist. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "maxLength": 64
      },
      "description": "22 URL-safe characters. Anything else is answered as an inactive link."
     },
     {
      "name": "kind",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "enum": [
        "pdf",
        "zip"
       ]
      }
     },
     {
      "name": "version",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 200,
       "description": "One version of the guide (L106); default the current one. LEARN_LINK_INACTIVE for a version that does not exist."
      }
     }
    ],
    "responses": {
     "302": {
      "description": "Redirect to the file, valid 15 minutes."
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/board/{token}": {
   "get": {
    "tags": [
     "Branding"
    ],
    "summary": "A classroom board",
    "description": "Now and next for the one class of a board link, without signing in: no student names, read-only, at most 2 reads a minute per link. BOARD_LINK_ENDED when the link was turned off or has expired.\n\n**Who can call:** anyone.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `token` | path | string | Required; pattern `^brd_[0-9a-f]{48}$`; at most 52 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one TimetableNow.\n\n| Field | Type | Notes |\n|---|---|---|\n| `section` | object | Always present. |\n| `on_date` | string | Always present; date YYYY-MM-DD. |\n| `at` | string | Always present; The school's local time this answer is for. |\n| `closed` | string | May be omitted; may be null; Why the school is closed today, when it is. |\n| `now` | NowLesson | May be omitted. |\n| `next` | NowLesson | May be omitted. |\n| `later` | array of NowLesson | Always present. |\n| `exam` | array of object | Always present. |\n| `school` | string | May be omitted; may be null; On the public board only. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^brd_[0-9a-f]{48}$",
       "maxLength": 52
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TimetableNow"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "label": "string"
          },
          "on_date": "2026-09-01",
          "at": "08:50",
          "closed": "string",
          "now": {
           "starts_at": "08:00",
           "ends_at": "08:40",
           "subject": "string",
           "teacher": "string",
           "cover": true,
           "room": "string",
           "minutes_left": 1,
           "period_no": 1,
           "live_url": "https://erp.example.pk/samas/hook"
          },
          "next": {
           "starts_at": "08:00",
           "ends_at": "08:40",
           "subject": "string",
           "teacher": "string",
           "cover": true,
           "room": "string",
           "minutes_left": 1,
           "period_no": 1,
           "live_url": "https://erp.example.pk/samas/hook"
          },
          "later": [
           {
            "starts_at": "08:00",
            "ends_at": "08:40",
            "subject": "string",
            "teacher": "string",
            "cover": true,
            "room": "string",
            "minutes_left": 1,
            "period_no": 1,
            "live_url": "https://erp.example.pk/samas/hook"
           }
          ],
          "exam": [
           {
            "subject": "string",
            "starts_at": "string",
            "ends_at": "string",
            "room": "string"
           }
          ],
          "school": "string"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/email-open/{token}": {
   "get": {
    "tags": [
     "Branding"
    ],
    "summary": "An email was opened",
    "description": "The one-pixel picture inside a welcome email. Loading it marks the email opened. Needs no credentials and always answers with the same transparent GIF (image/gif), whatever the token.\n\n**Who can call:** anyone.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `token` | path | string | Required; pattern `^[0-9a-f]{32}(\\.gif)?$`; at most 36 characters. |\n\n**Response 200:** no body.\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[0-9a-f]{32}(\\.gif)?$",
       "maxLength": 36
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A 1 by 1 transparent GIF.",
      "content": {
       "image/gif": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/site/home": {
   "get": {
    "tags": [
     "Site"
    ],
    "summary": "The home page's data",
    "description": "Listed real schools (never demo or test schools), quotes with consent (and a guardian's for anyone under eighteen), the five newest released public roadmap items, and the listed schools' looks (brands, six at most, HM42), for samas.school. Needs no credentials and sets no cookie. Cached 60 seconds. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** anyone.\n\n**Response 200:** `{ data, meta }` where `data` is one SiteHome.\n\n| Field | Type | Notes |\n|---|---|---|\n| `schools` | array of object | Always present. |\n| `school_count` | integer | Always present; Listed schools, computed (HM4). |\n| `quotes` | array of object | Always present. |\n| `updates` | array of SitePublicItem | Always present. |\n| `hero_names` | array of object | Always present; at most 3 items; The hero register's names, in order, only for scholars who agreed (HM26); empty means the rows read [student]. |\n| `brands` | array of SiteBrand | Always present; at most 6 items; The listed schools' own looks, by name, six at most (HM42): the hero's chips and browser frame. Only schools that are listed, active, real and set up; empty when none is. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SiteHome"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "schools": [
           {
            "name": "string",
            "web_url": "string",
            "blurb": "string"
           }
          ],
          "school_count": 1,
          "quotes": [
           {
            "display_name": "Sample S.",
            "quote": "string",
            "person_kind": "scholar",
            "school_name": "string"
           }
          ],
          "updates": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "title": "string",
            "summary": "string",
            "stage": "considering",
            "weight_schools": 1,
            "public_votes": 1,
            "released_on": "2026-09-22",
            "pictures": [
             {
              "url": "string",
              "name": "string"
             }
            ]
           }
          ],
          "hero_names": [
           {
            "display": "Sample S."
           }
          ],
          "brands": [
           {
            "name": "string",
            "subdomain": "nexus",
            "web_url": "https://nexus.samas.school",
            "primary_color": "#1e3a8a",
            "accent_color": "#f59e0b",
            "font": "inter",
            "style": "standard",
            "logo_url": "https://erp.example.pk/samas/hook"
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/site/roadmap": {
   "get": {
    "tags": [
     "Site"
    ],
    "summary": "The public roadmap",
    "description": "Roadmap items a super admin made public (RM13a), at considering, planned, building or released (shown as Live), with how many schools asked and how many visitors said me too. Released items carry their release pictures (links valid one day). Never a school's name. Cached 60 seconds. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** anyone.\n\n**Response 200:** `{ data, meta }` where `data` is one SiteRoadmap.\n\n| Field | Type | Notes |\n|---|---|---|\n| `items` | array of SitePublicItem | Always present. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SiteRoadmap"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "items": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "title": "string",
            "summary": "string",
            "stage": "considering",
            "weight_schools": 1,
            "public_votes": 1,
            "released_on": "2026-09-22",
            "pictures": [
             {
              "url": "string",
              "name": "string"
             }
            ]
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/site/updates": {
   "get": {
    "tags": [
     "Site"
    ],
    "summary": "What's new",
    "description": "Released public roadmap items, newest first, with their pictures, for samas.school/new. Cached 60 seconds. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** anyone.\n\n**Response 200:** `{ data, meta }` where `data` is one SiteUpdates.\n\n| Field | Type | Notes |\n|---|---|---|\n| `updates` | array of SitePublicItem | Always present. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SiteUpdates"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "updates": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "title": "string",
            "summary": "string",
            "stage": "considering",
            "weight_schools": 1,
            "public_votes": 1,
            "released_on": "2026-09-22",
            "pictures": [
             {
              "url": "string",
              "name": "string"
             }
            ]
           }
          ]
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/site/whoami": {
   "get": {
    "tags": [
     "Site"
    ],
    "summary": "Which door is mine",
    "description": "For samas.school's two doors (HM2, HM37): a visitor who is already signed in sees Open my school or Open my Learn. The page sends the sign-in token it already holds (the session cookie the sign-in page left, read in the browser) as a bearer token, after the first paint, and only when that cookie is there; with no token the answer is none and nothing is looked up. A token is accepted when the sign-in service signed it, for a person saMas knows, issued within the last 7 days (the home page never loads the sign-in code that would refresh it); the answer is only a door, never data about the person. A bad or unknown token is answered none, never an error. Never cached (no-store); no cookie is set. At most 60 per address in 10 minutes. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** anyone.\n\n**Response 200:** `{ data, meta }` where `data` is one SiteWhoami.\n\n| Field | Type | Notes |\n|---|---|---|\n| `kind` | string | Always present; one of `none`, `admin`, `scholar`, `other`; none: no valid sign-in was sent. admin: a school admin, principal or finance admin (the door reads Open my school). scholar: a student whose school has Learn on (Open my Learn). other: anyone else signed in; both doors stay as they are. |\n| `school_subdomain` | string | May be omitted; at most 63 characters; The school's address, for admin and scholar only. |\n| `url` | string | May be omitted; https URL; at most 200 characters; Where the door goes: the school's admin site, or her Learn. For admin and scholar only. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SiteWhoami"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "kind": "none",
          "school_subdomain": "string",
          "url": "https://erp.example.pk/samas/hook"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  },
  "/school/public/site/requests": {
   "post": {
    "tags": [
     "Site"
    ],
    "summary": "Ask for a demo school",
    "description": "A school owner's request from samas.school (HM17, HM18): no account is made. The saMas team is emailed. 201 with a REQ-YYYY-NNNN reference; 204 and nothing kept when the website field is filled. At most 5 per address in 10 minutes (RATE_LIMITED) and 20 a day (SITE_LIMIT). Body 2 KB at most. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** anyone.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `name` | string | Required; 2 to 80 characters. |\n| `school_name` | string | Required; 2 to 120 characters. |\n| `city` | string | Required; 2 to 60 characters. |\n| `size_band` | SizeBand | Required; one of `under_50`, `50_200`, `200_800`, `800_plus`; Roughly how many students. |\n| `contact` | string | Required; 5 to 200 characters; An email (name@example.com) or a phone or WhatsApp number (+92300 0000000). |\n| `message` | string | Optional; may be null; at most 1000 characters; What it should do first. |\n| `answers` | object | Optional; may be null; The fit quiz's answers, q1 to q5, each up to 80 characters; anything else is dropped (HM16). |\n| `website` | string | Optional; at most 200 characters; Leave empty. A form field people never see; a filled one is answered 204 and nothing is kept. |\n\n**Response 201:** `{ data, meta }` where `data` is one SiteRef.\n\n| Field | Type | Notes |\n|---|---|---|\n| `ref` | string | Always present. |\n\n**Response 204:** no body.\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/SiteRequestWrite"
       },
       "example": {
        "name": "string",
        "school_name": "string",
        "city": "string",
        "size_band": "under_50",
        "contact": "string",
        "message": "string",
        "answers": {
         "q1": "string",
         "q2": "string",
         "q3": "string",
         "q4": "string",
         "q5": "string"
        },
        "website": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SiteRef"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "ref": "REQ-2026-0001"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "204": {
      "description": "Taken, nothing kept."
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-people-only": true,
    "security": [],
    "x-public": true
   }
  },
  "/school/public/site/ideas": {
   "post": {
    "tags": [
     "Site"
    ],
    "summary": "Wish for a feature",
    "description": "A wish from samas.school/features: it reaches the saMas team's Queue as an outside report of kind idea (OUT-YYYY-NNNN) and shows publicly only once it joins a roadmap item a super admin made public. 204 when the website field is filled. At most 3 per address in 10 minutes and 20 a day. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** anyone.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `title` | string | Required; 3 to 120 characters. |\n| `body` | string | Required; 3 to 1000 characters; What it would let you do. |\n| `requester_kind` | string | Required; one of `admin`, `teacher`, `finance`, `parent`, `student`, `prospect`. |\n| `contact_email` | string | Optional; may be null; email address; at most 200 characters; Optional: to hear when it is planned and when it is live. Used for nothing else (HM36). |\n| `website` | string | Optional; at most 200 characters; Leave empty (see SiteRequestWrite). |\n\n**Response 201:** `{ data, meta }` where `data` is one SiteRef.\n\n| Field | Type | Notes |\n|---|---|---|\n| `ref` | string | Always present. |\n\n**Response 204:** no body.\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/SiteIdeaWrite"
       },
       "example": {
        "title": "string",
        "body": "string",
        "requester_kind": "admin",
        "contact_email": "parent@example.pk",
        "website": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SiteRef"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "ref": "REQ-2026-0001"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "204": {
      "description": "Taken, nothing kept."
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-people-only": true,
    "security": [],
    "x-public": true
   }
  },
  "/school/public/site/roadmap/{id}/vote": {
   "post": {
    "tags": [
     "Site"
    ],
    "summary": "Me too",
    "description": "One per browser per public item; the same token again takes it back. Counted apart from the schools' weight. NOT_FOUND for an item that is not public, or is released. At most 30 per address in 10 minutes. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** anyone.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `token` | string | Required; pattern `^[0-9a-f]{32}$`; at most 80 characters; 32 hex characters the browser made once and keeps; the server keeps only a salted hash. |\n\n**Response 200:** `{ data, meta }` where `data` is one SiteVoteResult.\n\n| Field | Type | Notes |\n|---|---|---|\n| `counted` | boolean | Always present; true: counted now; false: taken back. |\n| `public_votes` | integer | Always present. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 404 NOT_FOUND (missing, or not visible to this caller)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/SiteVote"
       },
       "example": {
        "token": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SiteVoteResult"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "counted": true,
          "public_votes": 1
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-people-only": true,
    "security": [],
    "x-public": true
   }
  },
  "/school/public/problem-reports": {
   "post": {
    "tags": [
     "Issues"
    ],
    "summary": "Report a problem without signing in",
    "description": "For when the app cannot be used: not signed in, or signed in but the account will not load (the trouble screen), or the app itself will not start. Needs no credentials. It reaches the saMas team, who are emailed, with an OUT-YYYY-NNNN reference. The same optional parts as a report from inside the app (the error, calls, steps, device and console), checked and cut the same way. No screenshot. 5 a day from one address and 200 a day in all (ISSUE_LIMIT).\n\n**Who can call:** anyone.\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `body` | string | Required; 3 to 2000 characters; What happened. |\n| `severity` | IssueSeverity | Required; one of `blocked`, `slowed`, `minor`; How much it stops the person: blocked (cannot carry on), slowed (can work around it), minor (a small thing). |\n| `contact_email` | string | Optional; may be null; email address; at most 200 characters; Where to answer, if the person wants an answer. Optional. |\n| `host` | string | Optional; may be null; pattern `^[a-z0-9.-]{1,100}$`; at most 100 characters; The site the person was on, such as nexus.samas.school. |\n| `signed_in` | boolean | Optional; True when they were signed in but their account would not load. |\n| `screen` | string | Optional; may be null; at most 200 characters. |\n| `app_version` | string | Optional; may be null; at most 40 characters. |\n| `browser` | string | Optional; may be null; at most 120 characters. |\n| `incident` | IssueIncident | Optional. |\n| `requests` | array of IssueRequestSeen | Optional; at most 30 items. |\n| `steps` | array of string | Optional; at most 30 items. |\n| `device` | IssueDevice | Optional. |\n| `console` | array of IssueConsoleLine | Optional; at most 100 items. |\n\n**Response 201:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `ref` | string | Always present. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/OutsideReportWrite"
       },
       "example": {
        "body": "string",
        "severity": "blocked",
        "contact_email": "parent@example.pk",
        "host": "string",
        "signed_in": true,
        "screen": "string",
        "app_version": "string",
        "browser": "string",
        "incident": {
         "kind": "api",
         "reference": "string",
         "code": "string",
         "status": 1,
         "method": "GET",
         "route": "string",
         "message": "string",
         "at": "2026-09-22T09:30:00Z",
         "stack": [
          "string"
         ]
        },
        "requests": [
         {
          "method": "GET",
          "route": "string",
          "status": 1,
          "ms": 1,
          "reference": "string",
          "at": "2026-09-22T09:30:00Z"
         }
        ],
        "steps": [
         "string"
        ],
        "device": {
         "viewport": "1280x800",
         "zoom": 1.5,
         "touch": true,
         "online": true,
         "connection": "string",
         "time_zone": "Asia/Karachi",
         "clock_offset_s": 1,
         "language": "string"
        },
        "console": [
         {
          "kind": "error",
          "text": "string",
          "count": 1,
          "first_at": "2026-09-22T09:30:00Z",
          "last_at": "2026-09-22T09:30:00Z",
          "stack": [
           "string"
          ]
         }
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "ref": {
             "type": "string",
             "example": "OUT-2026-0001"
            }
           },
           "required": [
            "ref"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "ref": "OUT-2026-0001"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "x-people-only": true,
    "security": [],
    "x-public": true
   }
  },
  "/school/public/branding": {
   "get": {
    "tags": [
     "Branding"
    ],
    "summary": "A school's look, without signing in",
    "description": "Name, colors, font, logo, sign-in image and welcome message, found by the school's web address (host) or school code. Needs no credentials, so a school's sign-in page and its own website can use it. Cached for 5 minutes.\n\n**Who can call:** anyone.\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `host` | query | string | Optional; at most 100 characters; A school web address such as primary.samas.school, or just primary. |\n| `school` | query | string | Optional; at most 40 characters; School code. |\n\n**Response 200:** `{ data, meta }` where `data` is one Branding.\n\n| Field | Type | Notes |\n|---|---|---|\n| `school_id` | string | Always present; UUID. |\n| `school_code` | string | Always present. |\n| `school_name` | string | Always present. |\n| `display_name` | string | Always present; Name shown in the app; the school name unless set. |\n| `tagline` | string | May be omitted; may be null. |\n| `subdomain` | string | May be omitted; may be null. |\n| `web_url` | string | May be omitted; may be null; https URL; The school's own web address, for example https://primary.samas.school. |\n| `primary_color` | string | Always present; pattern `^#[0-9a-f]{6}$`. |\n| `accent_color` | string | Always present; pattern `^#[0-9a-f]{6}$`. |\n| `font` | BrandFont | Always present; one of `inter`, `poppins`, `nunito`, `lora`, `merriweather`, `noto-sans`, `hanken-grotesk`, `bricolage`, `figtree`, `noto-nastaliq-urdu`. |\n| `logo_url` | string | May be omitted; may be null; https URL. |\n| `signin_image_url` | string | May be omitted; may be null; https URL. |\n| `welcome_message` | string | May be omitted; may be null. |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `style` | BrandStyle | May be omitted; one of `standard`, `paper`, `garden`; standard: the saMas look. paper: ivory paper with a faint pattern, serif headings (Instrument Serif), a gold rule on cards and a midnight sidebar; meant for a dark main color. garden: an ivory courtyard with a faint eight-point star lattice, Amiri headings, a gold rule on cards and a deep green sidebar; meant for a deep green main color. |\n\n**Errors**\n\n- 400 validation (UNKNOWN_FIELD, MISSING_FIELD, INVALID_TYPE, TEXT_TOO_LONG, INVALID_ENUM, OUT_OF_RANGE and others; see Validation rules)\n- 401 UNAUTHENTICATED or TOKEN_EXPIRED\n- 403 NOT_ALLOWED (role, scope, school or read-only View-as)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "host",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "A school web address such as primary.samas.school, or just primary.",
       "maxLength": 100
      }
     },
     {
      "name": "school",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "School code.",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Branding"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "school_code": "string",
          "school_name": "string",
          "display_name": "string",
          "tagline": "string",
          "subdomain": "primary",
          "web_url": "https://erp.example.pk/samas/hook",
          "primary_color": "#1e3a8a",
          "accent_color": "#f59e0b",
          "font": "inter",
          "logo_url": "https://erp.example.pk/samas/hook",
          "signin_image_url": "https://erp.example.pk/samas/hook",
          "welcome_message": "string",
          "updated_at": "2026-09-22T09:30:00Z",
          "style": "standard"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     },
     "429": {
      "$ref": "#/components/responses/TooManyRequests"
     }
    },
    "security": [],
    "x-public": true
   }
  }
 },
 "components": {
  "securitySchemes": {
   "clerkToken": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT",
    "description": "Session token of a signed-in person (the same token the web app uses). Role and school come from the person's active context."
   },
   "schoolKey": {
    "type": "apiKey",
    "in": "header",
    "name": "X-Api-Key",
    "description": "Per-school key (sk_school_...). Scoped; created by the school admin under Settings, API keys, or by the platform."
   },
   "platformKey": {
    "type": "apiKey",
    "in": "header",
    "name": "X-Api-Key",
    "description": "Platform key (sk_platform_...). Issued only to saMas School operators, never to schools."
   },
   "partnerKey": {
    "type": "apiKey",
    "in": "header",
    "name": "X-Api-Key",
    "description": "Partner key (sk_partner_...), one per school that connected the app, made by the school admin under Settings, Madrasa, Quran reading apps. Works on /v1/partner only."
   }
  },
  "parameters": {
   "cursor": {
    "name": "cursor",
    "in": "query",
    "schema": {
     "type": "string",
     "maxLength": 200
    },
    "description": "Cursor from the previous page."
   },
   "size": {
    "name": "size",
    "in": "query",
    "schema": {
     "type": "integer",
     "minimum": 1,
     "maximum": 200,
     "default": 50
    },
    "description": "Page size."
   },
   "id": {
    "name": "id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "student_id": {
    "name": "student_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "section_id": {
    "name": "section_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "shift_id": {
    "name": "shift_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "version_id": {
    "name": "version_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "timing_id": {
    "name": "timing_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "room_id": {
    "name": "room_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "teacher_id": {
    "name": "teacher_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    }
   },
   "idempotency": {
    "name": "Idempotency-Key",
    "in": "header",
    "schema": {
     "type": "string",
     "maxLength": 64
    },
    "description": "Send a unique value per intended write; a retry with the same key returns the original result instead of a duplicate."
   },
   "school_header": {
    "name": "X-School-Id",
    "in": "header",
    "schema": {
     "type": "string",
     "format": "uuid",
     "maxLength": 40
    },
    "description": "Platform keys only, and required for them on the Admin API: the school the key works inside for this request (L81). A signed-in person's school comes from their sign-in and a school key's from the key; both ignore it."
   },
   "request_id": {
    "name": "X-Request-Id",
    "in": "header",
    "schema": {
     "type": "string",
     "maxLength": 64
    },
    "description": "Echoed back in the response for tracing."
   }
  },
  "responses": {
   "BadRequest": {
    "description": "Validation failed.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": {
        "code": "MISSING_FIELD",
        "message": "A required field is missing.",
        "hint": "'full_name' is required.",
        "field": "/body/full_name",
        "request_id": "req_01J8Z3X4"
       }
      }
     }
    }
   },
   "Unauthorized": {
    "description": "Missing or invalid credentials.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": {
        "code": "MISSING_FIELD",
        "message": "A required field is missing.",
        "hint": "'full_name' is required.",
        "field": "/body/full_name",
        "request_id": "req_01J8Z3X4"
       }
      }
     }
    }
   },
   "Forbidden": {
    "description": "Authenticated, but not allowed (role, scope, school, read-only View-as).",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": {
        "code": "MISSING_FIELD",
        "message": "A required field is missing.",
        "hint": "'full_name' is required.",
        "field": "/body/full_name",
        "request_id": "req_01J8Z3X4"
       }
      }
     }
    }
   },
   "NotFound": {
    "description": "Not found, or not visible to this caller (the two are not distinguished).",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": {
        "code": "MISSING_FIELD",
        "message": "A required field is missing.",
        "hint": "'full_name' is required.",
        "field": "/body/full_name",
        "request_id": "req_01J8Z3X4"
       }
      }
     }
    }
   },
   "Conflict": {
    "description": "Rule violated (SECTION_FULL, HAS_PAYMENTS, ON_HOLD, ALREADY_DECIDED, RECEIPT_IMMUTABLE...).",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": {
        "code": "MISSING_FIELD",
        "message": "A required field is missing.",
        "hint": "'full_name' is required.",
        "field": "/body/full_name",
        "request_id": "req_01J8Z3X4"
       }
      }
     }
    }
   },
   "TooManyRequests": {
    "description": "Rate limit exceeded. Retry-After says when.",
    "headers": {
     "Retry-After": {
      "schema": {
       "type": "integer"
      }
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      },
      "example": {
       "error": {
        "code": "MISSING_FIELD",
        "message": "A required field is missing.",
        "hint": "'full_name' is required.",
        "field": "/body/full_name",
        "request_id": "req_01J8Z3X4"
       }
      }
     }
    }
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "object",
      "properties": {
       "code": {
        "type": "string",
        "description": "Stable machine code, the same one the database raises (NOT_ALLOWED, SECTION_FULL, RECEIPT_IMMUTABLE, ON_HOLD...).",
        "example": "SECTION_FULL"
       },
       "message": {
        "type": "string",
        "description": "Human sentence for the person, safe to show.",
        "example": "This section is full (30 of 30 seats). Raise the capacity or pick another section."
       },
       "hint": {
        "type": "string",
        "description": "Optional extra guidance."
       },
       "details": {
        "type": "object",
        "additionalProperties": true,
        "description": "Facts beside the hint when the server has them, for a screen to offer the fix: the next free number (ADMISSION_NO_TAKEN, ROLL_NO_TAKEN), who holds a phone, masked (GUARDIAN_PHONE_IN_USE), the next school day (DAY_CLOSED)."
       },
       "request_id": {
        "type": "string"
       }
      },
      "required": [
       "code",
       "message"
      ]
     }
    },
    "required": [
     "error"
    ]
   },
   "Page": {
    "type": "object",
    "properties": {
     "next": {
      "type": [
       "string",
       "null"
      ],
      "description": "Opaque cursor for the next page, null when there is no more."
     },
     "size": {
      "type": "integer",
      "description": "Items in this page."
     },
     "total": {
      "type": "integer",
      "description": "All matching items across pages. Only on lists that can count cheaply (the audit trail)."
     }
    },
    "required": [
     "next",
     "size"
    ]
   },
   "Money": {
    "type": "object",
    "properties": {
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "currency": {
      "type": "string",
      "description": "ISO 4217",
      "example": "PKR"
     }
    },
    "required": [
     "amount",
     "currency"
    ]
   },
   "School": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "code": {
      "type": "string",
      "example": "DEMO"
     },
     "name": {
      "type": "string",
      "example": "saMas School Demo"
     },
     "is_active": {
      "type": "boolean"
     },
     "settings": {
      "type": "object",
      "properties": {
       "directory_visibility": {
        "type": "string",
        "enum": [
         "staff_only",
         "school"
        ],
        "description": "Who parents and students can see in the people directory."
       },
       "timezone": {
        "type": "string",
        "example": "Asia/Karachi"
       },
       "locale": {
        "type": "string",
        "example": "en-PK"
       },
       "fee_reply_to": {
        "type": [
         "string",
         "null"
        ],
        "format": "email",
        "description": "Where replies to fee reminders and receipts go. Empty: the emails say not to reply."
       },
       "admission_no_prefix": {
        "type": [
         "string",
         "null"
        ],
        "maxLength": 10,
        "description": "In front of the admission numbers the school offers (NX- gives NX-2026-0013). Empty: the year alone."
       },
       "self_attendance": {
        "type": "string",
        "enum": [
         "on",
         "off"
        ],
        "description": "Students mark themselves present from a live class (I'm here, HM40). Off by default; left out until a school sets it, which also means off."
       }
      }
     }
    },
    "required": [
     "id",
     "code",
     "name"
    ]
   },
   "Branding": {
    "type": "object",
    "properties": {
     "school_id": {
      "type": "string",
      "format": "uuid"
     },
     "school_code": {
      "type": "string"
     },
     "school_name": {
      "type": "string"
     },
     "display_name": {
      "type": "string",
      "description": "Name shown in the app; the school name unless set."
     },
     "tagline": {
      "type": [
       "string",
       "null"
      ]
     },
     "subdomain": {
      "type": [
       "string",
       "null"
      ],
      "example": "primary"
     },
     "web_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "The school's own web address, for example https://primary.samas.school."
     },
     "primary_color": {
      "type": "string",
      "pattern": "^#[0-9a-f]{6}$",
      "example": "#1e3a8a"
     },
     "accent_color": {
      "type": "string",
      "pattern": "^#[0-9a-f]{6}$",
      "example": "#f59e0b"
     },
     "font": {
      "$ref": "#/components/schemas/BrandFont"
     },
     "logo_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri"
     },
     "signin_image_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri"
     },
     "welcome_message": {
      "type": [
       "string",
       "null"
      ]
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "style": {
      "$ref": "#/components/schemas/BrandStyle"
     }
    },
    "required": [
     "school_id",
     "school_code",
     "school_name",
     "display_name",
     "primary_color",
     "accent_color",
     "font"
    ]
   },
   "BrandFont": {
    "type": "string",
    "enum": [
     "inter",
     "poppins",
     "nunito",
     "lora",
     "merriweather",
     "noto-sans",
     "hanken-grotesk",
     "bricolage",
     "figtree",
     "noto-nastaliq-urdu"
    ]
   },
   "BrandStyle": {
    "type": "string",
    "enum": [
     "standard",
     "paper",
     "garden"
    ],
    "description": "standard: the saMas look. paper: ivory paper with a faint pattern, serif headings (Instrument Serif), a gold rule on cards and a midnight sidebar; meant for a dark main color. garden: an ivory courtyard with a faint eight-point star lattice, Amiri headings, a gold rule on cards and a deep green sidebar; meant for a deep green main color."
   },
   "BrandingWrite": {
    "type": "object",
    "properties": {
     "display_name": {
      "type": [
       "string",
       "null"
      ],
      "minLength": 2,
      "maxLength": 80
     },
     "tagline": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "primary_color": {
      "type": "string",
      "description": "Buttons and the sidebar carry white text on it, so it must be dark enough (contrast at least 4.5 to 1).",
      "pattern": "^#[0-9a-fA-F]{6}$"
     },
     "accent_color": {
      "type": "string",
      "description": "Highlights carry dark text on it (contrast at least 4.5 to 1 with near-black).",
      "pattern": "^#[0-9a-fA-F]{6}$"
     },
     "font": {
      "$ref": "#/components/schemas/BrandFont"
     },
     "style": {
      "$ref": "#/components/schemas/BrandStyle"
     },
     "logo_upload_id": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 64,
      "description": "From the uploads endpoint. null removes the logo."
     },
     "signin_image_upload_id": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 64,
      "description": "From the uploads endpoint. null removes the image."
     },
     "welcome_message": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300
     }
    },
    "description": "Fields left out keep their value."
   },
   "BrandingUpload": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "logo",
       "signin_image",
       "letter_image"
      ],
      "description": "letter_image: a picture for a welcome letter (the letter in another language, set as an image so every mail app shows its script)."
     },
     "content_type": {
      "type": "string",
      "enum": [
       "image/png",
       "image/jpeg",
       "image/webp"
      ]
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 2097152
     }
    },
    "required": [
     "kind",
     "content_type",
     "size_bytes"
    ]
   },
   "UploadUrl": {
    "type": "object",
    "properties": {
     "upload_id": {
      "type": "string"
     },
     "url": {
      "type": "string",
      "format": "uri",
      "description": "PUT the file bytes here within 15 minutes."
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "upload_id",
     "url",
     "expires_at"
    ]
   },
   "AiConnection": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "assistant": {
      "type": "string",
      "example": "Claude"
     },
     "person": {
      "type": [
       "string",
       "null"
      ]
     },
     "role": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Role"
       },
       {
        "type": "null"
       }
      ]
     },
     "school_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "connected_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_used_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "disconnected_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "assistant",
     "connected_at"
    ]
   },
   "FamilyStatement": {
    "type": "object",
    "properties": {
     "family": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       },
       "phone": {
        "type": [
         "string",
         "null"
        ]
       },
       "students": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          },
          "full_name": {
           "type": "string"
          },
          "admission_no": {
           "type": [
            "string",
            "null"
           ]
          },
          "class_name": {
           "type": [
            "string",
            "null"
           ]
          },
          "section_name": {
           "type": [
            "string",
            "null"
           ]
          }
         },
         "required": [
          "id",
          "full_name"
         ]
        }
       }
      },
      "required": [
       "id",
       "name",
       "students"
      ]
     },
     "school_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "currency": {
      "type": "string",
      "example": "PKR"
     },
     "from": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "to": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "opening_balance": {
      "type": "string",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "description": "What the family owed on the day before 'from'. Negative means the family was in credit."
     },
     "closing_balance": {
      "type": "string",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "description": "What the family owed at the end of 'to'."
     },
     "totals": {
      "type": "object",
      "properties": {
       "charged": {
        "type": "string",
        "description": "Amount with two decimals, as a string.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "cancelled": {
        "type": "string",
        "description": "Invoices voided in the period.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "paid": {
        "type": "string",
        "description": "Amount with two decimals, as a string.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "reversed": {
        "type": "string",
        "description": "Payments reversed in the period.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "refunded": {
        "type": "string",
        "description": "Amount with two decimals, as a string.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       }
      },
      "required": [
       "charged",
       "cancelled",
       "paid",
       "reversed",
       "refunded"
      ]
     },
     "lines": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "date": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "kind": {
         "type": "string",
         "enum": [
          "invoice",
          "void",
          "payment",
          "reversal",
          "refund"
         ]
        },
        "reference": {
         "type": [
          "string",
          "null"
         ],
         "description": "Invoice, receipt or refund number."
        },
        "description": {
         "type": "string"
        },
        "student": {
         "type": [
          "string",
          "null"
         ]
        },
        "debit": {
         "type": "string",
         "description": "Adds to what is owed.",
         "pattern": "^-?\\d+\\.\\d{2}$",
         "example": "12500.00"
        },
        "credit": {
         "type": "string",
         "description": "Takes off what is owed.",
         "pattern": "^-?\\d+\\.\\d{2}$",
         "example": "12500.00"
        },
        "balance": {
         "type": "string",
         "pattern": "^-?\\d+\\.\\d{2}$",
         "description": "Running balance after this line."
        },
        "invoice_id": {
         "type": [
          "string",
          "null"
         ],
         "format": "uuid"
        },
        "payment_id": {
         "type": [
          "string",
          "null"
         ],
         "format": "uuid"
        }
       },
       "required": [
        "date",
        "kind",
        "description",
        "debit",
        "credit",
        "balance"
       ]
      }
     },
     "generated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "family",
     "currency",
     "from",
     "to",
     "opening_balance",
     "closing_balance",
     "totals",
     "lines"
    ],
    "description": "A ledger of one family account: invoices as they stand now (late fees included), voids, payments, reversals and refunds, each dated the day it happened, with a running balance."
   },
   "AiAccess": {
    "type": "object",
    "properties": {
     "enabled": {
      "type": "boolean",
      "description": "Staff may connect AI assistants over MCP."
     },
     "hide_names": {
      "type": "boolean",
      "description": "People appear as initials and contact details are removed in what assistants read (staff connections)."
     },
     "parents_enabled": {
      "type": "boolean",
      "description": "Parents may connect AI assistants too. They read only their own children, with names shown. Off by default; turning it off disconnects every parent's assistant."
     },
     "mcp_url": {
      "type": "string",
      "format": "uri"
     },
     "help_url": {
      "type": "string",
      "format": "uri"
     },
     "updated_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "enabled",
     "hide_names",
     "mcp_url",
     "help_url"
    ]
   },
   "ViewAsTarget": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "role": {
      "$ref": "#/components/schemas/Role"
     },
     "school_id": {
      "type": "string",
      "format": "uuid"
     },
     "school_name": {
      "type": "string"
     },
     "school_code": {
      "type": "string"
     },
     "is_school_admin": {
      "type": "boolean",
      "description": "True when the person is a school admin of that school. Such people are only offered on the beta sites."
     }
    },
    "required": [
     "user_id",
     "role",
     "school_id",
     "school_name"
    ]
   },
   "SchoolUpdateWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "description": "The subject: what is fixed or new.",
      "minLength": 3,
      "maxLength": 120
     },
     "message": {
      "type": "string",
      "description": "What changed and how to try it, in plain words.",
      "minLength": 10,
      "maxLength": 2000
     },
     "link": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 201,
      "pattern": "^/[A-Za-z0-9/_?=&.%-]{0,200}$",
      "description": "The page of the app to try it on, starting with /. Help when left out."
     },
     "include_finance": {
      "type": "boolean",
      "description": "Also the school's finance in-charge, for a change about finance."
     },
     "picture_upload_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "maxItems": 3,
      "description": "Pictures of the fix or feature, attached to the email: up to 3, each from POST /platform/schools/{id}/update-email/upload-url."
     }
    },
    "required": [
     "title",
     "message"
    ]
   },
   "SchoolUpdateQueued": {
    "type": "object",
    "properties": {
     "emails": {
      "type": "integer",
      "description": "Emails queued: one for up to five people."
     },
     "recipients": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "user_id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": [
          "string",
          "null"
         ]
        },
        "email": {
         "type": "string"
        },
        "role": {
         "type": "string"
        }
       },
       "required": [
        "user_id",
        "full_name",
        "email",
        "role"
       ]
      }
     },
     "copied_to": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "The sender, copied on each email."
      }
     },
     "pictures": {
      "type": "integer",
      "description": "Pictures attached to each email."
     }
    },
    "required": [
     "emails",
     "recipients",
     "copied_to"
    ]
   },
   "GuardianChoice": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "also": {
      "type": [
       "string",
       "null"
      ],
      "description": "Null for someone whose account is a parent here. Otherwise what else they are: their role at this school (teacher, school_admin and so on), or another_school when their account belongs to another school first."
     },
     "signed_in": {
      "type": "boolean",
      "description": "Whether they have signed in to saMas School at least once."
     },
     "children": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "student_id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string"
        },
        "relation": {
         "type": "string",
         "enum": [
          "mother",
          "father",
          "guardian",
          "other"
         ]
        },
        "section": {
         "type": [
          "string",
          "null"
         ],
         "description": "Class and section, such as Grade 5 A."
        }
       },
       "required": [
        "student_id",
        "full_name",
        "relation"
       ]
      },
      "description": "The students at this school they are already a guardian of, so a sibling's parent is found in one go."
     }
    },
    "required": [
     "user_id",
     "full_name",
     "email",
     "phone",
     "also"
    ]
   },
   "Person": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "description": "Internal id, stable across sign-in providers.",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ],
      "example": "Ahmed Khan"
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email"
     },
     "phone": {
      "type": [
       "string",
       "null"
      ],
      "example": "+923001234567"
     },
     "role": {
      "anyOf": [
       {
        "$ref": "#/components/schemas/Role"
       },
       {
        "type": "string",
        "const": "super_admin"
       },
       {
        "type": "null"
       }
      ],
      "description": "Default role. super_admin for platform staff; null for an account with no school yet."
     },
     "is_active": {
      "type": "boolean"
     },
     "signed_in": {
      "type": "boolean",
      "description": "False while the person is invited and has not signed in yet. Present on GET /admin/people."
     }
    },
    "required": [
     "user_id",
     "full_name",
     "role"
    ]
   },
   "SearchHit": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "student",
       "person",
       "family",
       "invoice",
       "receipt"
      ]
     },
     "id": {
      "type": "string",
      "description": "Open it with the matching endpoint for its kind.",
      "format": "uuid"
     },
     "label": {
      "type": "string",
      "description": "Name, invoice number or receipt number.",
      "example": "Ali Khan"
     },
     "subtitle": {
      "type": [
       "string",
       "null"
      ],
      "description": "Short context: admission number and class, role and phone, student and month, family and amount.",
      "example": "ADM-0042 . Grade 5 A"
     },
     "score": {
      "type": "number",
      "description": "Higher is a better match. Exact matches score 2, prefixes 1.5, fuzzy matches 0.3 to 1."
     }
    },
    "required": [
     "kind",
     "id",
     "label",
     "score"
    ]
   },
   "SessionPerson": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ],
      "example": "Hina Siddiqui"
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email"
     },
     "phone": {
      "type": [
       "string",
       "null"
      ],
      "example": "+923001234567"
     },
     "role": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/SessionRole"
       },
       {
        "type": "null"
       }
      ]
     },
     "organization_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "school_name": {
      "type": [
       "string",
       "null"
      ],
      "example": "saMas School Demo"
     },
     "school_code": {
      "type": [
       "string",
       "null"
      ],
      "example": "DEMO"
     },
     "is_super_admin": {
      "type": "boolean"
     },
     "is_active": {
      "type": "boolean"
     }
    },
    "required": [
     "user_id",
     "is_super_admin",
     "is_active"
    ]
   },
   "Membership": {
    "type": "object",
    "properties": {
     "school_id": {
      "type": "string",
      "format": "uuid"
     },
     "school_name": {
      "type": "string"
     },
     "school_code": {
      "type": "string"
     },
     "role": {
      "$ref": "#/components/schemas/Role"
     },
     "is_default": {
      "type": "boolean"
     },
     "is_active": {
      "type": "boolean",
      "description": "True for the role and school currently in use."
     },
     "web_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "The school's own web address, when it has one."
     }
    },
    "required": [
     "school_id",
     "school_name",
     "school_code",
     "role",
     "is_default",
     "is_active"
    ]
   },
   "ViewAsSession": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "target_user_id": {
      "type": "string",
      "format": "uuid"
     },
     "target_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "target_email": {
      "type": [
       "string",
       "null"
      ]
     },
     "target_role": {
      "$ref": "#/components/schemas/Role"
     },
     "school_id": {
      "type": "string",
      "format": "uuid"
     },
     "school_name": {
      "type": "string"
     },
     "mode": {
      "type": "string",
      "enum": [
       "read_only",
       "full"
      ]
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "description": "Null for a quiet session (a super admin who gave no reason; nobody is emailed)."
     },
     "started_at": {
      "type": "string",
      "format": "date-time"
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "extended_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "can_extend": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "target_user_id",
     "target_role",
     "school_id",
     "mode",
     "reason",
     "started_at",
     "expires_at",
     "can_extend"
    ]
   },
   "OfficeOverview": {
    "type": "object",
    "properties": {
     "school_date": {
      "type": "string",
      "description": "Today in the school's time zone.",
      "format": "date",
      "example": "2026-09-01"
     },
     "currency": {
      "type": "string",
      "example": "PKR"
     },
     "people": {
      "type": "object",
      "properties": {
       "students": {
        "type": "integer"
       },
       "staff": {
        "type": "integer"
       },
       "teachers": {
        "type": "integer"
       },
       "parents": {
        "type": "integer"
       },
       "classes": {
        "type": "integer"
       },
       "sections": {
        "type": "integer"
       }
      },
      "required": [
       "students",
       "staff",
       "teachers",
       "parents",
       "classes",
       "sections"
      ]
     },
     "attendance_today": {
      "type": "object",
      "properties": {
       "present": {
        "type": "integer"
       },
       "absent": {
        "type": "integer"
       },
       "late": {
        "type": "integer"
       },
       "excused": {
        "type": "integer"
       },
       "marked_sections": {
        "type": "integer"
       },
       "total_sections": {
        "type": "integer"
       },
       "rate": {
        "type": [
         "number",
         "null"
        ],
        "description": "Present or late, as a percentage of the students marked. Null before any register."
       }
      },
      "required": [
       "present",
       "absent",
       "late",
       "excused",
       "marked_sections",
       "total_sections",
       "rate"
      ]
     },
     "attendance_trend": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "date": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "rate": {
         "type": "number"
        },
        "present": {
         "type": "integer"
        },
        "absent": {
         "type": "integer"
        }
       },
       "required": [
        "date",
        "rate",
        "present",
        "absent"
       ]
      },
      "description": "The last 30 days with any register marked, oldest first."
     },
     "unmarked_sections": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "section_id": {
         "type": "string",
         "format": "uuid"
        },
        "class_name": {
         "type": "string"
        },
        "section_name": {
         "type": "string"
        },
        "class_teacher": {
         "type": [
          "string",
          "null"
         ]
        }
       },
       "required": [
        "section_id",
        "class_name",
        "section_name"
       ]
      },
      "description": "Sections without today's register. Empty while no section has marked (for example on a holiday)."
     },
     "money": {
      "type": "object",
      "properties": {
       "collected_this_month": {
        "type": "string",
        "description": "Amount with two decimals, as a string.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "collected_last_month": {
        "type": "string",
        "description": "Amount with two decimals, as a string.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "outstanding": {
        "type": "string",
        "description": "Amount with two decimals, as a string.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "overdue": {
        "type": "string",
        "description": "Amount with two decimals, as a string.",
        "pattern": "^-?\\d+\\.\\d{2}$",
        "example": "12500.00"
       },
       "overdue_invoices": {
        "type": "integer"
       }
      },
      "required": [
       "collected_this_month",
       "collected_last_month",
       "outstanding",
       "overdue",
       "overdue_invoices"
      ]
     },
     "waiting": {
      "type": "object",
      "properties": {
       "proofs": {
        "type": "integer"
       },
       "refunds": {
        "type": "integer"
       },
       "discounts": {
        "type": "integer"
       }
      },
      "required": [
       "proofs",
       "refunds",
       "discounts"
      ]
     },
     "upcoming": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "title": {
         "type": "string"
        },
        "kind": {
         "type": "string",
         "example": "exam"
        },
        "starts_at": {
         "type": "string",
         "format": "date-time"
        },
        "ends_at": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        },
        "all_day": {
         "type": "boolean"
        }
       },
       "required": [
        "id",
        "title",
        "kind",
        "starts_at",
        "all_day"
       ]
      }
     },
     "tasks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/OfficeTask"
      },
      "description": "Today at the school (PRD 5.12 DE13): what needs a person now, computed from the school's own data each time. Done items drop off. Only the tasks the caller's role may act on."
     },
     "actions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/OfficeAction"
      },
      "description": "Do something (DE14): the six things the office does most, most used first, with how many times each was saved in the last 30 days."
     }
    },
    "required": [
     "school_date",
     "currency",
     "people",
     "attendance_today",
     "attendance_trend",
     "unmarked_sections",
     "money",
     "waiting",
     "upcoming"
    ]
   },
   "OfficeTask": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "proofs_waiting",
       "attendance_missing",
       "admissions_due",
       "reminders_sent",
       "invoices_missing"
      ],
      "description": "proofs_waiting: parents' payment claims and slips waiting for a match. attendance_missing: sections with a lesson today whose register is not marked after the first period. admissions_due: applications with a test or interview within 2 days. reminders_sent: fee reminders that went out today. invoices_missing: active students with no invoice for this month once the generate day has passed, and students with no section (since 2026-10-27)."
     },
     "count": {
      "type": "integer",
      "minimum": 1
     },
     "title": {
      "type": "string",
      "example": "2 parents say they paid, waiting for a match"
     },
     "detail": {
      "type": [
       "string",
       "null"
      ],
      "description": "One short line under the title: who, how old, which class.",
      "example": "oldest 3 days"
     },
     "link": {
      "type": "string",
      "description": "The screen to open, in the right place.",
      "example": "/finance/proofs"
     }
    },
    "required": [
     "kind",
     "count",
     "title",
     "detail",
     "link"
    ]
   },
   "OfficeAction": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "enum": [
       "enrol",
       "attendance",
       "collect",
       "notice",
       "slip",
       "staff"
      ]
     },
     "title": {
      "type": "string",
      "example": "Enrol a student"
     },
     "link": {
      "type": "string",
      "example": "/admin/students?new=1"
     },
     "screen": {
      "type": "string",
      "description": "The screen key whose saves are counted.",
      "example": "admin-students"
     },
     "count_30d": {
      "type": "integer",
      "minimum": 0,
      "description": "Saves on that screen by people at this school in the last 30 days (help events of kind save)."
     }
    },
    "required": [
     "key",
     "title",
     "link",
     "screen",
     "count_30d"
    ]
   },
   "TeacherOverview": {
    "type": "object",
    "properties": {
     "school_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7,
      "description": "1 is Monday."
     },
     "lessons": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "period_no": {
         "type": "integer"
        },
        "label": {
         "type": [
          "string",
          "null"
         ]
        },
        "starts_at": {
         "type": [
          "string",
          "null"
         ],
         "example": "08:00"
        },
        "ends_at": {
         "type": [
          "string",
          "null"
         ],
         "example": "08:40"
        },
        "section_id": {
         "type": "string",
         "format": "uuid"
        },
        "class_name": {
         "type": "string"
        },
        "section_name": {
         "type": "string"
        },
        "subject": {
         "type": "string"
        },
        "room": {
         "type": [
          "string",
          "null"
         ]
        },
        "register_marked": {
         "type": "boolean",
         "description": "Whether the section's whole-day register is marked today."
        },
        "cover": {
         "oneOf": [
          {
           "$ref": "#/components/schemas/LessonCover"
          },
          {
           "type": "null"
          }
         ],
         "description": "Cover today: a lesson this teacher covers, or one of theirs someone else covers."
        }
       },
       "required": [
        "period_no",
        "section_id",
        "class_name",
        "section_name",
        "subject",
        "register_marked"
       ]
      }
     },
     "sections": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "section_id": {
         "type": "string",
         "format": "uuid"
        },
        "class_name": {
         "type": "string"
        },
        "section_name": {
         "type": "string"
        },
        "is_class_teacher": {
         "type": "boolean"
        },
        "students": {
         "type": "integer"
        },
        "marked_today": {
         "type": "boolean"
        },
        "rate_30d": {
         "type": [
          "number",
          "null"
         ],
         "description": "Attendance over the last 30 days, percent."
        },
        "subjects": {
         "type": "array",
         "items": {
          "type": "string"
         }
        }
       },
       "required": [
        "section_id",
        "class_name",
        "section_name",
        "is_class_teacher",
        "students",
        "marked_today",
        "rate_30d",
        "subjects"
       ]
      }
     }
    },
    "required": [
     "school_date",
     "weekday",
     "lessons",
     "sections"
    ]
   },
   "PlatformSchool": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "display_name": {
      "type": "string"
     },
     "primary_color": {
      "type": "string",
      "pattern": "^#[0-9a-f]{6}$"
     },
     "logo_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri"
     },
     "web_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri"
     },
     "is_active": {
      "type": "boolean"
     },
     "maintenance": {
      "type": "boolean"
     },
     "currency": {
      "type": "string"
     },
     "students": {
      "type": "integer"
     },
     "staff": {
      "type": "integer"
     },
     "sections": {
      "type": "integer"
     },
     "marked_sections_today": {
      "type": "integer"
     },
     "attendance_rate_today": {
      "type": [
       "number",
       "null"
      ]
     },
     "collected_this_month": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "overdue": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "requests_24h": {
      "type": "integer"
     },
     "errors_24h": {
      "type": "integer"
     },
     "last_activity_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "health": {
      "type": "string",
      "enum": [
       "ok",
       "warning",
       "off"
      ],
      "description": "off: suspended or in maintenance; warning: errors in the last day or no activity for a week."
     }
    },
    "required": [
     "id",
     "code",
     "name",
     "display_name",
     "primary_color",
     "is_active",
     "maintenance",
     "currency",
     "students",
     "staff",
     "sections",
     "marked_sections_today",
     "attendance_rate_today",
     "collected_this_month",
     "overdue",
     "requests_24h",
     "errors_24h",
     "health"
    ]
   },
   "PlatformOverview": {
    "type": "object",
    "properties": {
     "totals": {
      "type": "object",
      "properties": {
       "schools": {
        "type": "integer"
       },
       "active_schools": {
        "type": "integer"
       },
       "students": {
        "type": "integer"
       },
       "staff": {
        "type": "integer"
       },
       "parents": {
        "type": "integer"
       },
       "requests_24h": {
        "type": "integer"
       },
       "errors_24h": {
        "type": "integer"
       }
      },
      "required": [
       "schools",
       "active_schools",
       "students",
       "staff",
       "parents",
       "requests_24h",
       "errors_24h"
      ]
     },
     "schools": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/PlatformSchool"
      }
     },
     "alerts": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "key": {
         "type": "string"
        },
        "detail": {
         "type": [
          "string",
          "null"
         ]
        },
        "since": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        }
       },
       "required": [
        "key"
       ]
      }
     }
    },
    "required": [
     "totals",
     "schools",
     "alerts"
    ]
   },
   "SchoolChoice": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "code": {
      "type": "string",
      "example": "DEMO"
     },
     "name": {
      "type": "string",
      "example": "saMas School Demo"
     },
     "display_name": {
      "type": "string",
      "example": "saMas School Demo"
     },
     "primary_color": {
      "type": "string",
      "example": "#1e3a8a",
      "pattern": "^#[0-9a-f]{6}$"
     },
     "logo_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri"
     },
     "web_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "The school's own web address, when it has one."
     },
     "roles": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Role"
      },
      "description": "The caller's roles at this school, the default first. Empty for a super admin with no role there."
     },
     "is_current": {
      "type": "boolean",
      "description": "The school the caller works in now."
     },
     "is_active": {
      "type": "boolean",
      "description": "False for a suspended school."
     }
    },
    "required": [
     "id",
     "code",
     "name",
     "display_name",
     "primary_color",
     "roles",
     "is_current",
     "is_active"
    ]
   },
   "SessionRole": {
    "type": "string",
    "enum": [
     "school_admin",
     "school_principal",
     "school_finance",
     "teacher",
     "class_head",
     "parent",
     "student",
     "school_prefect",
     "super_admin"
    ],
    "description": "A Role, or super_admin for a saMas School super admin."
   },
   "ColorMode": {
    "type": "string",
    "enum": [
     "light",
     "dark",
     "system"
    ],
    "description": "Light, dark, or follow the device."
   },
   "Session": {
    "type": "object",
    "properties": {
     "actor": {
      "allOf": [
       {
        "$ref": "#/components/schemas/SessionPerson"
       }
      ],
      "description": "The person who signed in."
     },
     "effective": {
      "allOf": [
       {
        "$ref": "#/components/schemas/SessionPerson"
       }
      ],
      "description": "The person the app behaves as: the actor, or the person being viewed during View-as."
     },
     "role": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/SessionRole"
       },
       {
        "type": "null"
       }
      ],
      "description": "The active role. super_admin for a super admin working as themselves."
     },
     "school_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The active school."
     },
     "is_super_admin": {
      "type": "boolean",
      "description": "False while viewing as someone, even for a super admin."
     },
     "is_staff": {
      "type": "boolean",
      "description": "saMas staff or a super admin (not while viewing as someone): Labs is shown."
     },
     "can_write": {
      "type": "boolean",
      "description": "False during a read-only View-as session; writes are refused."
     },
     "memberships": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Membership"
      }
     },
     "view_as": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/ViewAsSession"
       },
       {
        "type": "null"
       }
      ]
     },
     "view_as_open_targets": {
      "type": "boolean",
      "description": "True on the beta sites when school admins may be viewed as (and school admins may view as each other). Always false on production. Clients offer View-as on administrators only when this is true."
     },
     "branding": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Branding"
       },
       {
        "type": "null"
       }
      ],
      "description": "The active school's look (the school being viewed during View-as)."
     },
     "add_ons": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "madrasa",
        "learn",
        "charity",
        "live_sabaq",
        "toefl",
        "flashcards"
       ]
      },
      "description": "The add-ons that are on (or in trial) for the active school; the app shows their pages only then."
     },
     "color_mode": {
      "allOf": [
       {
        "$ref": "#/components/schemas/ColorMode"
       }
      ],
      "description": "The signed-in person's own choice of light or dark, kept on their account."
     },
     "admin_site_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "Where this person does admin work: their school's admin site (https://admin.primary.samas.school), the saMas staff site for a super admin, or null for roles that have none."
     },
     "admin_site_required": {
      "type": "boolean",
      "description": "True when admin work is only accepted through the admin site (the regular site should send admins there)."
     }
    },
    "required": [
     "actor",
     "effective",
     "is_super_admin",
     "can_write",
     "memberships"
    ]
   },
   "StudentDetail": {
    "allOf": [
     {
      "$ref": "#/components/schemas/Student"
     },
     {
      "type": "object",
      "properties": {
       "address": {
        "type": [
         "string",
         "null"
        ]
       },
       "previous_school": {
        "type": [
         "string",
         "null"
        ]
       },
       "notes": {
        "type": [
         "string",
         "null"
        ]
       },
       "admission_date": {
        "type": [
         "string",
         "null"
        ],
        "format": "date"
       }
      }
     }
    ]
   },
   "TimetableTarget": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "section",
       "teacher",
       "subject",
       "room"
      ]
     },
     "id": {
      "type": "string",
      "description": "The section, teacher or subject id, or the room's name.",
      "maxLength": 40
     },
     "label": {
      "type": "string",
      "maxLength": 120
     },
     "sub": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "description": "For a teacher, the subjects they teach."
     }
    },
    "required": [
     "kind",
     "id",
     "label"
    ]
   },
   "TimetableLesson": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7,
      "description": "1 is Monday."
     },
     "period_no": {
      "type": "integer"
     },
     "starts_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "ends_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "subject": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string",
          "maxLength": 60
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 80
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20
     },
     "shift": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string",
          "maxLength": 40
         },
         "colour": {
          "type": "string",
          "maxLength": 7
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The shift the lesson is in."
     },
     "span": {
      "type": "integer",
      "minimum": 0,
      "maximum": 3,
      "description": "2 or 3 on the first period of a double or triple, 0 on the periods it continues into."
     },
     "with": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string",
       "maxLength": 80
      },
      "description": "The other classes taught this lesson together."
     },
     "option": {
      "type": "boolean",
      "description": "One option of an option block: other subjects run in the same period for other students. A child's week shows only the child's option."
     },
     "on_date": {
      "type": "string",
      "description": "The date this lesson is shown for: the next time its weekday comes.",
      "format": "date",
      "example": "2026-09-01"
     },
     "cover": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LessonCover"
       },
       {
        "type": "null"
       }
      ],
      "description": "Cover arranged for this lesson on its date: staff see for whom; the class's students and families see the cover teacher only; nobody else."
     },
     "matches": {
      "type": "boolean",
      "description": "False when a subject or room filter leaves this lesson out; screens fade it rather than drop it."
     },
     "live_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "maxLength": 300,
      "description": "The lesson's live-class link (Zoom, Google Meet or Jitsi Meet), when the office or its teacher set one (HM38). Sent to staff and to the class's own students and families; null otherwise."
     }
    },
    "required": [
     "weekday",
     "period_no",
     "starts_at",
     "ends_at",
     "section",
     "matches"
    ]
   },
   "TimetableWeek": {
    "type": "object",
    "properties": {
     "target": {
      "$ref": "#/components/schemas/TimetableTarget"
     },
     "days": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 1,
       "maximum": 7
      }
     },
     "periods": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Period"
      }
     },
     "lessons": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimetableLesson"
      }
     },
     "shifts": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "name": {
         "type": "string",
         "maxLength": 40
        },
        "colour": {
         "type": "string",
         "maxLength": 7
        },
        "is_main": {
         "type": "boolean"
        },
        "periods": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/Period"
         }
        },
        "days": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "weekday": {
            "type": "integer"
           },
           "on_date": {
            "type": "string",
            "description": "The date this weekday is shown for: the next time it comes.",
            "format": "date",
            "example": "2026-09-01"
           },
           "kind": {
            "type": "string",
            "enum": [
             "full",
             "short",
             "own",
             "closed"
            ]
           },
           "ends_after": {
            "type": [
             "integer",
             "null"
            ]
           },
           "closed": {
            "type": [
             "string",
             "null"
            ],
            "maxLength": 80,
            "description": "Why there are no lessons: a closure, a holiday, or Not running."
           },
           "follows": {
            "type": [
             "integer",
             "null"
            ],
            "description": "The weekday whose lessons run on this changed day."
           },
           "timing": {
            "type": [
             "string",
             "null"
            ],
            "maxLength": 40,
            "description": "The timing in force that day."
           },
           "periods": {
            "type": [
             "array",
             "null"
            ],
            "items": {
             "$ref": "#/components/schemas/Period"
            },
            "description": "That day's blocks when they differ from the normal day."
           }
          },
          "required": [
           "weekday",
           "kind"
          ]
         }
        }
       },
       "required": [
        "id",
        "name",
        "periods"
       ]
      }
     },
     "free": {
      "oneOf": [
       {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "weekday": {
           "type": "integer"
          },
          "period_no": {
           "type": "integer"
          },
          "shift_id": {
           "type": "string",
           "format": "uuid"
          },
          "starts_at": {
           "type": "string",
           "maxLength": 5
          },
          "ends_at": {
           "type": "string",
           "maxLength": 5
          }
         },
         "required": [
          "weekday",
          "period_no"
         ]
        }
       },
       {
        "type": "null"
       }
      ],
      "description": "A teacher's free periods: for the office, and for a teacher asking about themselves."
     },
     "summary": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "lessons": {
          "type": "integer"
         },
         "free": {
          "type": "integer"
         },
         "days_working": {
          "type": "integer"
         },
         "latest_finish": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 5
         },
         "classes": {
          "type": "integer"
         },
         "shifts": {
          "type": "integer"
         }
        },
        "required": [
         "lessons",
         "free",
         "days_working",
         "classes"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "Totals, on the same terms as free."
     },
     "showing": {
      "type": "object",
      "properties": {
       "matching": {
        "type": "integer"
       },
       "total": {
        "type": "integer"
       }
      },
      "required": [
       "matching",
       "total"
      ]
     },
     "valid_from": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "The last time a timetable in this week was published."
     }
    },
    "required": [
     "target",
     "days",
     "periods",
     "lessons",
     "showing"
    ]
   },
   "TimetableSearchHit": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "maxLength": 40
     },
     "label": {
      "type": "string",
      "maxLength": 120
     },
     "sub": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "lessons": {
      "type": "integer",
      "description": "Published lessons."
     }
    },
    "required": [
     "id",
     "label",
     "lessons"
    ]
   },
   "TimetableSearch": {
    "type": "object",
    "properties": {
     "teachers": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimetableSearchHit"
      }
     },
     "sections": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimetableSearchHit"
      }
     },
     "subjects": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimetableSearchHit"
      }
     },
     "rooms": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimetableSearchHit"
      }
     }
    },
    "required": [
     "teachers",
     "sections",
     "subjects",
     "rooms"
    ],
    "description": "Families and students get sections only."
   },
   "TimetableFree": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7
     },
     "period": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "period_no": {
          "type": "integer"
         },
         "label": {
          "type": "string",
          "maxLength": 20
         },
         "starts_at": {
          "type": "string",
          "maxLength": 5
         },
         "ends_at": {
          "type": "string",
          "maxLength": 5
         },
         "shift": {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "name": {
            "type": "string",
            "maxLength": 40
           }
          },
          "required": [
           "id",
           "name"
          ]
         }
        },
        "required": [
         "period_no",
         "label",
         "starts_at",
         "ends_at"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "Null outside lesson time."
     },
     "teachers": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "user_id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": [
          "string",
          "null"
         ],
         "maxLength": 120
        },
        "subjects": {
         "type": [
          "string",
          "null"
         ],
         "maxLength": 200
        }
       },
       "required": [
        "user_id",
        "full_name"
       ]
      }
     }
    },
    "required": [
     "weekday",
     "period",
     "teachers"
    ]
   },
   "TeacherSlot": {
    "allOf": [
     {
      "$ref": "#/components/schemas/TimetableSlot"
     },
     {
      "type": "object",
      "properties": {
       "section": {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string"
         },
         "class_name": {
          "type": [
           "string",
           "null"
          ]
         }
        },
        "required": [
         "id",
         "name"
        ]
       }
      },
      "required": [
       "section"
      ]
     }
    ]
   },
   "AttendanceHistory": {
    "type": "object",
    "properties": {
     "records": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/AttendanceRecord"
      }
     },
     "summary": {
      "$ref": "#/components/schemas/AttendanceSummary"
     }
    },
    "required": [
     "records",
     "summary"
    ]
   },
   "Role": {
    "type": "string",
    "enum": [
     "school_admin",
     "school_principal",
     "school_finance",
     "teacher",
     "class_head",
     "parent",
     "student",
     "school_prefect"
    ]
   },
   "AcademicYear": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "2026-27"
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "is_current": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "name",
     "starts_on",
     "ends_on"
    ]
   },
   "Class": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "Grade 5"
     },
     "sort_order": {
      "type": "integer"
     },
     "min_age": {
      "type": [
       "integer",
       "null"
      ]
     },
     "max_age": {
      "type": [
       "integer",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "name"
    ]
   },
   "ClassWithSections": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "Grade 5"
     },
     "sort_order": {
      "type": "integer"
     },
     "min_age": {
      "type": [
       "integer",
       "null"
      ]
     },
     "max_age": {
      "type": [
       "integer",
       "null"
      ]
     },
     "sections": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Section"
      }
     }
    },
    "required": [
     "id",
     "name",
     "sections"
    ]
   },
   "Section": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_name": {
      "type": "string",
      "example": "Grade 5"
     },
     "name": {
      "type": "string",
      "example": "A",
      "pattern": "^[A-Z][A-Z0-9]{0,3}$"
     },
     "capacity": {
      "type": "integer",
      "minimum": 1,
      "maximum": 60
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "example": "Room 105"
     },
     "class_teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "enrolled": {
      "type": "integer",
      "description": "Students, or members for an extra class."
     },
     "kind": {
      "type": "string",
      "enum": [
       "normal",
       "extra"
      ],
      "description": "An extra class is for students who also belong to a normal section."
     },
     "label": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40,
      "description": "An extra class's name, shown instead of the class and section names."
     },
     "shift_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     }
    },
    "required": [
     "id",
     "class_id",
     "name",
     "capacity"
    ]
   },
   "PersonRef": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "user_id",
     "full_name"
    ]
   },
   "Subject": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "Mathematics"
     },
     "code": {
      "type": [
       "string",
       "null"
      ],
      "example": "MATH"
     }
    },
    "required": [
     "id",
     "name"
    ]
   },
   "SectionSubject": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "section_id": {
      "type": "string",
      "format": "uuid"
     },
     "subject": {
      "$ref": "#/components/schemas/Subject"
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "periods_per_week": {
      "type": "integer",
      "minimum": 1,
      "maximum": 10
     }
    },
    "required": [
     "id",
     "section_id",
     "subject"
    ]
   },
   "Student": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "admission_no": {
      "type": "string",
      "example": "DS-2026-001"
     },
     "full_name": {
      "type": "string",
      "example": "Amna Khan"
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "female",
       "male",
       "other",
       null
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "inactive",
       "graduated",
       "withdrawn"
      ]
     },
     "section": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string"
         },
         "class_name": {
          "type": [
           "string",
           "null"
          ]
         },
         "room": {
          "type": [
           "string",
           "null"
          ]
         }
        }
       },
       {
        "type": "null"
       }
      ]
     },
     "section_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Same as section.id; handy for filtering."
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "roll_no": {
      "type": [
       "integer",
       "null"
      ]
     },
     "family_account_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "user_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The student's own sign-in, when they have one. On the list since 2026-10-15 (it was only on StudentDetail)."
     },
     "guardians": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Guardian"
      }
     }
    },
    "required": [
     "id",
     "admission_no",
     "full_name",
     "date_of_birth",
     "status"
    ]
   },
   "StudentWrite": {
    "type": "object",
    "properties": {
     "admission_no": {
      "type": "string",
      "pattern": "^[A-Za-z0-9-]{2,20}$",
      "maxLength": 80
     },
     "full_name": {
      "type": "string",
      "pattern": "^[A-Za-z'\\- ]{2,80}$",
      "maxLength": 80
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01",
      "maxLength": 40
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "female",
       "male",
       "other",
       null
      ]
     },
     "section_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "maxLength": 40
     },
     "roll_no": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 999
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "inactive",
       "graduated",
       "withdrawn"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "maxLength": 254
     },
     "address": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "previous_school": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500
     },
     "user_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Link the student's own sign-in (a person with the student role).",
      "maxLength": 40
     }
    },
    "required": [
     "admission_no",
     "full_name",
     "date_of_birth"
    ],
    "additionalProperties": false
   },
   "Guardian": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "relation": {
      "type": "string",
      "enum": [
       "mother",
       "father",
       "guardian",
       "other"
      ]
     },
     "is_primary": {
      "type": "boolean"
     }
    },
    "required": [
     "user_id",
     "full_name",
     "relation",
     "is_primary"
    ]
   },
   "GuardianEnrol": {
    "type": "object",
    "properties": {
     "person_id": {
      "type": "string",
      "description": "A parent already at the school (from GET /admin/guardian-choices). With it, the other fields are ignored.",
      "format": "uuid"
     },
     "full_name": {
      "type": "string",
      "description": "A new parent to invite: their name, with a phone or an email so they can sign in.",
      "minLength": 2,
      "maxLength": 80
     },
     "phone": {
      "type": "string",
      "pattern": "^\\+[1-9]\\d{6,14}$"
     },
     "email": {
      "type": "string",
      "format": "email",
      "maxLength": 200
     },
     "relation": {
      "type": "string",
      "enum": [
       "mother",
       "father",
       "guardian",
       "other"
      ],
      "description": "Default guardian."
     },
     "is_primary": {
      "type": "boolean",
      "default": true,
      "description": "The primary guardian gets the alerts. Default true."
     }
    },
    "description": "The family in the same call: link a parent already at the school (person_id), or invite a new one (full_name with phone or email) through the same path as POST /admin/people with the parent role, welcome email queued, and link them. Everything is one transaction: a refusal on the parent leaves no child behind. GUARDIAN_PHONE_IN_USE when the phone is already someone's at this school (details.person_id says whose, the name masked)."
   },
   "StudentEnrol": {
    "type": "object",
    "properties": {
     "admission_no": {
      "type": "string",
      "description": "Optional since 2026-10-14: left out, the next free number from the school's pattern is used (GET /admin/students/next-admission-no). A typed one already in use is refused (ADMISSION_NO_TAKEN, details.next_free).",
      "pattern": "^[A-Za-z0-9-]{2,20}$"
     },
     "full_name": {
      "type": "string",
      "pattern": "^[A-Za-z'\\- ]{2,80}$",
      "maxLength": 80
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01",
      "maxLength": 40
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "female",
       "male",
       "other",
       null
      ]
     },
     "section_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "maxLength": 40
     },
     "roll_no": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 999
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "inactive",
       "graduated",
       "withdrawn"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "maxLength": 254
     },
     "address": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "previous_school": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500
     },
     "user_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Link the student's own sign-in (a person with the student role).",
      "maxLength": 40
     },
     "guardian": {
      "$ref": "#/components/schemas/GuardianEnrol"
     }
    },
    "required": [
     "full_name",
     "date_of_birth"
    ]
   },
   "NextAdmissionNo": {
    "type": "object",
    "properties": {
     "admission_no": {
      "type": "string",
      "example": "2026-0013"
     },
     "pattern": {
      "type": "string",
      "description": "The school's pattern: the calendar year and a four digit number, with the school's prefix when it has one.",
      "example": "YYYY-NNNN"
     }
    },
    "required": [
     "admission_no",
     "pattern"
    ]
   },
   "NextRollNo": {
    "type": "object",
    "properties": {
     "next_free": {
      "type": "integer",
      "description": "The highest roll in the section plus one."
     },
     "roll": {
      "type": [
       "integer",
       "null"
      ],
      "description": "The roll asked about, when one was."
     },
     "taken": {
      "type": [
       "boolean",
       "null"
      ],
      "description": "Whether that roll is already someone's; null when no roll was asked about."
     },
     "taken_by": {
      "type": [
       "string",
       "null"
      ],
      "description": "Whose it is, as the first name and an initial."
     }
    },
    "required": [
     "next_free"
    ]
   },
   "PhotoUpload": {
    "type": "object",
    "properties": {
     "content_type": {
      "type": "string",
      "enum": [
       "image/jpeg",
       "image/png",
       "image/webp"
      ]
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 10485760
     }
    },
    "required": [
     "content_type",
     "size_bytes"
    ]
   },
   "AttendancePhotoRead": {
    "type": "object",
    "properties": {
     "rows": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "roll": {
         "type": "integer"
        },
        "student_id": {
         "type": [
          "string",
          "null"
         ],
         "format": "uuid"
        },
        "status": {
         "$ref": "#/components/schemas/AttendanceStatus"
        },
        "confidence": {
         "type": "number",
         "minimum": 0,
         "maximum": 1,
         "description": "How sure the reading is; below 0.7 the screen highlights the row."
        }
       },
       "required": [
        "roll",
        "status",
        "confidence"
       ]
      }
     },
     "unreadable": {
      "type": "boolean",
      "description": "True when fewer than half the rolls were found; the rows are then whatever was read."
     }
    },
    "required": [
     "rows",
     "unreadable"
    ]
   },
   "MarksPhotoRead": {
    "type": "object",
    "properties": {
     "rows": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "roll": {
         "type": "integer"
        },
        "student_id": {
         "type": [
          "string",
          "null"
         ],
         "format": "uuid"
        },
        "value": {
         "type": [
          "string",
          "null"
         ],
         "description": "The mark as written: digits, or A for absent, or X for exempt; null when the cell was blank or unreadable."
        },
        "confidence": {
         "type": "number",
         "minimum": 0,
         "maximum": 1
        }
       },
       "required": [
        "roll",
        "value",
        "confidence"
       ]
      }
     },
     "unreadable": {
      "type": "boolean"
     }
    },
    "required": [
     "rows",
     "unreadable"
    ]
   },
   "AttendanceStatus": {
    "type": "string",
    "enum": [
     "present",
     "absent",
     "late",
     "excused"
    ]
   },
   "AttendanceRow": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid",
      "maxLength": 40
     },
     "status": {
      "$ref": "#/components/schemas/AttendanceStatus"
     },
     "note": {
      "type": "string",
      "maxLength": 200,
      "description": "Required when status is excused."
     }
    },
    "required": [
     "student_id",
     "status"
    ],
    "additionalProperties": false
   },
   "AttendanceRecord": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "section_id": {
      "type": "string",
      "format": "uuid"
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "period_no": {
      "type": "integer",
      "description": "0 means the whole day."
     },
     "status": {
      "$ref": "#/components/schemas/AttendanceStatus"
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "marked_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "marked_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "student_id",
     "section_id",
     "on_date",
     "status"
    ]
   },
   "AttendanceSummary": {
    "type": "object",
    "properties": {
     "present": {
      "type": "integer"
     },
     "absent": {
      "type": "integer"
     },
     "late": {
      "type": "integer"
     },
     "excused": {
      "type": "integer"
     },
     "total": {
      "type": "integer"
     },
     "pct": {
      "type": [
       "number",
       "null"
      ]
     }
    }
   },
   "Assessment": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "section_subject_id": {
      "type": "string",
      "format": "uuid"
     },
     "subject": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Subject"
       },
       {
        "type": "null"
       }
      ]
     },
     "title": {
      "type": "string",
      "example": "Unit test 1"
     },
     "kind": {
      "type": "string",
      "enum": [
       "quiz",
       "test",
       "assignment",
       "exam",
       "project"
      ]
     },
     "max_marks": {
      "type": "integer"
     },
     "weight": {
      "type": "number"
     },
     "held_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "published_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "exam_paper_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Set on exam columns: teachers enter marks, the office manages and publishes them under Exams."
     }
    },
    "required": [
     "id",
     "title",
     "kind",
     "max_marks"
    ]
   },
   "Mark": {
    "type": "object",
    "properties": {
     "assessment_id": {
      "type": "string",
      "format": "uuid"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "marks": {
      "type": [
       "integer",
       "null"
      ]
     },
     "absent": {
      "type": "boolean"
     },
     "exempt": {
      "type": "boolean",
      "description": "Did not sit the paper and is not counted (since 2026-10-14)."
     },
     "feedback": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "assessment_id",
     "student_id"
    ]
   },
   "TimetableSlot": {
    "type": "object",
    "properties": {
     "section_id": {
      "type": "string",
      "format": "uuid"
     },
     "section_subject_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The subject and teacher assignment in this slot."
     },
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7,
      "description": "1 is Monday."
     },
     "period_no": {
      "type": "integer"
     },
     "subject": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Subject"
       },
       {
        "type": "null"
       }
      ]
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "room": {
      "type": [
       "string",
       "null"
      ]
     },
     "span": {
      "type": "integer",
      "minimum": 0,
      "maximum": 3,
      "description": "Periods the lesson takes: 2 or 3 for a double or triple on its first period, 0 on the periods it continues into."
     },
     "combined_key": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Shared by the sections taught this lesson together."
     },
     "room_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "live_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "maxLength": 300,
      "description": "The lesson's live-class link (Zoom, Google Meet or Jitsi Meet), on a published lesson only, when the office or its teacher set one (HM38). Sent to staff and to the class's own students and families; null otherwise."
     }
    },
    "required": [
     "section_id",
     "weekday",
     "period_no"
    ]
   },
   "Period": {
    "type": "object",
    "properties": {
     "period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24
     },
     "label": {
      "type": "string",
      "example": "P1"
     },
     "starts_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "example": "08:00",
      "maxLength": 5
     },
     "ends_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "example": "08:40",
      "maxLength": 5
     },
     "is_break": {
      "type": "boolean"
     },
     "block_type": {
      "type": "string",
      "enum": [
       "period",
       "break",
       "assembly",
       "lunch",
       "homeroom",
       "advisory",
       "outdoor",
       "movement",
       "cleaning",
       "clubs",
       "team_time",
       "flex",
       "work_cycle"
      ],
      "description": "Only period takes lessons. Sent back; when writing through /admin/periods, is_break decides it."
     },
     "weekday": {
      "type": "integer",
      "minimum": 0,
      "maximum": 7,
      "description": "0 is the normal day; 1 to 7 are a day's own bell times."
     },
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "version_id": {
      "type": "string",
      "format": "uuid"
     }
    },
    "required": [
     "period_no",
     "label",
     "starts_at",
     "ends_at"
    ]
   },
   "PeriodWrite": {
    "type": "object",
    "properties": {
     "period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24
     },
     "label": {
      "type": "string",
      "minLength": 1,
      "maxLength": 20,
      "example": "Period 1"
     },
     "block_type": {
      "type": "string",
      "enum": [
       "period",
       "break",
       "assembly",
       "lunch",
       "homeroom",
       "advisory",
       "outdoor",
       "movement",
       "cleaning",
       "clubs",
       "team_time",
       "flex",
       "work_cycle"
      ]
     },
     "starts_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5,
      "example": "08:00"
     },
     "ends_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5,
      "example": "08:40"
     }
    },
    "required": [
     "period_no",
     "label",
     "block_type",
     "starts_at",
     "ends_at"
    ]
   },
   "DayRule": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "full",
       "short",
       "own",
       "closed"
      ],
      "description": "full: the normal bell times; short: ends after a period; own: that day's own bell times; closed: no school."
     },
     "after_period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24,
      "description": "For a short day."
     }
    },
    "required": [
     "kind"
    ]
   },
   "ShiftVersion": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "status": {
      "type": "string",
      "enum": [
       "planned",
       "published"
      ]
     },
     "effective_from": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "Null while planned."
     },
     "weekdays": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 1,
       "maximum": 7
      }
     },
     "day_rules": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/DayRule"
      },
      "description": "Keyed 1 (Monday) to 7; a day not listed is full."
     },
     "bell_times": {
      "type": "object",
      "additionalProperties": {
       "type": "array",
       "items": {
        "$ref": "#/components/schemas/Period"
       }
      },
      "description": "Keyed by weekday: 0 is the normal day, 1 to 7 a day's own times."
     },
     "lessons": {
      "type": "integer",
      "description": "Published lessons in this version."
     },
     "drafts": {
      "type": "integer",
      "description": "Draft lessons in this version."
     }
    },
    "required": [
     "id",
     "status",
     "effective_from",
     "weekdays",
     "day_rules",
     "bell_times"
    ]
   },
   "ShiftSection": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "label": {
      "type": "string",
      "maxLength": 80
     },
     "kind": {
      "type": "string",
      "enum": [
       "normal",
       "extra"
      ]
     }
    },
    "required": [
     "id",
     "label"
    ]
   },
   "Shift": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "maxLength": 40,
      "example": "Evening shift"
     },
     "colour": {
      "type": "string",
      "pattern": "^#[0-9a-fA-F]{6}$",
      "maxLength": 7,
      "example": "#a855f7"
     },
     "starts_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "ends_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "is_main": {
      "type": "boolean"
     },
     "sort_order": {
      "type": "integer"
     },
     "weekdays": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "integer"
      },
      "description": "The days it runs today."
     },
     "live": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/ShiftVersion"
       },
       {
        "type": "null"
       }
      ],
      "description": "The version live today."
     },
     "planned": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/ShiftVersion"
       },
       {
        "type": "null"
       }
      ],
      "description": "The next version: planned changes, or published changes that start later. Office only; null for others."
     },
     "timings": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Timing"
      }
     },
     "timing_today": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string",
          "maxLength": 40
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The timing in force today."
     },
     "sections": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ShiftSection"
      }
     },
     "moving_in": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ShiftSection"
      }
     }
    },
    "required": [
     "id",
     "name",
     "colour",
     "is_main",
     "live",
     "planned",
     "sections"
    ]
   },
   "ShiftWrite": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 40
     },
     "colour": {
      "type": "string",
      "pattern": "^#[0-9a-fA-F]{6}$",
      "maxLength": 7
     },
     "weekdays": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 1,
       "maximum": 7
      }
     },
     "starts_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "ends_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     }
    },
    "required": [
     "name",
     "weekdays"
    ]
   },
   "ShiftPatch": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 40
     },
     "colour": {
      "type": "string",
      "pattern": "^#[0-9a-fA-F]{6}$",
      "maxLength": 7
     },
     "starts_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "ends_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "sort_order": {
      "type": "integer",
      "minimum": 0,
      "maximum": 100
     },
     "is_main": {
      "type": "boolean",
      "description": "true makes this the main shift. A main shift cannot be unset; make another one main."
     }
    }
   },
   "TimingTime": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 0,
      "maximum": 7,
      "description": "0 is the normal day; 1 to 7 a day with its own bell times."
     },
     "period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24
     },
     "starts_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "ends_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "dropped": {
      "type": "boolean",
      "description": "Left out on these dates."
     }
    },
    "required": [
     "weekday",
     "period_no"
    ]
   },
   "Timing": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "maxLength": 40,
      "example": "Ramadan timing"
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "chosen": {
      "type": "boolean",
      "description": "Wins the timings it overlaps."
     },
     "needs_review": {
      "type": "boolean",
      "description": "It no longer gives every block of the bell times, so it is not used until fixed."
     },
     "overlaps": {
      "type": "boolean"
     },
     "times": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimingTime"
      }
     }
    },
    "required": [
     "id",
     "name",
     "starts_on",
     "ends_on",
     "chosen",
     "needs_review",
     "times"
    ]
   },
   "TimingWrite": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 40
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "chosen": {
      "type": "boolean"
     },
     "times": {
      "type": "array",
      "maxItems": 192,
      "items": {
       "$ref": "#/components/schemas/TimingTime"
      }
     }
    }
   },
   "DayOverride": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "shift_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Null for the whole school."
     },
     "shift_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "kind": {
      "type": "string",
      "enum": [
       "closed",
       "follows"
      ]
     },
     "follows_weekday": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 7
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "on_date",
     "kind"
    ]
   },
   "DayOverrideWrite": {
    "type": "object",
    "properties": {
     "shift_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "kind": {
      "type": "string",
      "enum": [
       "closed",
       "follows"
      ]
     },
     "follows_weekday": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 7
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     }
    },
    "required": [
     "on_date",
     "kind"
    ]
   },
   "TimetableNotice": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "type": "string",
      "enum": [
       "version",
       "timing_start",
       "timing_end",
       "closed",
       "follows",
       "holiday"
      ]
     },
     "shift_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "shift_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "send_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "title": {
      "type": "string",
      "maxLength": 120
     },
     "message": {
      "type": "string",
      "maxLength": 400
     },
     "sent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "recipients": {
      "type": [
       "integer",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "kind",
     "on_date",
     "send_on",
     "title",
     "message"
    ]
   },
   "Room": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "maxLength": 20,
      "example": "Science Lab"
     },
     "capacity": {
      "type": [
       "integer",
       "null"
      ]
     },
     "one_class": {
      "type": "boolean",
      "description": "Takes one class at a time: two classes in it at once is a clash."
     },
     "lessons": {
      "type": "integer",
      "description": "Published lessons in it."
     }
    },
    "required": [
     "id",
     "name",
     "one_class"
    ]
   },
   "RoomWrite": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 20
     },
     "capacity": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 500
     },
     "one_class": {
      "type": "boolean"
     }
    }
   },
   "UnavailableTime": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7
     },
     "starts_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "ends_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     }
    },
    "required": [
     "weekday",
     "starts_at",
     "ends_at"
    ]
   },
   "LessonCover": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "cover",
       "combine",
       "free"
      ]
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ],
      "description": "The cover teacher."
     },
     "for": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120,
      "description": "The absent teacher (staff only)."
     },
     "with": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80,
      "description": "The class it is combined with (staff only)."
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     }
    },
    "required": [
     "kind"
    ]
   },
   "CoverAbsence": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "teacher": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "from_period_no": {
      "type": [
       "integer",
       "null"
      ]
     },
     "to_period_no": {
      "type": [
       "integer",
       "null"
      ]
     },
     "source": {
      "type": "string",
      "enum": [
       "leave",
       "office"
      ]
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     }
    },
    "required": [
     "id",
     "teacher",
     "source"
    ]
   },
   "CoverLesson": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 80
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "period_no": {
      "type": "integer"
     },
     "label": {
      "type": "string",
      "maxLength": 20
     },
     "starts_at": {
      "type": "string",
      "maxLength": 5
     },
     "ends_at": {
      "type": "string",
      "maxLength": 5
     },
     "shift": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "subject": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string",
          "maxLength": 60
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "absent_teacher": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20
     },
     "cover": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LessonCover"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "section",
     "period_no",
     "starts_at",
     "ends_at",
     "absent_teacher"
    ]
   },
   "CoverDay": {
    "type": "object",
    "properties": {
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "daily_limit": {
      "type": "integer"
     },
     "absences": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CoverAbsence"
      }
     },
     "lessons": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CoverLesson"
      }
     },
     "counts": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "teacher": {
         "$ref": "#/components/schemas/PersonRef"
        },
        "today": {
         "type": "integer"
        },
        "month": {
         "type": "integer"
        },
        "year": {
         "type": "integer"
        }
       },
       "required": [
        "teacher",
        "today",
        "month",
        "year"
       ]
      }
     }
    },
    "required": [
     "on_date",
     "daily_limit",
     "absences",
     "lessons",
     "counts"
    ]
   },
   "CoverSuggestion": {
    "type": "object",
    "properties": {
     "teacher": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "same_subject": {
      "type": "boolean"
     },
     "covers_today": {
      "type": "integer"
     },
     "covers_month": {
      "type": "integer"
     },
     "why": {
      "type": "string",
      "maxLength": 200
     }
    },
    "required": [
     "teacher",
     "same_subject",
     "covers_today",
     "covers_month"
    ]
   },
   "CoverSheet": {
    "type": "object",
    "properties": {
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "absent": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 120
      }
     },
     "rows": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "period": {
         "type": [
          "string",
          "null"
         ]
        },
        "starts_at": {
         "type": [
          "string",
          "null"
         ]
        },
        "ends_at": {
         "type": [
          "string",
          "null"
         ]
        },
        "class": {
         "type": [
          "string",
          "null"
         ]
        },
        "subject": {
         "type": [
          "string",
          "null"
         ]
        },
        "absent": {
         "type": [
          "string",
          "null"
         ]
        },
        "room": {
         "type": [
          "string",
          "null"
         ]
        },
        "arrangement": {
         "type": "string",
         "maxLength": 200
        },
        "note": {
         "type": [
          "string",
          "null"
         ]
        }
       },
       "required": [
        "arrangement"
       ]
      }
     }
    },
    "required": [
     "on_date",
     "rows"
    ]
   },
   "MyCover": {
    "type": "object",
    "properties": {
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "period_no": {
      "type": "integer"
     },
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 80
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "for": {
      "type": [
       "string",
       "null"
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "starts_at": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "on_date",
     "period_no",
     "section"
    ]
   },
   "TeacherLoad": {
    "type": "object",
    "properties": {
     "teacher": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "periods": {
      "type": "integer",
      "description": "Periods a week; a double counts twice, a lesson taught to several classes at once counts once."
     },
     "minutes": {
      "type": "integer",
      "description": "Minutes a week; fairer than periods across shifts with different period lengths."
     },
     "free": {
      "type": "integer",
      "description": "Periods without a lesson on the days the teacher works, in their shifts."
     },
     "days": {
      "type": "integer"
     },
     "weekends": {
      "type": "boolean",
      "description": "Teaches on a Saturday or Sunday."
     },
     "shifts": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 40
      }
     },
     "latest_finish": {
      "type": [
       "string",
       "null"
      ],
      "example": "13:20"
     },
     "subjects": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 60
      }
     },
     "covers_month": {
      "type": "integer"
     },
     "covers_year": {
      "type": "integer"
     },
     "weekly_limit": {
      "type": [
       "integer",
       "null"
      ],
      "description": "The school's limit; null until it is set."
     },
     "load": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 5,
      "description": "over (over the limit), near (within 2 of it), under (under 15 periods) or ok; null until the limit is set."
     }
    },
    "required": [
     "teacher",
     "periods",
     "minutes",
     "free",
     "days",
     "weekends",
     "shifts",
     "subjects",
     "covers_month",
     "covers_year",
     "load"
    ]
   },
   "StatsKpi": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "enum": [
       "lessons",
       "teachers",
       "average_load",
       "over_limit",
       "clashes",
       "unpublished",
       "covers_month"
      ]
     },
     "label": {
      "type": "string",
      "maxLength": 60
     },
     "value": {
      "type": [
       "number",
       "null"
      ]
     },
     "opens": {
      "type": "string",
      "enum": [
       "teachers",
       "coverage",
       "clashes",
       "unpublished"
      ],
      "description": "The list behind the number."
     },
     "query": {
      "type": "object",
      "description": "Filters for GET /admin/timetable/teachers when opens is teachers.",
      "additionalProperties": true
     }
    },
    "required": [
     "key",
     "label",
     "value",
     "opens"
    ]
   },
   "TimetableStats": {
    "type": "object",
    "properties": {
     "weekly_limit": {
      "type": [
       "integer",
       "null"
      ]
     },
     "kpis": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/StatsKpi"
      }
     },
     "teacher_load": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "teacher": {
         "$ref": "#/components/schemas/PersonRef"
        },
        "periods": {
         "type": "integer"
        },
        "minutes": {
         "type": "integer"
        },
        "load": {
         "type": [
          "string",
          "null"
         ]
        }
       },
       "required": [
        "teacher",
        "periods",
        "minutes"
       ]
      }
     },
     "free_heatmap": {
      "type": "object",
      "properties": {
       "shift": {
        "oneOf": [
         {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "name": {
            "type": "string",
            "maxLength": 40
           }
          },
          "required": [
           "id",
           "name"
          ]
         },
         {
          "type": "null"
         }
        ]
       },
       "cells": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "weekday": {
           "type": "integer"
          },
          "period_no": {
           "type": "integer"
          },
          "label": {
           "type": [
            "string",
            "null"
           ]
          },
          "starts_at": {
           "type": "string",
           "example": "08:00"
          },
          "free": {
           "type": "integer",
           "description": "Teachers not teaching then and not marked not available."
          }
         },
         "required": [
          "weekday",
          "period_no",
          "starts_at",
          "free"
         ]
        }
       }
      },
      "required": [
       "cells"
      ]
     },
     "coverage": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "section": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          },
          "label": {
           "type": "string",
           "maxLength": 80
          }
         },
         "required": [
          "id",
          "label"
         ]
        },
        "subject": {
         "type": "string",
         "maxLength": 60
        },
        "teacher": {
         "type": [
          "string",
          "null"
         ]
        },
        "planned": {
         "type": "integer",
         "description": "Periods a week in the subject's assignment."
        },
        "placed": {
         "type": "integer",
         "description": "Periods in the live timetable."
        }
       },
       "required": [
        "section",
        "subject",
        "planned",
        "placed"
       ]
      }
     },
     "unpublished": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "label": {
         "type": "string",
         "maxLength": 80
        }
       },
       "required": [
        "id",
        "label"
       ]
      }
     },
     "clashes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "label": {
         "type": "string",
         "maxLength": 80
        },
        "clashes": {
         "type": "integer"
        }
       },
       "required": [
        "id",
        "label",
        "clashes"
       ]
      }
     }
    },
    "required": [
     "kpis",
     "teacher_load",
     "free_heatmap",
     "coverage",
     "unpublished",
     "clashes"
    ]
   },
   "TimetableHistoryEntry": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "published_at": {
      "type": "string",
      "format": "date-time"
     },
     "published_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "lessons": {
      "type": "integer"
     },
     "live": {
      "type": "boolean",
      "description": "The version in use now."
     },
     "slots": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "weekday": {
         "type": "integer"
        },
        "period_no": {
         "type": "integer"
        },
        "subject": {
         "type": [
          "string",
          "null"
         ]
        },
        "teacher": {
         "type": [
          "string",
          "null"
         ]
        },
        "room": {
         "type": [
          "string",
          "null"
         ]
        },
        "span": {
         "type": "integer"
        }
       },
       "required": [
        "weekday",
        "period_no"
       ]
      }
     }
    },
    "required": [
     "id",
     "published_at",
     "lessons",
     "live",
     "slots"
    ]
   },
   "TimetableImportRow": {
    "type": "object",
    "properties": {
     "row": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100000,
      "description": "The row number in the file, for messages."
     },
     "class": {
      "type": "string",
      "maxLength": 60
     },
     "section": {
      "type": "string",
      "maxLength": 40
     },
     "day": {
      "type": "string",
      "description": "Mon, Monday, 1 or پیر.",
      "maxLength": 20
     },
     "period": {
      "type": "string",
      "description": "The period's number or its name in the bell times.",
      "maxLength": 40
     },
     "subject": {
      "type": "string",
      "description": "The subject's name or code.",
      "maxLength": 60
     },
     "teacher": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "length": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2,
      "description": "1, 2 or 3 periods in a row."
     },
     "combined_with": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "Other classes taught together, as Grade 5 B, separated by commas."
     }
    },
    "required": [
     "class",
     "section",
     "day",
     "period",
     "subject"
    ]
   },
   "TimetableImportClash": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 80
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "weekday": {
      "type": "integer"
     },
     "period_no": {
      "type": "integer"
     },
     "kind": {
      "type": "string",
      "maxLength": 20
     },
     "who": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "section",
     "weekday",
     "period_no",
     "kind"
    ]
   },
   "TimetableImportPreview": {
    "type": "object",
    "properties": {
     "rows": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "row": {
         "type": "integer"
        },
        "status": {
         "type": "string",
         "enum": [
          "ok",
          "warning",
          "error"
         ]
        },
        "messages": {
         "type": "array",
         "items": {
          "type": "string",
          "maxLength": 300
         }
        }
       },
       "required": [
        "row",
        "status",
        "messages"
       ]
      }
     },
     "summary": {
      "type": "object",
      "properties": {
       "ok": {
        "type": "integer"
       },
       "warnings": {
        "type": "integer"
       },
       "errors": {
        "type": "integer"
       },
       "sections": {
        "type": "integer",
        "description": "Classes that would get a new draft."
       },
       "lessons": {
        "type": "integer"
       }
      },
      "required": [
       "ok",
       "warnings",
       "errors",
       "sections",
       "lessons"
      ]
     },
     "clashes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimetableImportClash"
      }
     },
     "problem": {
      "type": [
       "string",
       "null"
      ],
      "description": "Why the drafts could not be tried, if they could not."
     }
    },
    "required": [
     "rows",
     "summary",
     "clashes"
    ]
   },
   "TimetableImportResult": {
    "type": "object",
    "properties": {
     "sections": {
      "type": "integer"
     },
     "lessons": {
      "type": "integer"
     },
     "skipped": {
      "type": "integer",
      "description": "Rows with errors left out."
     },
     "clashes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TimetableImportClash"
      }
     }
    },
    "required": [
     "sections",
     "lessons",
     "skipped",
     "clashes"
    ]
   },
   "NowLesson": {
    "type": "object",
    "properties": {
     "starts_at": {
      "type": "string",
      "example": "08:00"
     },
     "ends_at": {
      "type": "string",
      "example": "08:40"
     },
     "subject": {
      "type": [
       "string",
       "null"
      ]
     },
     "teacher": {
      "type": [
       "string",
       "null"
      ],
      "description": "The cover teacher when there is cover."
     },
     "cover": {
      "type": "boolean"
     },
     "room": {
      "type": [
       "string",
       "null"
      ]
     },
     "minutes_left": {
      "type": [
       "integer",
       "null"
      ],
      "description": "For the lesson happening now."
     },
     "period_no": {
      "type": "integer",
      "description": "The lesson's first period (I'm here takes it)."
     },
     "live_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "maxLength": 300,
      "description": "The lesson's live-class link (Zoom, Google Meet or Jitsi Meet), when the office or its teacher set one (HM38). Never on the public board."
     }
    },
    "required": [
     "starts_at",
     "ends_at",
     "cover"
    ]
   },
   "TimetableNow": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 80
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "at": {
      "type": "string",
      "example": "08:50",
      "description": "The school's local time this answer is for."
     },
     "closed": {
      "type": [
       "string",
       "null"
      ],
      "description": "Why the school is closed today, when it is."
     },
     "now": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/NowLesson"
       },
       {
        "type": "null"
       }
      ]
     },
     "next": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/NowLesson"
       },
       {
        "type": "null"
       }
      ]
     },
     "later": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/NowLesson"
      }
     },
     "exam": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "subject": {
         "type": "string",
         "maxLength": 60
        },
        "starts_at": {
         "type": "string"
        },
        "ends_at": {
         "type": "string"
        },
        "room": {
         "type": [
          "string",
          "null"
         ]
        }
       },
       "required": [
        "subject",
        "starts_at",
        "ends_at"
       ]
      }
     },
     "school": {
      "type": [
       "string",
       "null"
      ],
      "description": "On the public board only."
     }
    },
    "required": [
     "section",
     "on_date",
     "at",
     "later",
     "exam"
    ]
   },
   "BoardLink": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "section_id": {
      "type": "string",
      "format": "uuid"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "revoked_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "last_seen_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "active": {
      "type": "boolean"
     },
     "created_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "token": {
      "type": "string",
      "maxLength": 80,
      "description": "Only in the answer that makes the link. Open /board/{token} on the screen."
     }
    },
    "required": [
     "id",
     "section_id",
     "created_at",
     "expires_at",
     "active"
    ]
   },
   "TimetableMaster": {
    "type": "object",
    "properties": {
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "rows_are": {
      "type": "string",
      "enum": [
       "teachers",
       "sections"
      ]
     },
     "columns": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "starts_at": {
         "type": "string"
        },
        "ends_at": {
         "type": "string"
        },
        "label": {
         "type": [
          "string",
          "null"
         ]
        }
       },
       "required": [
        "starts_at",
        "ends_at"
       ]
      }
     },
     "rows": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "label": {
         "type": "string",
         "maxLength": 120
        },
        "away": {
         "type": "boolean"
        },
        "cells": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "starts_at": {
            "type": "string"
           },
           "ends_at": {
            "type": "string"
           },
           "kind": {
            "type": "string",
            "enum": [
             "lesson",
             "cover",
             "away",
             "free"
            ]
           },
           "text": {
            "type": "string",
            "maxLength": 120
           },
           "sub": {
            "type": [
             "string",
             "null"
            ]
           }
          },
          "required": [
           "starts_at",
           "ends_at",
           "kind",
           "text"
          ]
         }
        }
       },
       "required": [
        "id",
        "label",
        "away",
        "cells"
       ]
      }
     }
    },
    "required": [
     "on_date",
     "rows_are",
     "columns",
     "rows"
    ]
   },
   "ClassMember": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": "string",
      "maxLength": 120
     },
     "admission_no": {
      "type": [
       "string",
       "null"
      ]
     },
     "section": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "label": {
          "type": "string",
          "maxLength": 80
         }
        },
        "required": [
         "id",
         "label"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The student's normal section."
     }
    },
    "required": [
     "student_id",
     "full_name"
    ]
   },
   "ExtraClassWrite": {
    "type": "object",
    "properties": {
     "class_id": {
      "type": "string",
      "description": "The class it is mainly for.",
      "format": "uuid"
     },
     "label": {
      "type": "string",
      "minLength": 1,
      "maxLength": 40
     },
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "capacity": {
      "type": "integer",
      "minimum": 1,
      "maximum": 60
     },
     "class_teacher_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     }
    }
   },
   "OptionBlock": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7
     },
     "period_no": {
      "type": "integer"
     },
     "options": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "section_subject_id": {
         "type": "string",
         "format": "uuid"
        },
        "subject": {
         "type": "string",
         "maxLength": 60
        },
        "teacher": {
         "oneOf": [
          {
           "$ref": "#/components/schemas/PersonRef"
          },
          {
           "type": "null"
          }
         ]
        },
        "student_ids": {
         "type": "array",
         "items": {
          "type": "string",
          "format": "uuid"
         }
        }
       },
       "required": [
        "section_subject_id",
        "subject",
        "student_ids"
       ]
      }
     }
    },
    "required": [
     "weekday",
     "period_no",
     "options"
    ]
   },
   "BalanceRuleSetting": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "maxLength": 40
     },
     "label": {
      "type": "string",
      "maxLength": 120
     },
     "unit": {
      "type": "string",
      "enum": [
       "periods",
       "minutes",
       "percent",
       "times",
       "time",
       "days",
       "hours"
      ]
     },
     "on": {
      "type": "boolean"
     },
     "value": {
      "type": [
       "number",
       "string",
       "null"
      ],
      "description": "A number in the rule's unit, or a time such as 13:00."
     },
     "min": {
      "type": [
       "number",
       "null"
      ]
     },
     "max": {
      "type": [
       "number",
       "null"
      ]
     },
     "origin": {
      "type": "string",
      "maxLength": 200,
      "description": "Where the idea comes from."
     },
     "why": {
      "type": "string",
      "maxLength": 200
     }
    },
    "required": [
     "key",
     "label",
     "unit",
     "on",
     "origin"
    ]
   },
   "BalanceResult": {
    "type": "object",
    "properties": {
     "rule": {
      "type": "string",
      "maxLength": 40
     },
     "label": {
      "type": "string",
      "maxLength": 120
     },
     "on": {
      "type": "boolean"
     },
     "value": {
      "type": [
       "number",
       "string",
       "null"
      ]
     },
     "unit": {
      "type": "string",
      "maxLength": 10
     },
     "status": {
      "type": "string",
      "enum": [
       "off",
       "pass",
       "attention",
       "fail"
      ],
      "description": "attention: every offender misses by a little."
     },
     "detail": {
      "type": [
       "string",
       "null"
      ]
     },
     "origin": {
      "type": "string",
      "maxLength": 200
     },
     "why": {
      "type": "string",
      "maxLength": 200
     },
     "offenders": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "label": {
         "type": "string",
         "maxLength": 120
        },
        "detail": {
         "type": "string",
         "maxLength": 200
        }
       },
       "required": [
        "label",
        "detail"
       ]
      }
     }
    },
    "required": [
     "rule",
     "label",
     "on",
     "status",
     "origin",
     "offenders"
    ]
   },
   "TeamTime": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "shift": {
      "type": "string",
      "maxLength": 40
     },
     "weekday": {
      "type": "integer"
     },
     "period_no": {
      "type": "integer"
     },
     "label": {
      "type": "string",
      "maxLength": 60
     },
     "teachers": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/PersonRef"
      }
     }
    },
    "required": [
     "id",
     "shift_id",
     "weekday",
     "period_no",
     "label",
     "teachers"
    ]
   },
   "TeamTimeSlot": {
    "type": "object",
    "properties": {
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "weekday": {
      "type": "integer"
     },
     "period_no": {
      "type": "integer"
     },
     "label": {
      "type": [
       "string",
       "null"
      ]
     },
     "starts_at": {
      "type": "string"
     },
     "ends_at": {
      "type": "string"
     }
    },
    "required": [
     "shift_id",
     "weekday",
     "period_no",
     "starts_at",
     "ends_at"
    ]
   },
   "PlanPublish": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7
     },
     "time": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5,
      "example": "14:00"
     }
    },
    "required": [
     "weekday",
     "time"
    ],
    "description": "When next week's plans publish themselves (if the class teacher wrote the note)."
   },
   "BringRule": {
    "type": "object",
    "properties": {
     "subject_id": {
      "type": "string",
      "format": "uuid"
     },
     "block_type": {
      "type": "string",
      "enum": [
       "period",
       "break",
       "assembly",
       "lunch",
       "homeroom",
       "advisory",
       "outdoor",
       "movement",
       "cleaning",
       "clubs",
       "team_time",
       "flex",
       "work_cycle"
      ]
     },
     "text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80,
      "example": "PE kit"
     }
    },
    "required": [
     "text"
    ],
    "description": "A subject or a block type, and what to bring on a day that has it."
   },
   "WeeklyPlanGoal": {
    "type": "object",
    "properties": {
     "section_subject_id": {
      "type": "string",
      "format": "uuid"
     },
     "subject": {
      "type": "string",
      "maxLength": 60
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "goal_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "goal": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "ticked": {
      "type": "boolean",
      "description": "Families: whether the student ticked it."
     },
     "can_write": {
      "type": "boolean",
      "description": "Staff: whether the caller writes this goal."
     },
     "ticks": {
      "type": "integer",
      "description": "Staff: students who ticked it."
     },
     "ticked_by": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "description": "Their names, for the class teacher, the subject teacher and the office."
     },
     "also_sections": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "label": {
         "type": "string",
         "maxLength": 60
        }
       },
       "required": [
        "id",
        "label"
       ]
      }
     }
    },
    "required": [
     "section_subject_id",
     "subject",
     "goal_id",
     "goal"
    ]
   },
   "WeeklyPlanDay": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "lessons": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "period_no": {
         "type": "integer"
        },
        "starts_at": {
         "type": "string",
         "maxLength": 5
        },
        "ends_at": {
         "type": "string",
         "maxLength": 5
        },
        "subject": {
         "type": "string",
         "maxLength": 60
        },
        "teacher": {
         "type": [
          "string",
          "null"
         ]
        },
        "room": {
         "type": [
          "string",
          "null"
         ]
        },
        "option": {
         "type": "boolean"
        },
        "section": {
         "type": [
          "string",
          "null"
         ],
         "description": "An extra class the student also attends."
        }
       },
       "required": [
        "period_no",
        "starts_at",
        "ends_at",
        "subject",
        "option"
       ]
      }
     },
     "homework_due": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "title": {
         "type": "string",
         "maxLength": 120
        },
        "subject": {
         "type": "string",
         "maxLength": 60
        }
       },
       "required": [
        "id",
        "title",
        "subject"
       ]
      }
     },
     "tests": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "title": {
         "type": "string",
         "maxLength": 80
        },
        "subject": {
         "type": "string",
         "maxLength": 60
        },
        "kind": {
         "type": "string",
         "maxLength": 20
        }
       },
       "required": [
        "title",
        "subject",
        "kind"
       ]
      }
     },
     "bring": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 80
      }
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "The class teacher's line for the day (TT63)."
     },
     "duties": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string",
         "maxLength": 40
        },
        "students": {
         "type": "array",
         "items": {
          "type": "string",
          "maxLength": 120
         }
        }
       },
       "required": [
        "name",
        "students"
       ]
      }
     }
    },
    "required": [
     "weekday",
     "on",
     "lessons",
     "homework_due",
     "tests",
     "bring"
    ],
    "description": "duties: staff see every duty with its students; a family sees only the child's own duties, with no names."
   },
   "WeeklyPlan": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 60
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "week": {
      "type": "string",
      "description": "The Monday of the week.",
      "format": "date",
      "example": "2026-09-01"
     },
     "published": {
      "type": "boolean"
     },
     "published_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "changed": {
      "type": "boolean",
      "description": "Edited after it was published."
     },
     "auto_published": {
      "type": "boolean"
     },
     "class_teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     },
     "bring": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "weekday": {
         "type": "integer"
        },
        "text": {
         "type": "string",
         "maxLength": 80
        }
       },
       "required": [
        "weekday",
        "text"
       ]
      }
     },
     "day_notes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "weekday": {
         "type": "integer"
        },
        "text": {
         "type": "string",
         "maxLength": 300
        }
       },
       "required": [
        "weekday",
        "text"
       ]
      },
      "description": "The class teacher's line for each day: what the class will do."
     },
     "goals": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WeeklyPlanGoal"
      }
     },
     "days": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WeeklyPlanDay"
      }
     },
     "can_lead": {
      "type": "boolean",
      "description": "Staff: the caller writes the note and publishes."
     },
     "students": {
      "type": "integer"
     },
     "publish_at": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PlanPublish"
       },
       {
        "type": "null"
       }
      ]
     },
     "sections_written": {
      "type": "integer"
     },
     "copied": {
      "type": "integer"
     }
    },
    "required": [
     "section",
     "week",
     "published"
    ],
    "description": "A family's read of a plan not published yet has only section, week and published."
   },
   "FlexOffering": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "period_no": {
      "type": "integer"
     },
     "title": {
      "type": "string",
      "maxLength": 60
     },
     "is_default": {
      "type": "boolean",
      "description": "The block's study hall: it takes everyone who does not choose."
     },
     "seats": {
      "type": "integer"
     },
     "taken": {
      "type": "integer"
     },
     "seats_left": {
      "type": "integer"
     },
     "requests": {
      "type": "integer"
     },
     "waiting": {
      "type": "integer"
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "room": {
      "type": [
       "string",
       "null"
      ]
     },
     "room_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "mine": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "on",
     "shift_id",
     "period_no",
     "title",
     "is_default",
     "seats",
     "taken",
     "seats_left"
    ]
   },
   "FlexOfferingWrite": {
    "type": "object",
    "properties": {
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24
     },
     "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 60
     },
     "room_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "seats": {
      "type": "integer",
      "minimum": 1,
      "maximum": 300
     },
     "is_default": {
      "type": "boolean"
     },
     "teacher_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The office only; a teacher's own sessions are theirs."
     }
    }
   },
   "FlexDay": {
    "type": "object",
    "properties": {
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "closed": {
      "type": "boolean",
      "description": "The cut-off has passed: choices and changes are closed."
     },
     "cutoff": {
      "type": "string",
      "maxLength": 5
     },
     "blocks": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "shift_id": {
         "type": "string",
         "format": "uuid"
        },
        "shift": {
         "type": "string",
         "maxLength": 40
        },
        "period_no": {
         "type": "integer"
        },
        "label": {
         "type": [
          "string",
          "null"
         ]
        },
        "starts_at": {
         "type": "string",
         "maxLength": 5
        },
        "ends_at": {
         "type": "string",
         "maxLength": 5
        },
        "students": {
         "type": "integer"
        },
        "unchosen": {
         "type": "integer",
         "description": "Students who have not chosen (they go to the study hall)."
        },
        "offerings": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/FlexOffering"
         }
        }
       },
       "required": [
        "shift_id",
        "shift",
        "period_no",
        "starts_at",
        "ends_at",
        "students",
        "unchosen",
        "offerings"
       ]
      }
     }
    },
    "required": [
     "on",
     "closed",
     "cutoff",
     "blocks"
    ]
   },
   "FlexRoster": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "period_no": {
      "type": "integer"
     },
     "title": {
      "type": "string",
      "maxLength": 60
     },
     "is_default": {
      "type": "boolean"
     },
     "seats": {
      "type": "integer"
     },
     "taken": {
      "type": "integer"
     },
     "seats_left": {
      "type": "integer"
     },
     "requests": {
      "type": "integer"
     },
     "waiting": {
      "type": "integer"
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "room": {
      "type": [
       "string",
       "null"
      ]
     },
     "room_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "mine": {
      "type": "boolean"
     },
     "students": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string",
         "maxLength": 120
        },
        "section": {
         "type": "string",
         "maxLength": 60
        },
        "kind": {
         "type": "string",
         "enum": [
          "chosen",
          "requested",
          "default"
         ]
        },
        "status": {
         "type": [
          "string",
          "null"
         ],
         "enum": [
          "present",
          "absent",
          "late",
          null
         ]
        }
       },
       "required": [
        "id",
        "full_name",
        "section",
        "kind"
       ]
      }
     },
     "block_students": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string",
         "maxLength": 120
        },
        "section": {
         "type": "string",
         "maxLength": 60
        }
       },
       "required": [
        "id",
        "full_name",
        "section"
       ]
      },
      "description": "Everyone in the block, for requesting; the session's teacher and the office."
     }
    },
    "required": [
     "id",
     "title",
     "students"
    ]
   },
   "FlexWhere": {
    "type": "object",
    "properties": {
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       }
      },
      "required": [
       "id",
       "full_name"
      ]
     },
     "section": {
      "type": "string",
      "maxLength": 60
     },
     "offering_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "title": {
      "type": [
       "string",
       "null"
      ]
     },
     "room": {
      "type": [
       "string",
       "null"
      ]
     },
     "kind": {
      "type": [
       "string",
       "null"
      ]
     },
     "status": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "student",
     "section"
    ]
   },
   "StudentFlex": {
    "type": "object",
    "properties": {
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "closed": {
      "type": "boolean"
     },
     "cutoff": {
      "type": "string",
      "maxLength": 5
     },
     "blocks": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "shift_id": {
         "type": "string",
         "format": "uuid"
        },
        "period_no": {
         "type": "integer"
        },
        "label": {
         "type": [
          "string",
          "null"
         ]
        },
        "starts_at": {
         "type": "string",
         "maxLength": 5
        },
        "ends_at": {
         "type": "string",
         "maxLength": 5
        },
        "place": {
         "oneOf": [
          {
           "type": "object",
           "properties": {
            "offering_id": {
             "type": "string",
             "format": "uuid"
            },
            "kind": {
             "type": "string",
             "enum": [
              "chosen",
              "requested",
              "default"
             ]
            }
           },
           "required": [
            "offering_id",
            "kind"
           ]
          },
          {
           "type": "null"
          }
         ]
        },
        "waiting": {
         "type": "array",
         "items": {
          "type": "string",
          "format": "uuid"
         }
        },
        "offerings": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "title": {
            "type": "string",
            "maxLength": 60
           },
           "is_default": {
            "type": "boolean"
           },
           "room": {
            "type": [
             "string",
             "null"
            ]
           },
           "teacher": {
            "type": [
             "string",
             "null"
            ]
           },
           "seats_left": {
            "type": "integer"
           }
          },
          "required": [
           "id",
           "title",
           "is_default",
           "seats_left"
          ]
         }
        }
       },
       "required": [
        "shift_id",
        "period_no",
        "starts_at",
        "ends_at",
        "waiting",
        "offerings"
       ]
      }
     }
    },
    "required": [
     "on",
     "closed",
     "cutoff",
     "blocks"
    ]
   },
   "CycleTime": {
    "type": "object",
    "properties": {
     "starts_at": {
      "type": "string",
      "maxLength": 5
     },
     "ends_at": {
      "type": "string",
      "maxLength": 5
     }
    },
    "required": [
     "starts_at",
     "ends_at"
    ]
   },
   "WorkCycleSections": {
    "type": "object",
    "properties": {
     "today": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "sections": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "label": {
         "type": "string",
         "maxLength": 60
        },
        "can_write": {
         "type": "boolean",
         "description": "The caller plans this class's small-group lessons."
        },
        "work_cycles_today": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/CycleTime"
         }
        },
        "students": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "full_name": {
            "type": "string",
            "maxLength": 120
           }
          },
          "required": [
           "id",
           "full_name"
          ]
         }
        }
       },
       "required": [
        "id",
        "label",
        "can_write",
        "work_cycles_today",
        "students"
       ]
      }
     }
    },
    "required": [
     "today",
     "sections"
    ]
   },
   "SmallGroupLesson": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "section_id": {
      "type": "string",
      "format": "uuid"
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "starts_at": {
      "type": "string",
      "maxLength": 5
     },
     "title": {
      "type": "string",
      "maxLength": 80
     },
     "material": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "place": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "students": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string",
         "maxLength": 120
        }
       },
       "required": [
        "id",
        "full_name"
       ]
      }
     }
    },
    "required": [
     "id",
     "section_id",
     "on",
     "starts_at",
     "title"
    ],
    "description": "A family's read has no students (no other children's names)."
   },
   "SmallGroupWrite": {
    "type": "object",
    "properties": {
     "section_id": {
      "type": "string",
      "format": "uuid"
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "starts_at": {
      "type": "string",
      "description": "Inside a work cycle of that day.",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
     },
     "material": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "place": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "student_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "minItems": 1,
      "maxItems": 12
     }
    }
   },
   "WorkPlanItem": {
    "type": "object",
    "properties": {
     "subject_id": {
      "type": "string",
      "format": "uuid"
     },
     "subject": {
      "type": "string",
      "maxLength": 60
     },
     "kind": {
      "type": "string",
      "enum": [
       "must",
       "may"
      ]
     },
     "target": {
      "type": [
       "integer",
       "null"
      ],
      "description": "How many times (must; optional for may)."
     },
     "done": {
      "type": "integer",
      "description": "Journal entries for the subject that week."
     }
    },
    "required": [
     "subject_id",
     "subject",
     "kind",
     "target",
     "done"
    ]
   },
   "JournalEntry": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "started_at": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 5
     },
     "minutes": {
      "type": "integer"
     },
     "subject_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "subject": {
      "type": [
       "string",
       "null"
      ]
     },
     "note": {
      "type": "string",
      "description": "The work.",
      "maxLength": 120
     },
     "by_student": {
      "type": "boolean"
     },
     "added_by": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "on",
     "minutes",
     "note",
     "by_student"
    ]
   },
   "JournalWrite": {
    "type": "object",
    "properties": {
     "on": {
      "type": "string",
      "description": "Staff only: the day (today or before).",
      "format": "date",
      "example": "2026-09-01"
     },
     "started_at": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "minutes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 240
     },
     "subject_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "note": {
      "type": "string",
      "description": "The work the child chose.",
      "minLength": 1,
      "maxLength": 120
     }
    },
    "required": [
     "minutes",
     "note"
    ]
   },
   "WorkPlan": {
    "type": "object",
    "properties": {
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       }
      },
      "required": [
       "id",
       "full_name"
      ]
     },
     "section": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "label": {
          "type": "string",
          "maxLength": 60
         }
        },
        "required": [
         "id",
         "label"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "week": {
      "type": "string",
      "description": "The Monday.",
      "format": "date",
      "example": "2026-09-01"
     },
     "montessori": {
      "type": "boolean",
      "description": "The child's class has a work cycle."
     },
     "work_cycles_today": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CycleTime"
      }
     },
     "journal_open": {
      "type": "boolean",
      "description": "The caller is the child and a work cycle is on now."
     },
     "items": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WorkPlanItem"
      }
     },
     "journal": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/JournalEntry"
      }
     },
     "small_groups": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SmallGroupLesson"
      }
     },
     "subjects": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "name": {
         "type": "string",
         "maxLength": 60
        }
       },
       "required": [
        "id",
        "name"
       ]
      }
     },
     "can_write": {
      "type": "boolean",
      "description": "Staff: the caller writes the plan and the journal."
     }
    },
    "required": [
     "student",
     "week",
     "montessori",
     "work_cycles_today",
     "journal_open",
     "items",
     "journal",
     "small_groups",
     "subjects"
    ]
   },
   "ClassDuty": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "maxLength": 40
     },
     "per_day": {
      "type": "integer",
      "minimum": 1,
      "maximum": 4
     }
    },
    "required": [
     "id",
     "name",
     "per_day"
    ]
   },
   "DutyRota": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 60
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "duties": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ClassDuty"
      }
     },
     "students": {
      "type": "integer"
     },
     "can_lead": {
      "type": "boolean"
     },
     "rota_until": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "school_days": {
      "type": "integer",
      "description": "After making a rota: the school days it covers."
     },
     "days": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "on": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "weekday": {
         "type": "integer"
        },
        "duties": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "duty_id": {
            "type": "string",
            "format": "uuid"
           },
           "name": {
            "type": "string",
            "maxLength": 40
           },
           "students": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "id": {
               "type": "string",
               "format": "uuid"
              },
              "full_name": {
               "type": "string",
               "maxLength": 120
              }
             },
             "required": [
              "id",
              "full_name"
             ]
            }
           }
          },
          "required": [
           "duty_id",
           "name",
           "students"
          ]
         }
        }
       },
       "required": [
        "on",
        "weekday",
        "duties"
       ]
      }
     }
    },
    "required": [
     "section",
     "duties",
     "students",
     "can_lead",
     "days"
    ],
    "description": "On the day, an absent student's duty is shown with the next name in school."
   },
   "WeeklyPlanSummary": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 60
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "week": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "class_teacher": {
      "type": "boolean",
      "description": "The caller is its class teacher."
     },
     "published": {
      "type": "boolean"
     },
     "changed": {
      "type": "boolean"
     },
     "has_note": {
      "type": "boolean"
     },
     "goals": {
      "type": "integer"
     },
     "subjects": {
      "type": "integer"
     },
     "checked": {
      "type": "boolean",
      "description": "The principal or the school admin has marked it checked (TT74)."
     },
     "comments": {
      "type": "integer",
      "description": "Review comments on it."
     }
    },
    "required": [
     "section",
     "week",
     "class_teacher",
     "published",
     "changed",
     "has_note",
     "goals",
     "subjects"
    ]
   },
   "WeeklyPlanComment": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "author": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "body": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000,
      "description": "Null for a check with no words."
     },
     "checked": {
      "type": "boolean",
      "description": "This comment marked the plan checked."
     },
     "at": {
      "type": "string",
      "format": "date-time"
     },
     "mine": {
      "type": "boolean"
     },
     "can_remove": {
      "type": "boolean",
      "description": "Its writer, the principal or the school admin."
     }
    },
    "required": [
     "id",
     "author",
     "body",
     "checked",
     "at",
     "mine",
     "can_remove"
    ]
   },
   "WeeklyPlanReview": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 60
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "week": {
      "type": "string",
      "description": "The Monday of the week.",
      "format": "date",
      "example": "2026-09-01"
     },
     "checked": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "by": {
          "oneOf": [
           {
            "$ref": "#/components/schemas/PersonRef"
           },
           {
            "type": "null"
           }
          ]
         },
         "at": {
          "type": "string",
          "format": "date-time"
         },
         "changed_since": {
          "type": "boolean",
          "description": "The plan was edited after the check."
         }
        },
        "required": [
         "by",
         "at",
         "changed_since"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "can_check": {
      "type": "boolean",
      "description": "The caller may mark the plan checked (the principal or the school admin)."
     },
     "comments": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WeeklyPlanComment"
      }
     }
    },
    "required": [
     "section",
     "week",
     "checked",
     "can_check",
     "comments"
    ],
    "description": "Staff only: families never see the review (TT74)."
   },
   "ShapeBlock": {
    "type": "object",
    "properties": {
     "label": {
      "type": "string",
      "description": "Empty for a period: it becomes Period 1, Period 2 and so on.",
      "maxLength": 20
     },
     "block_type": {
      "type": "string",
      "enum": [
       "period",
       "break",
       "assembly",
       "lunch",
       "homeroom",
       "advisory",
       "outdoor",
       "movement",
       "cleaning",
       "clubs",
       "team_time",
       "flex",
       "work_cycle"
      ]
     },
     "minutes": {
      "type": "integer",
      "minimum": 5,
      "maximum": 240
     }
    },
    "required": [
     "block_type",
     "minutes"
    ]
   },
   "ShapeDayRule": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "full",
       "short",
       "closed"
      ],
      "description": "short: the day ends after a period; closed: no school."
     },
     "after_period": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24,
      "description": "For a short day: the day ends after this teaching period (the 5th period, not block number 5)."
     }
    },
    "required": [
     "kind"
    ]
   },
   "Shape": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "maxLength": 40
     },
     "name": {
      "type": "string",
      "maxLength": 60
     },
     "origin": {
      "type": "string",
      "maxLength": 200,
      "description": "Where the shape comes from."
     },
     "sentence": {
      "type": "string",
      "maxLength": 300
     },
     "start": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5
     },
     "weekdays": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 1,
       "maximum": 7
      }
     },
     "day_rules": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/ShapeDayRule"
      }
     },
     "blocks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ShapeBlock"
      }
     }
    },
    "required": [
     "key",
     "name",
     "origin",
     "sentence",
     "start",
     "weekdays",
     "day_rules",
     "blocks"
    ]
   },
   "ShapeWrite": {
    "type": "object",
    "properties": {
     "shape": {
      "type": "string",
      "enum": [
       "pakistan_standard",
       "nordic_rhythm",
       "japanese_day",
       "montessori",
       "california_block",
       "blank"
      ],
      "description": "A starting shape from GET /admin/shifts/shapes. Its days, rules and blocks are used for anything not given."
     },
     "start": {
      "type": "string",
      "description": "When the first block starts. Default: the shape's own start.",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "maxLength": 5,
      "example": "08:00"
     },
     "weekdays": {
      "type": "array",
      "minItems": 1,
      "maxItems": 7,
      "items": {
       "type": "integer",
       "minimum": 1,
       "maximum": 7
      }
     },
     "day_rules": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/ShapeDayRule"
      },
      "description": "Keyed 1 (Monday) to 7; a day not listed is full."
     },
     "blocks": {
      "type": "array",
      "minItems": 1,
      "maxItems": 24,
      "items": {
       "$ref": "#/components/schemas/ShapeBlock"
      }
     }
    },
    "description": "A shape, or your own blocks, from a start time. Give shape, or blocks, or both (blocks then replace the shape's)."
   },
   "ShapeStats": {
    "type": "object",
    "properties": {
     "school_days": {
      "type": "integer"
     },
     "day": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "description": "The first block's start and the last block's end, on the day with the most periods."
     },
     "periods_a_week": {
      "type": "integer"
     },
     "period_minutes": {
      "type": [
       "number",
       "null"
      ]
     },
     "learning_minutes_a_week": {
      "type": "number"
     },
     "breaks_minutes": {
      "type": "number"
     },
     "movement_minutes": {
      "type": "number"
     },
     "longest_stretch_minutes": {
      "type": "number"
     },
     "most_in_a_row": {
      "type": "integer"
     },
     "team_minutes_a_week": {
      "type": "number"
     }
    },
    "required": [
     "school_days",
     "periods_a_week",
     "learning_minutes_a_week",
     "most_in_a_row"
    ]
   },
   "ShapePreview": {
    "type": "object",
    "properties": {
     "current": {
      "$ref": "#/components/schemas/ShapeStats"
     },
     "proposed": {
      "$ref": "#/components/schemas/ShapeStats"
     },
     "blocks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/PeriodWrite"
      }
     },
     "day_rules": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/DayRule"
      }
     },
     "weekdays_change": {
      "type": "object",
      "properties": {
       "from": {
        "type": "array",
        "items": {
         "type": "integer"
        }
       },
       "to": {
        "type": "array",
        "items": {
         "type": "integer"
        }
       }
      },
      "required": [
       "from",
       "to"
      ]
     },
     "sections": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "section_id": {
         "type": "string",
         "format": "uuid"
        },
        "label": {
         "type": "string",
         "maxLength": 60
        },
        "planned": {
         "type": "integer",
         "description": "Periods a week its subjects ask for."
        },
        "placed": {
         "type": "integer",
         "description": "Periods it has now."
        },
        "fits": {
         "type": "integer",
         "description": "Of those, the periods that keep a place in the shape."
        },
        "room": {
         "type": "integer",
         "description": "Periods a week in the shape."
        },
        "subjects_to_trim": {
         "type": "array",
         "items": {
          "type": "string",
          "maxLength": 60
         }
        }
       },
       "required": [
        "section_id",
        "label",
        "planned",
        "placed",
        "fits",
        "room",
        "subjects_to_trim"
       ]
      }
     },
     "balance": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "rule": {
         "type": "string",
         "maxLength": 40
        },
        "label": {
         "type": "string",
         "maxLength": 120
        },
        "on": {
         "type": "boolean"
        },
        "value": {
         "type": [
          "number",
          "string",
          "null"
         ]
        },
        "unit": {
         "type": "string",
         "maxLength": 10
        },
        "status": {
         "type": "string",
         "enum": [
          "off",
          "pass",
          "attention",
          "fail"
         ],
         "description": "attention: every offender misses by a little."
        },
        "detail": {
         "type": [
          "string",
          "null"
         ]
        },
        "origin": {
         "type": "string",
         "maxLength": 200
        },
        "why": {
         "type": "string",
         "maxLength": 200
        },
        "offenders": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "label": {
            "type": "string",
            "maxLength": 120
           },
           "detail": {
            "type": "string",
            "maxLength": 200
           }
          },
          "required": [
           "label",
           "detail"
          ]
         }
        },
        "status_now": {
         "type": [
          "string",
          "null"
         ],
         "description": "The rule's status on the live timetable."
        }
       },
       "required": [
        "rule",
        "label",
        "on",
        "status",
        "origin",
        "offenders"
       ]
      }
     },
     "teacher_load": {
      "type": "object",
      "properties": {
       "average_now": {
        "type": [
         "number",
         "null"
        ]
       },
       "average_after": {
        "type": [
         "number",
         "null"
        ]
       }
      },
      "required": [
       "average_now",
       "average_after"
      ]
     }
    },
    "required": [
     "current",
     "proposed",
     "blocks",
     "day_rules",
     "weekdays_change",
     "sections",
     "balance",
     "teacher_load"
    ]
   },
   "TimetableSettings": {
    "type": "object",
    "properties": {
     "cover_daily_limit": {
      "type": "integer",
      "minimum": 0,
      "maximum": 8,
      "description": "Covers a teacher may take a day."
     },
     "weekly_limit": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 10,
      "maximum": 60
     },
     "balance_rules": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/BalanceRuleSetting"
      }
     },
     "hard_subject_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "movement_subject_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "young_class_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "teen_class_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "plan_publish": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PlanPublish"
       },
       {
        "type": "null"
       }
      ]
     },
     "bring_rules": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/BringRule"
      }
     },
     "flex_cutoff": {
      "type": "string",
      "description": "When Flex choices close on the day.",
      "maxLength": 5
     }
    },
    "required": [
     "cover_daily_limit"
    ]
   },
   "TimetableClash": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer"
     },
     "period_no": {
      "type": "integer"
     },
     "kind": {
      "type": "string",
      "enum": [
       "teacher",
       "room",
       "student",
       "unavailable"
      ],
      "description": "unavailable: the teacher is not at the school then (other_section_id is null)."
     },
     "who": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "teacher_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "other_section_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "other_period_no": {
      "type": [
       "integer",
       "null"
      ]
     },
     "first_date": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "The first date both lessons happen at overlapping times."
     }
    },
    "required": [
     "weekday",
     "period_no"
    ]
   },
   "TimetableVersionRef": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "status": {
      "type": "string",
      "enum": [
       "planned",
       "published"
      ]
     },
     "effective_from": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "live": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "status",
     "live"
    ]
   },
   "ChildShift": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "maxLength": 40
     },
     "colour": {
      "type": "string",
      "maxLength": 7
     },
     "weekdays": {
      "type": "array",
      "items": {
       "type": "integer"
      }
     },
     "day_rules": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/DayRule"
      }
     },
     "today": {
      "type": "object",
      "properties": {
       "on_date": {
        "type": "string",
        "format": "date",
        "example": "2026-09-01"
       },
       "blocks": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "period_no": {
           "type": "integer"
          },
          "label": {
           "type": "string",
           "maxLength": 20
          },
          "block_type": {
           "type": "string",
           "maxLength": 20
          },
          "starts_at": {
           "type": "string",
           "maxLength": 5
          },
          "ends_at": {
           "type": "string",
           "maxLength": 5
          }
         },
         "required": [
          "period_no",
          "label",
          "starts_at",
          "ends_at"
         ]
        }
       }
      },
      "required": [
       "on_date",
       "blocks"
      ]
     },
     "timing_today": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string",
          "maxLength": 40
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "closures": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "on_date": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "reason": {
         "type": [
          "string",
          "null"
         ],
         "maxLength": 80
        },
        "follows": {
         "type": [
          "integer",
          "null"
         ],
         "description": "The weekday whose timetable this day follows."
        }
       },
       "required": [
        "on_date"
       ]
      }
     },
     "next_change": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "effective_from": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01"
         }
        },
        "required": [
         "effective_from"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "When the shift's bell times next change."
     }
    },
    "required": [
     "id",
     "name",
     "today"
    ]
   },
   "Announcement": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "created_by": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "user_id of the author."
     },
     "author": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ],
      "description": "Who posted it; null for a key or a removed account."
     },
     "title": {
      "type": "string"
     },
     "body": {
      "type": "string"
     },
     "audience_roles": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Role"
      }
     },
     "section_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "publish_at": {
      "type": "string",
      "format": "date-time"
     },
     "expires_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "pinned": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "title",
     "body",
     "publish_at"
    ]
   },
   "AnnouncementWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "body": {
      "type": "string",
      "minLength": 10,
      "maxLength": 4000
     },
     "audience_roles": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Role"
      },
      "maxItems": 500
     },
     "section_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "maxItems": 500
     },
     "publish_at": {
      "type": "string",
      "format": "date-time",
      "maxLength": 40
     },
     "expires_at": {
      "type": "string",
      "format": "date-time",
      "maxLength": 40
     },
     "pinned": {
      "type": "boolean"
     }
    },
    "required": [
     "title",
     "body"
    ],
    "additionalProperties": false
   },
   "CalendarEvent": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "kind": {
      "type": "string",
      "enum": [
       "holiday",
       "exam",
       "ptm",
       "event",
       "deadline"
      ]
     },
     "starts_at": {
      "type": "string",
      "format": "date-time"
     },
     "ends_at": {
      "type": "string",
      "format": "date-time"
     },
     "all_day": {
      "type": "boolean"
     },
     "description": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "title",
     "kind",
     "starts_at",
     "ends_at"
    ]
   },
   "FeeHead": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "Tuition"
     },
     "code": {
      "type": "string",
      "example": "TUITION",
      "pattern": "^[A-Z0-9_]{2,12}$"
     },
     "kind": {
      "type": "string",
      "enum": [
       "recurring",
       "one_time"
      ]
     },
     "is_active": {
      "type": "boolean"
     },
     "sort_order": {
      "type": "integer"
     }
    },
    "required": [
     "id",
     "name",
     "code",
     "kind"
    ]
   },
   "FeeStructureLine": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "academic_year_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "null means every class; a class-specific line overrides it."
     },
     "fee_head_id": {
      "type": "string",
      "format": "uuid"
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "frequency": {
      "type": "string",
      "enum": [
       "monthly",
       "term",
       "annual",
       "one_time"
      ]
     }
    },
    "required": [
     "academic_year_id",
     "fee_head_id",
     "amount",
     "frequency"
    ]
   },
   "FeeSettings": {
    "type": "object",
    "properties": {
     "currency": {
      "type": "string",
      "example": "PKR"
     },
     "due_day": {
      "type": "integer",
      "minimum": 1,
      "maximum": 28
     },
     "late_fee_amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "late_fee_after_days": {
      "type": "integer",
      "minimum": 0,
      "maximum": 60
     },
     "second_approval_percent": {
      "type": "number",
      "minimum": 0,
      "maximum": 100
     },
     "refund_approval_amount": {
      "type": "string",
      "description": "Refunds above this need a second office person. 0 means every refund does.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     }
    }
   },
   "FamilyAccount": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "Ahmed Khan"
     },
     "primary_guardian": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "students": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string"
        },
        "admission_no": {
         "type": [
          "string",
          "null"
         ]
        },
        "status": {
         "type": "string",
         "enum": [
          "active",
          "inactive",
          "graduated",
          "withdrawn"
         ]
        },
        "class_name": {
         "type": [
          "string",
          "null"
         ]
        },
        "section_name": {
         "type": [
          "string",
          "null"
         ]
        }
       }
      }
     },
     "outstanding": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "is_locked": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "name"
    ]
   },
   "InvoiceStatus": {
    "type": "string",
    "enum": [
     "issued",
     "part_paid",
     "paid",
     "void"
    ],
    "description": "Overdue is not a status; it is derived: issued or part_paid and past due_date."
   },
   "InvoiceItem": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "fee_head_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "description": {
      "type": "string",
      "example": "Tuition"
     },
     "amount": {
      "type": "string",
      "description": "Negative for discounts.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "is_late_fee": {
      "type": "boolean"
     },
     "adjustment_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     }
    },
    "required": [
     "description",
     "amount"
    ]
   },
   "Invoice": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "invoice_no": {
      "type": "string",
      "example": "INV-2026-000123"
     },
     "student": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": "string"
         },
         "admission_no": {
          "type": [
           "string",
           "null"
          ]
         },
         "class_name": {
          "type": [
           "string",
           "null"
          ]
         },
         "section_name": {
          "type": [
           "string",
           "null"
          ]
         }
        }
       },
       {
        "type": "null"
       }
      ],
      "description": "Null when the student record is not visible to the caller (a student seeing a sibling's invoice on the family account)."
     },
     "family_account_id": {
      "type": "string",
      "format": "uuid"
     },
     "family_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "family_phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "period_start": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "period_end": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "issue_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "due_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "status": {
      "$ref": "#/components/schemas/InvoiceStatus"
     },
     "is_overdue": {
      "type": "boolean"
     },
     "currency": {
      "type": "string",
      "example": "PKR"
     },
     "subtotal": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "discount_total": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "late_fee": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "total": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "paid_amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "balance": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "items": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/InvoiceItem"
      }
     },
     "notes": {
      "type": [
       "string",
       "null"
      ]
     },
     "void_reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "voided_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "is_locked": {
      "type": "boolean"
     },
     "on_hold": {
      "type": "boolean"
     },
     "payments": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "payment_id": {
         "type": "string",
         "format": "uuid"
        },
        "receipt_no": {
         "type": "string"
        },
        "paid_on": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "method": {
         "$ref": "#/components/schemas/PaymentMethod"
        },
        "amount": {
         "type": "string",
         "description": "Amount with two decimals, as a string.",
         "pattern": "^-?\\d+\\.\\d{2}$",
         "example": "12500.00"
        },
        "status": {
         "type": "string",
         "enum": [
          "confirmed",
          "reversed"
         ]
        }
       }
      }
     }
    },
    "required": [
     "id",
     "invoice_no",
     "student",
     "family_account_id",
     "period_start",
     "due_date",
     "status",
     "total",
     "paid_amount",
     "balance"
    ]
   },
   "PaymentMethod": {
    "type": "string",
    "enum": [
     "cash",
     "bank_transfer",
     "cheque",
     "wallet"
    ]
   },
   "Payment": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "receipt_no": {
      "type": "string",
      "example": "RCP-2026-000045"
     },
     "family_account_id": {
      "type": "string",
      "format": "uuid"
     },
     "family_name": {
      "type": "string"
     },
     "family_phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "currency": {
      "type": "string"
     },
     "method": {
      "$ref": "#/components/schemas/PaymentMethod"
     },
     "paid_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "reference": {
      "type": [
       "string",
       "null"
      ]
     },
     "notes": {
      "type": [
       "string",
       "null"
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "confirmed",
       "reversed"
      ]
     },
     "reversed_reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "reversed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "received_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "proof_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "allocations": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "invoice_id": {
         "type": "string",
         "format": "uuid"
        },
        "invoice_no": {
         "type": "string"
        },
        "student_name": {
         "type": [
          "string",
          "null"
         ]
        },
        "period_start": {
         "type": [
          "string",
          "null"
         ],
         "format": "date"
        },
        "amount": {
         "type": "string",
         "description": "Amount with two decimals, as a string.",
         "pattern": "^-?\\d+\\.\\d{2}$",
         "example": "12500.00"
        }
       }
      }
     },
     "unallocated": {
      "type": "string",
      "description": "Part of the payment not applied to any invoice and not refunded (credit).",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "refunded": {
      "type": "string",
      "description": "Approved refunds taken out of this payment.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "refunds": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "refund_no": {
         "type": [
          "string",
          "null"
         ],
         "description": "Issued when the refund is approved."
        },
        "amount": {
         "type": "string",
         "description": "Amount with two decimals, as a string.",
         "pattern": "^-?\\d+\\.\\d{2}$",
         "example": "12500.00"
        },
        "method": {
         "$ref": "#/components/schemas/PaymentMethod"
        },
        "status": {
         "type": "string",
         "enum": [
          "pending",
          "approved",
          "rejected"
         ]
        },
        "refunded_on": {
         "type": [
          "string",
          "null"
         ],
         "format": "date"
        }
       },
       "required": [
        "id",
        "amount",
        "status"
       ]
      }
     }
    },
    "required": [
     "id",
     "receipt_no",
     "family_account_id",
     "amount",
     "method",
     "paid_on",
     "status"
    ]
   },
   "PaymentWrite": {
    "type": "object",
    "properties": {
     "family_account_id": {
      "type": "string",
      "format": "uuid",
      "maxLength": 40
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string; never negative.",
      "pattern": "^\\d+\\.\\d{2}$",
      "example": "12500.00",
      "maxLength": 80
     },
     "method": {
      "$ref": "#/components/schemas/PaymentMethod"
     },
     "paid_on": {
      "type": "string",
      "description": "Today or earlier.",
      "format": "date",
      "example": "2026-09-01",
      "maxLength": 40
     },
     "reference": {
      "type": "string",
      "description": "Cheque, transfer or wallet reference, kept on the receipt. On POST /school/payments an external_ref, when sent, is stored as the reference instead and this field is ignored.",
      "maxLength": 80
     },
     "notes": {
      "type": "string",
      "maxLength": 300
     },
     "invoice_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "description": "Restrict allocation to these invoices. Omit to allocate oldest due first across the family's open invoices. Empty list keeps the whole amount unallocated.",
      "maxItems": 500
     }
    },
    "required": [
     "family_account_id",
     "amount",
     "method",
     "paid_on"
    ],
    "additionalProperties": false
   },
   "Adjustment": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "student_name": {
      "type": "string"
     },
     "student_admission_no": {
      "type": [
       "string",
       "null"
      ]
     },
     "class_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "section_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "fee_head_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "null means every recurring head."
     },
     "fee_head_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "kind": {
      "type": "string",
      "enum": [
       "discount",
       "scholarship",
       "waiver",
       "sibling"
      ]
     },
     "percent": {
      "type": [
       "number",
       "null"
      ]
     },
     "amount": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^\\d+\\.\\d{2}$"
     },
     "valid_from": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "valid_to": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "reason": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "awaiting_second",
       "approved",
       "rejected"
      ]
     },
     "requested_by": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "approved_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "second_approved_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "decision_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "student_id",
     "kind",
     "valid_from",
     "reason",
     "status"
    ]
   },
   "AdjustmentWrite": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "fee_head_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "kind": {
      "type": "string",
      "enum": [
       "discount",
       "scholarship",
       "waiver",
       "sibling"
      ]
     },
     "percent": {
      "type": "number",
      "exclusiveMinimum": 0,
      "maximum": 100
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "valid_from": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "valid_to": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "reason": {
      "type": "string",
      "minLength": 5,
      "maxLength": 300
     }
    },
    "required": [
     "student_id",
     "kind",
     "valid_from",
     "reason"
    ],
    "description": "Exactly one of percent or amount."
   },
   "Proof": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "family_account_id": {
      "type": "string",
      "format": "uuid"
     },
     "family_name": {
      "type": "string"
     },
     "family_phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "student_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "students": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string"
        }
       },
       "required": [
        "id",
        "full_name"
       ]
      }
     },
     "fee_month": {
      "type": "string",
      "description": "First day of the month.",
      "format": "date",
      "example": "2026-09-01"
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "method": {
      "$ref": "#/components/schemas/PaymentMethod"
     },
     "reference": {
      "type": [
       "string",
       "null"
      ]
     },
     "file_url": {
      "type": [
       "string",
       "null"
      ],
      "description": "Short-lived signed URL (5 minutes) or null."
     },
     "status": {
      "type": "string",
      "enum": [
       "waiting",
       "matched",
       "returned",
       "rejected"
      ]
     },
     "return_reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "duplicate_of": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "matched_payment_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "submitted_by": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "submitted_at": {
      "type": "string",
      "format": "date-time"
     },
     "reviewed_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "reviewed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "kind": {
      "type": "string",
      "enum": [
       "slip",
       "claim"
      ],
      "description": "claim: the parent said \"I have paid\" on one invoice (POST /me/invoices/{id}/paid), with or without a slip attached. slip: a proof sent by month (POST /me/proofs), with or without a file, tied to no invoice."
     },
     "invoice_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The invoice the parent answered: set for a claim, null for a slip."
     },
     "invoice_no": {
      "type": [
       "string",
       "null"
      ]
     },
     "paid_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "The day the parent says they paid, for a claim."
     }
    },
    "required": [
     "id",
     "family_account_id",
     "student_ids",
     "fee_month",
     "amount",
     "method",
     "status",
     "submitted_at",
     "kind"
    ]
   },
   "ProofWrite": {
    "type": "object",
    "properties": {
     "student_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "minItems": 1,
      "maxItems": 8
     },
     "fee_month": {
      "type": "string",
      "description": "Any day; normalised to the first of the month.",
      "format": "date",
      "example": "2026-09-01"
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string; never negative.",
      "pattern": "^\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "method": {
      "type": "string",
      "enum": [
       "bank_transfer",
       "wallet",
       "cheque",
       "cash"
      ]
     },
     "reference": {
      "type": "string",
      "maxLength": 80
     },
     "upload_id": {
      "type": "string",
      "description": "From POST /me/proofs/upload-url. Optional since 2026-10-14: without it the proof is a claim with no file."
     }
    },
    "required": [
     "student_ids",
     "fee_month",
     "amount",
     "method"
    ]
   },
   "ClaimWrite": {
    "type": "object",
    "properties": {
     "paid_on": {
      "type": "string",
      "description": "The day the parent paid; not in the future.",
      "format": "date",
      "example": "2026-09-01"
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string; never negative.",
      "pattern": "^\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "method": {
      "type": "string",
      "enum": [
       "bank_transfer",
       "wallet",
       "cheque",
       "cash"
      ]
     },
     "reference": {
      "type": "string",
      "description": "The transaction or cheque number; required for a transfer or a cheque unless a slip is attached.",
      "maxLength": 80
     },
     "upload_id": {
      "type": "string",
      "description": "Optional: a slip uploaded through POST /me/proofs/upload-url."
     }
    },
    "required": [
     "paid_on",
     "amount",
     "method"
    ],
    "description": "A parent's answer to a fee reminder: I have paid."
   },
   "Flag": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "entity_type": {
      "type": "string",
      "enum": [
       "invoice",
       "payment",
       "proof"
      ]
     },
     "entity_id": {
      "type": "string",
      "format": "uuid"
     },
     "category": {
      "type": "string",
      "enum": [
       "dispute",
       "amount_discrepancy",
       "missing_document",
       "duplicate",
       "other"
      ]
     },
     "priority": {
      "type": "string",
      "enum": [
       "low",
       "normal",
       "high"
      ]
     },
     "reason": {
      "type": "string"
     },
     "raised_by": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "is_resolved": {
      "type": "boolean"
     },
     "resolved_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "resolved_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "resolution": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "entity_type",
     "entity_id",
     "category",
     "reason",
     "is_resolved"
    ]
   },
   "Hold": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "entity_type": {
      "type": "string",
      "enum": [
       "invoice",
       "payment",
       "family_account"
      ]
     },
     "entity_id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "type": "string",
      "enum": [
       "audit",
       "legal"
      ]
     },
     "reason": {
      "type": "string"
     },
     "placed_by": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "placed_at": {
      "type": "string",
      "format": "date-time"
     },
     "released_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "released_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "entity_type",
     "entity_id",
     "kind",
     "reason"
    ]
   },
   "ReminderRule": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "days_overdue": {
      "type": "integer",
      "minimum": 0,
      "maximum": 180
     },
     "channel": {
      "type": "string",
      "enum": [
       "in_app",
       "email",
       "whatsapp"
      ],
      "description": "in_app or email. whatsapp is reserved and not sent yet: a rule with it is refused (INVALID_ENUM) until WhatsApp reminders ship."
     },
     "template": {
      "type": "string",
      "description": "Placeholders: {student}, {invoice}, {amount}, {due_date}.",
      "minLength": 10,
      "maxLength": 600
     },
     "is_active": {
      "type": "boolean"
     }
    },
    "required": [
     "days_overdue",
     "channel",
     "template"
    ]
   },
   "DuesRow": {
    "type": "object",
    "properties": {
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_name": {
      "type": "string"
     },
     "students_due": {
      "type": "integer"
     },
     "outstanding": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "overdue_0_30": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "overdue_31_60": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "overdue_61_plus": {
      "type": "string",
      "description": "Amount with two decimals, as a string.",
      "pattern": "^-?\\d+\\.\\d{2}$",
      "example": "12500.00"
     }
    }
   },
   "ChangeLogEntry": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Opaque, increasing."
     },
     "entity_type": {
      "type": "string",
      "example": "invoice"
     },
     "entity_id": {
      "type": "string",
      "format": "uuid"
     },
     "operation": {
      "type": "string",
      "enum": [
       "INSERT",
       "UPDATE",
       "DELETE"
      ]
     },
     "summary": {
      "type": [
       "string",
       "null"
      ],
      "example": "Changed notes on invoice INV-2026-000123"
     },
     "old": {
      "type": [
       "object",
       "null"
      ]
     },
     "new": {
      "type": [
       "object",
       "null"
      ]
     },
     "actor": {
      "type": [
       "string",
       "null"
      ],
      "description": "Name of the person who made the change."
     },
     "acting_as": {
      "type": [
       "string",
       "null"
      ],
      "description": "Name of the person being viewed as, when the change was made during View-as."
     },
     "changed_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "entity_type",
     "entity_id",
     "operation",
     "summary",
     "changed_at"
    ]
   },
   "ApiKey": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "Bank feed importer"
     },
     "prefix": {
      "type": "string",
      "description": "First 12 characters, for identification.",
      "example": "sk_school_9f3a"
     },
     "scopes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Scope"
      }
     },
     "role": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Role"
       },
       {
        "type": "null"
       }
      ]
     },
     "created_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_used_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "revoked_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "expires_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "After this the key stops working. Null: until revoked."
     },
     "notify_email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "description": "Platform keys: the person copied when this key marks a report fixed or tells a school about a change."
     }
    },
    "required": [
     "id",
     "name",
     "prefix",
     "scopes",
     "role"
    ]
   },
   "ApiKeyCreated": {
    "allOf": [
     {
      "$ref": "#/components/schemas/ApiKey"
     },
     {
      "type": "object",
      "properties": {
       "secret": {
        "type": "string",
        "description": "The full key. Shown once, never stored in clear.",
        "example": "sk_school_9f3a..."
       }
      },
      "required": [
       "secret"
      ]
     }
    ]
   },
   "Scope": {
    "type": "string",
    "enum": [
     "read:students",
     "write:students",
     "read:attendance",
     "write:attendance",
     "read:grades",
     "read:timetable",
     "read:fees",
     "write:invoices",
     "write:payments",
     "read:proofs",
     "write:proofs",
     "read:people",
     "write:announcements",
     "read:audit",
     "webhooks",
     "write:people",
     "write:structure",
     "write:imports",
     "write:admissions",
     "write:fees",
     "read:quran",
     "read:learn_summary",
     "write:charity_links",
     "read:issues",
     "write:issues",
     "write:school_updates",
     "read:learn",
     "write:learn",
     "partner:links",
     "partner:practice",
     "partner:recitations",
     "write:roles",
     "write:schools"
    ]
   },
   "WebhookEndpoint": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "url": {
      "type": "string",
      "format": "uri",
      "example": "https://erp.example.pk/samas/hook"
     },
     "events": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WebhookEvent"
      }
     },
     "is_active": {
      "type": "boolean"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_delivery_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "failing_since": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "url",
     "events",
     "is_active"
    ]
   },
   "WebhookEvent": {
    "type": "string",
    "enum": [
     "invoice.issued",
     "invoice.voided",
     "payment.recorded",
     "payment.reversed",
     "proof.submitted",
     "proof.matched",
     "student.enrolled",
     "student.updated",
     "attendance.saved",
     "lesson.assigned",
     "lesson.graded",
     "juz.tested"
    ]
   },
   "HomeworkFile": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Keep a file on save by sending {id}."
     },
     "name": {
      "type": "string",
      "example": "worksheet.pdf"
     },
     "content_type": {
      "type": "string"
     },
     "size_bytes": {
      "type": "integer"
     },
     "url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "Download link, valid 10 minutes."
     }
    },
    "required": [
     "id",
     "name",
     "content_type",
     "size_bytes"
    ]
   },
   "HomeworkFileRef": {
    "oneOf": [
     {
      "type": "object",
      "properties": {
       "upload_id": {
        "type": "string",
        "maxLength": 64,
        "description": "From the upload-url call."
       }
      },
      "required": [
       "upload_id"
      ]
     },
     {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "maxLength": 64,
        "description": "A file already attached, to keep it."
       }
      },
      "required": [
       "id"
      ]
     }
    ]
   },
   "HomeworkUpload": {
    "type": "object",
    "properties": {
     "file_name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "content_type": {
      "type": "string",
      "enum": [
       "image/jpeg",
       "image/png",
       "image/webp",
       "application/pdf",
       "application/msword",
       "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
       "application/vnd.ms-excel",
       "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
       "application/vnd.ms-powerpoint",
       "application/vnd.openxmlformats-officedocument.presentationml.presentation"
      ]
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 20971520
     }
    },
    "required": [
     "file_name",
     "content_type",
     "size_bytes"
    ]
   },
   "Assignment": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "section_subject_id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "instructions": {
      "type": [
       "string",
       "null"
      ]
     },
     "due_at": {
      "type": "string",
      "format": "date-time"
     },
     "max_marks": {
      "type": "integer"
     },
     "allow_late": {
      "type": "boolean"
     },
     "counts_to_grade": {
      "type": "boolean",
      "description": "Returned marks become a gradebook column."
     },
     "attachments": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "set_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Null while a draft (only the teacher sees it)."
     },
     "grades_returned_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "class_size": {
      "type": "integer"
     },
     "submitted": {
      "type": "integer"
     },
     "late": {
      "type": "integer"
     },
     "graded": {
      "type": "integer"
     },
     "average_pct": {
      "type": [
       "number",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "section_subject_id",
     "title",
     "due_at",
     "max_marks",
     "allow_late",
     "counts_to_grade",
     "attachments"
    ]
   },
   "AssignmentWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "instructions": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 4000
     },
     "due_at": {
      "type": "string",
      "description": "In the future, within a year.",
      "format": "date-time"
     },
     "max_marks": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
     },
     "allow_late": {
      "type": "boolean"
     },
     "counts_to_grade": {
      "type": "boolean"
     },
     "attachments": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     },
     "set": {
      "type": "boolean",
      "description": "true sets it for the class (families are told); leave out to keep a draft."
     }
    }
   },
   "HomeworkSubmission": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "body": {
      "type": [
       "string",
       "null"
      ]
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "submitted_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Null for work marked without an online hand-in."
     },
     "is_late": {
      "type": "boolean"
     },
     "marks": {
      "type": [
       "integer",
       "null"
      ]
     },
     "feedback": {
      "type": [
       "string",
       "null"
      ]
     },
     "graded_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "student_id",
     "files",
     "is_late"
    ]
   },
   "AssignmentDetail": {
    "allOf": [
     {
      "$ref": "#/components/schemas/Assignment"
     },
     {
      "type": "object",
      "properties": {
       "roster": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "student": {
           "type": "object",
           "properties": {
            "id": {
             "type": "string",
             "format": "uuid"
            },
            "full_name": {
             "type": "string"
            },
            "admission_no": {
             "type": [
              "string",
              "null"
             ]
            }
           },
           "required": [
            "id",
            "full_name"
           ]
          },
          "submission": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/HomeworkSubmission"
            },
            {
             "type": "null"
            }
           ]
          }
         },
         "required": [
          "student",
          "submission"
         ]
        }
       }
      },
      "required": [
       "roster"
      ]
     }
    ]
   },
   "ChildHomework": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "instructions": {
      "type": [
       "string",
       "null"
      ]
     },
     "due_at": {
      "type": "string",
      "format": "date-time"
     },
     "max_marks": {
      "type": "integer"
     },
     "allow_late": {
      "type": "boolean"
     },
     "attachments": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "subject": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "due",
       "overdue",
       "handed_in",
       "late",
       "marked",
       "missing"
      ],
      "description": "due: not handed in yet; overdue: past due, late work still taken; missing: past due and closed; handed_in or late: waiting for marks; marked: marks returned."
     },
     "submission": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/HomeworkSubmission"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "id",
     "title",
     "due_at",
     "max_marks",
     "status",
     "subject",
     "attachments"
    ]
   },
   "Insight": {
    "type": "object",
    "properties": {
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string"
       },
       "admission_no": {
        "type": [
         "string",
         "null"
        ]
       },
       "class_name": {
        "type": "string"
       },
       "section_name": {
        "type": "string"
       },
       "section_id": {
        "type": "string",
        "format": "uuid"
       }
      },
      "required": [
       "id",
       "full_name"
      ]
     },
     "average_pct": {
      "type": [
       "number",
       "null"
      ]
     },
     "marked": {
      "type": "integer",
      "description": "Pieces of marked work behind the average."
     },
     "attendance_pct": {
      "type": [
       "number",
       "null"
      ]
     },
     "attendance_days": {
      "type": "integer"
     },
     "missing_assignments": {
      "type": "integer"
     },
     "reasons": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "low_average",
        "low_attendance",
        "missing_work"
       ]
      }
     }
    },
    "required": [
     "student",
     "reasons",
     "missing_assignments"
    ]
   },
   "ExamTerm": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string"
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "instructions": {
      "type": [
       "string",
       "null"
      ]
     },
     "scheduled_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When the date sheet went out to families."
     },
     "papers": {
      "type": "integer"
     },
     "classes_published": {
      "type": "integer"
     }
    },
    "required": [
     "id",
     "name",
     "starts_on",
     "ends_on"
    ]
   },
   "ExamTermWrite": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 3,
      "maxLength": 80
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": "string",
      "description": "At most 60 days after the first day.",
      "format": "date",
      "example": "2026-09-01"
     },
     "instructions": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     }
    }
   },
   "ExamPaper": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "class": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "subject": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "starts_at": {
      "type": "string",
      "example": "09:00"
     },
     "duration_min": {
      "type": "integer"
     },
     "room": {
      "type": [
       "string",
       "null"
      ]
     },
     "invigilator": {
      "type": [
       "string",
       "null"
      ]
     },
     "max_marks": {
      "type": "integer"
     }
    },
    "required": [
     "id",
     "class",
     "subject",
     "on_date",
     "starts_at",
     "duration_min",
     "max_marks"
    ]
   },
   "ExamPaperWrite": {
    "type": "object",
    "properties": {
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "subject_id": {
      "type": "string",
      "format": "uuid"
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "starts_at": {
      "type": "string",
      "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
      "example": "09:00"
     },
     "duration_min": {
      "type": "integer",
      "minimum": 15,
      "maximum": 300
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "invigilator": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "max_marks": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
     }
    }
   },
   "ExamClassStatus": {
    "type": "object",
    "properties": {
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_name": {
      "type": "string"
     },
     "papers": {
      "type": "integer"
     },
     "expected": {
      "type": "integer",
      "description": "Marks expected: students times papers."
     },
     "entered": {
      "type": "integer"
     },
     "published_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "published_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "class_id",
     "class_name",
     "papers",
     "expected",
     "entered"
    ]
   },
   "ExamTermDetail": {
    "allOf": [
     {
      "$ref": "#/components/schemas/ExamTerm"
     },
     {
      "type": "object",
      "properties": {
       "papers_list": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/ExamPaper"
        }
       },
       "classes": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/ExamClassStatus"
        }
       }
      },
      "required": [
       "papers_list",
       "classes"
      ]
     }
    ]
   },
   "ChildExams": {
    "type": "object",
    "properties": {
     "term": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       },
       "starts_on": {
        "type": "string",
        "format": "date",
        "example": "2026-09-01"
       },
       "ends_on": {
        "type": "string",
        "format": "date",
        "example": "2026-09-01"
       },
       "instructions": {
        "type": [
         "string",
         "null"
        ]
       }
      },
      "required": [
       "id",
       "name",
       "starts_on",
       "ends_on"
      ]
     },
     "published_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Results for the child's class; marks are null until then."
     },
     "papers": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "subject": {
         "type": "string"
        },
        "on_date": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "starts_at": {
         "type": "string"
        },
        "duration_min": {
         "type": "integer"
        },
        "room": {
         "type": [
          "string",
          "null"
         ]
        },
        "max_marks": {
         "type": "integer"
        },
        "marks": {
         "type": [
          "integer",
          "null"
         ]
        },
        "absent": {
         "type": [
          "boolean",
          "null"
         ]
        }
       },
       "required": [
        "id",
        "subject",
        "on_date",
        "starts_at",
        "duration_min",
        "max_marks"
       ]
      }
     }
    },
    "required": [
     "term",
     "papers"
    ]
   },
   "DesignStage": {
    "type": "string",
    "enum": [
     "proposed",
     "approved",
     "in_development",
     "in_testing",
     "live",
     "parked",
     "wont_do"
    ],
    "description": "Where a design stands: proposed, approved, in development, in testing, live in the app, parked, or won't do."
   },
   "Design": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "example": "Finance-Reports",
      "description": "The mockup's id."
     },
     "title": {
      "type": "string"
     },
     "prd": {
      "type": [
       "string",
       "null"
      ],
      "description": "PRD story, e.g. F10."
     },
     "area": {
      "type": [
       "string",
       "null"
      ],
      "example": "finance"
     },
     "stage": {
      "$ref": "#/components/schemas/DesignStage"
     },
     "app_route": {
      "type": [
       "string",
       "null"
      ],
      "example": "/finance/reports",
      "description": "Where it lives in the app once built."
     },
     "developer": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "target_date": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "notes": {
      "type": [
       "string",
       "null"
      ]
     },
     "sort_order": {
      "type": "integer"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "open_comments": {
      "type": "integer"
     },
     "comments": {
      "type": "integer"
     },
     "has_spec": {
      "type": "boolean",
      "description": "A PRD and TDD have been generated (super admins can read them)."
     }
    },
    "required": [
     "id",
     "title",
     "stage",
     "open_comments",
     "comments"
    ]
   },
   "DesignSpec": {
    "type": "object",
    "properties": {
     "design_id": {
      "type": "string"
     },
     "prd_md": {
      "type": "string",
      "description": "Markdown."
     },
     "tdd_md": {
      "type": "string",
      "description": "Markdown."
     },
     "rule_count": {
      "type": "integer",
      "description": "How many rules and checks the two documents list."
     },
     "generated_at": {
      "type": "string",
      "format": "date-time"
     },
     "source": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "design_id",
     "prd_md",
     "tdd_md",
     "rule_count",
     "generated_at"
    ]
   },
   "DesignWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 160
     },
     "prd": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "area": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "stage": {
      "$ref": "#/components/schemas/DesignStage"
     },
     "app_route": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "pattern": "^/"
     },
     "developer_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "A saMas staff member or super admin."
     },
     "target_date": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     },
     "note": {
      "type": "string",
      "maxLength": 300,
      "description": "Why the stage changed; kept in the history."
     }
    }
   },
   "DesignStageChange": {
    "type": "object",
    "properties": {
     "from_stage": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/DesignStage"
       },
       {
        "type": "null"
       }
      ]
     },
     "to_stage": {
      "$ref": "#/components/schemas/DesignStage"
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "changed_at": {
      "type": "string",
      "format": "date-time"
     },
     "changed_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "to_stage",
     "changed_at"
    ]
   },
   "DesignComment": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "design_id": {
      "type": "string"
     },
     "prd_id": {
      "type": [
       "string",
       "null"
      ]
     },
     "kind": {
      "type": "string",
      "enum": [
       "comment",
       "suggestion"
      ]
     },
     "body": {
      "type": "string"
     },
     "x_pct": {
      "type": [
       "number",
       "null"
      ]
     },
     "y_pct": {
      "type": [
       "number",
       "null"
      ]
     },
     "parent_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "status": {
      "type": "string",
      "enum": [
       "open",
       "resolved"
      ]
     },
     "author_id": {
      "type": "string"
     },
     "author_name": {
      "type": "string"
     },
     "author_email": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "design_id",
     "kind",
     "body",
     "status",
     "author_id",
     "author_name",
     "created_at"
    ]
   },
   "WebhookDelivery": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "endpoint_id": {
      "type": "string",
      "format": "uuid"
     },
     "event": {
      "type": "string",
      "enum": [
       "invoice.issued",
       "invoice.voided",
       "payment.recorded",
       "payment.reversed",
       "proof.submitted",
       "proof.matched",
       "student.enrolled",
       "student.updated",
       "attendance.saved",
       "lesson.assigned",
       "lesson.graded",
       "juz.tested",
       "link.approved",
       "link.revoked",
       "recitation.reviewed",
       "live.session.started",
       "live.session.ended",
       "ping"
      ],
      "description": "A WebhookEvent, or an event only a connected Quran reading app's endpoint receives (link.approved, link.revoked, recitation.reviewed, live.session.started, live.session.ended), or ping."
     },
     "payload": {
      "type": "object"
     },
     "attempts": {
      "type": "integer"
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "delivered",
       "failed",
       "skipped_unlinked"
      ],
      "description": "skipped_unlinked: a delivery to a Quran reading app dropped because the link ended before it was sent."
     },
     "last_status_code": {
      "type": [
       "integer",
       "null"
      ]
     },
     "next_attempt_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "Notification": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "type": "string",
      "example": "fee_reminder"
     },
     "title": {
      "type": "string"
     },
     "body": {
      "type": "string"
     },
     "data": {
      "type": "object"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "read_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "kind",
     "title",
     "body",
     "created_at"
    ]
   },
   "AddOn": {
    "type": "object",
    "properties": {
     "add_on": {
      "type": "string",
      "enum": [
       "madrasa",
       "learn",
       "charity",
       "live_sabaq",
       "toefl",
       "flashcards"
      ]
     },
     "state": {
      "type": "string",
      "enum": [
       "off",
       "trial",
       "on",
       "ended"
      ]
     },
     "active": {
      "type": "boolean"
     },
     "trial_ends_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "started_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "ended_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "add_on",
     "state",
     "active"
    ]
   },
   "QuranSurah": {
    "type": "object",
    "properties": {
     "surah": {
      "type": "integer",
      "minimum": 1,
      "maximum": 114
     },
     "name_arabic": {
      "type": "string",
      "maxLength": 40
     },
     "name_english": {
      "type": "string",
      "maxLength": 40
     },
     "ayah_count": {
      "type": "integer"
     },
     "revelation": {
      "type": "string",
      "enum": [
       "makki",
       "madani"
      ]
     },
     "juz_start": {
      "type": "integer"
     },
     "juz_end": {
      "type": "integer"
     },
     "juz_boundaries": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "juz": {
         "type": "integer"
        },
        "ayah_from": {
         "type": "integer"
        }
       },
       "required": [
        "juz",
        "ayah_from"
       ]
      }
     }
    },
    "required": [
     "surah",
     "name_arabic",
     "name_english",
     "ayah_count",
     "revelation",
     "juz_start",
     "juz_end",
     "juz_boundaries"
    ]
   },
   "QuranAyah": {
    "type": "object",
    "properties": {
     "ayah": {
      "type": "integer"
     },
     "text": {
      "type": "string",
      "description": "The Uthmani text (Hafs, Kufan count)."
     },
     "words": {
      "type": "array",
      "items": {
       "type": "string"
      }
     }
    },
    "required": [
     "ayah",
     "text",
     "words"
    ],
    "description": "A word is its 0-based position after splitting the text on spaces, the same rule as the Quran reading app."
   },
   "QuranProgram": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "enum": [
       "qaida",
       "nazra",
       "hifz",
       "tajweed",
       "qiraat"
      ]
     },
     "name": {
      "type": "string",
      "maxLength": 20
     },
     "parts": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "kinds": {
      "type": "array",
      "items": {
       "type": "string"
      }
     }
    },
    "required": [
     "key",
     "name",
     "parts",
     "kinds"
    ]
   },
   "QuranEnrollment": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "program": {
      "type": "string",
      "enum": [
       "qaida",
       "nazra",
       "hifz",
       "tajweed",
       "qiraat"
      ]
     },
     "riwayah": {
      "type": "string",
      "maxLength": 20,
      "example": "hafs_asim"
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "started_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ended_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "ended_reason": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "student_id",
     "program",
     "riwayah",
     "started_on"
    ]
   },
   "QuranLessonPart": {
    "type": "object",
    "properties": {
     "lesson_id": {
      "type": "string",
      "format": "uuid"
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "part": {
      "type": "string",
      "enum": [
       "sabaq",
       "sabqi",
       "manzil"
      ]
     },
     "kind": {
      "type": "string",
      "enum": [
       "range",
       "juz",
       "qaida"
      ]
     },
     "surah": {
      "type": "integer"
     },
     "ayah_from": {
      "type": "integer"
     },
     "ayah_to": {
      "type": "integer"
     },
     "juz_from": {
      "type": "integer"
     },
     "juz_to": {
      "type": "integer"
     },
     "qaida_lesson": {
      "type": "integer"
     },
     "riwayah": {
      "type": "string",
      "maxLength": 20
     },
     "result": {
      "type": "string",
      "enum": [
       "pending",
       "passed",
       "repeat",
       "absent"
      ]
     },
     "grade": {
      "type": "string",
      "enum": [
       "mumtaz",
       "jayyid_jiddan",
       "jayyid",
       "maqbul",
       "repeat"
      ]
     },
     "remark": {
      "type": "string",
      "maxLength": 200
     },
     "graded_at": {
      "type": "string",
      "format": "date-time"
     },
     "due_date": {
      "type": "string",
      "description": "Set when the part is homework the Quran reading app shows.",
      "format": "date",
      "example": "2026-09-01"
     },
     "assignment_status": {
      "type": "string",
      "enum": [
       "assigned",
       "withdrawn",
       "submitted",
       "graded"
      ]
     }
    },
    "required": [
     "lesson_id",
     "on",
     "part",
     "kind",
     "result"
    ],
    "description": "Fields that do not apply to the kind are left out."
   },
   "QuranDay": {
    "type": "object",
    "properties": {
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "parts": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/QuranLessonPart"
      }
     }
    },
    "required": [
     "on",
     "parts"
    ]
   },
   "QuranLessonPartInput": {
    "type": "object",
    "properties": {
     "part": {
      "type": "string",
      "enum": [
       "sabaq",
       "sabqi",
       "manzil"
      ]
     },
     "kind": {
      "type": "string",
      "enum": [
       "range",
       "juz",
       "qaida"
      ],
      "description": "Worked out from the fields when left out."
     },
     "surah": {
      "type": "integer",
      "minimum": 1,
      "maximum": 114
     },
     "ayah_from": {
      "type": "integer",
      "minimum": 1,
      "maximum": 286
     },
     "ayah_to": {
      "type": "integer",
      "minimum": 1,
      "maximum": 286
     },
     "juz_from": {
      "type": "integer",
      "minimum": 1,
      "maximum": 30
     },
     "juz_to": {
      "type": "integer",
      "minimum": 1,
      "maximum": 30
     },
     "qaida_lesson": {
      "type": "integer",
      "minimum": 1,
      "maximum": 60
     },
     "due_date": {
      "type": "string",
      "description": "Makes the part homework for the Quran reading app (lesson.assigned).",
      "format": "date",
      "example": "2026-09-01"
     }
    },
    "required": [
     "part"
    ]
   },
   "JuzState": {
    "type": "object",
    "properties": {
     "juz": {
      "type": "integer",
      "minimum": 1,
      "maximum": 30
     },
     "state": {
      "type": "string",
      "enum": [
       "not_started",
       "in_progress",
       "memorized",
       "tested"
      ]
     },
     "riwayah": {
      "type": "string",
      "maxLength": 20
     },
     "memorized_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "tested_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "test_grade": {
      "type": "string",
      "enum": [
       "mumtaz",
       "jayyid_jiddan",
       "jayyid",
       "maqbul"
      ]
     }
    },
    "required": [
     "juz",
     "state"
    ]
   },
   "TajweedNote": {
    "type": "object",
    "properties": {
     "note_id": {
      "type": "string",
      "format": "uuid"
     },
     "lesson_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "recitation_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "surah": {
      "type": "integer"
     },
     "ayah": {
      "type": "integer"
     },
     "word_index": {
      "type": "integer"
     },
     "word": {
      "type": [
       "string",
       "null"
      ]
     },
     "category": {
      "type": "string",
      "enum": [
       "makharij",
       "madd",
       "ghunnah",
       "qalqalah",
       "waqf",
       "other"
      ]
     },
     "text": {
      "type": "string",
      "maxLength": 200
     },
     "written_at": {
      "type": "string",
      "format": "date-time"
     },
     "written_by": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "at_seconds": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Where in the recording the note sits, for notes written while reviewing a recitation."
     }
    },
    "required": [
     "note_id",
     "surah",
     "ayah",
     "word_index",
     "category",
     "text",
     "written_at"
    ]
   },
   "TajweedCounts": {
    "type": "object",
    "additionalProperties": {
     "type": "integer"
    },
    "description": "Notes per category: makharij, madd, ghunnah, qalqalah, waqf, other."
   },
   "QuranAttentionRow": {
    "type": "object",
    "properties": {
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       },
       "first_name": {
        "type": "string",
        "maxLength": 60
       }
      },
      "required": [
       "id",
       "full_name",
       "first_name"
      ]
     },
     "halaqa": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "label": {
          "type": "string",
          "maxLength": 60
         }
        },
        "required": [
         "id",
         "label"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "rule": {
      "type": "string",
      "enum": [
       "manzil_overdue",
       "ready_for_test",
       "repeat_streak",
       "practice_dropped"
      ]
     },
     "since": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "detail": {
      "type": "object",
      "additionalProperties": true
     }
    },
    "required": [
     "student",
     "rule",
     "since",
     "detail"
    ]
   },
   "QuranStudent": {
    "type": "object",
    "properties": {
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "first_name": {
        "type": "string",
        "maxLength": 60
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       },
       "admission_no": {
        "type": "string",
        "maxLength": 40
       },
       "halaqa": {
        "oneOf": [
         {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "label": {
            "type": "string",
            "maxLength": 60
           }
          },
          "required": [
           "id",
           "label"
          ]
         },
         {
          "type": "null"
         }
        ]
       }
      },
      "required": [
       "id",
       "first_name",
       "full_name"
      ]
     },
     "enrollment": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/QuranEnrollment"
       },
       {
        "type": "null"
       }
      ]
     },
     "today": {
      "$ref": "#/components/schemas/QuranDay"
     },
     "week": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/QuranDay"
      }
     },
     "juz_map": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/JuzState"
      }
     },
     "tajweed": {
      "type": "object",
      "properties": {
       "days_30": {
        "$ref": "#/components/schemas/TajweedCounts"
       },
       "days_90": {
        "$ref": "#/components/schemas/TajweedCounts"
       },
       "recent": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/TajweedNote"
        }
       }
      },
      "required": [
       "days_30",
       "days_90",
       "recent"
      ]
     },
     "attention": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "rule": {
         "type": "string",
         "maxLength": 30
        },
        "since": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "detail": {
         "type": "object",
         "additionalProperties": true
        }
       },
       "required": [
        "rule",
        "since"
       ]
      }
     },
     "recitations": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Recitation"
      }
     },
     "qiraat": {
      "type": "object",
      "properties": {
       "paths": {
        "type": "array",
        "items": {
         "type": "object",
         "additionalProperties": true
        }
       }
      },
      "required": [
       "paths"
      ]
     },
     "practice": {
      "type": "object",
      "properties": {
       "linked": {
        "type": "boolean"
       },
       "state": {
        "type": "string",
        "maxLength": 30
       }
      },
      "required": [
       "linked",
       "state"
      ]
     }
    },
    "required": [
     "student",
     "today",
     "week",
     "juz_map",
     "tajweed",
     "qiraat",
     "practice"
    ],
    "description": "qiraat fills in with phase 5 and practice with phase 2 (the Quran reading app link)."
   },
   "QuranSheet": {
    "type": "object",
    "properties": {
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 60
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "students": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string",
         "maxLength": 120
        },
        "first_name": {
         "type": "string",
         "maxLength": 60
        },
        "roll_no": {
         "type": [
          "integer",
          "null"
         ]
        },
        "program": {
         "type": [
          "string",
          "null"
         ]
        },
        "riwayah": {
         "type": [
          "string",
          "null"
         ]
        },
        "enrolled": {
         "type": "boolean"
        },
        "parts": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/QuranLessonPart"
         }
        },
        "juz_memorized": {
         "type": "integer"
        },
        "attention": {
         "type": "array",
         "items": {
          "type": "string"
         }
        },
        "practice": {
         "type": "object",
         "properties": {
          "state": {
           "type": "string",
           "maxLength": 30
          }
         },
         "required": [
          "state"
         ]
        }
       },
       "required": [
        "id",
        "full_name",
        "enrolled",
        "parts",
        "juz_memorized",
        "attention",
        "practice"
       ]
      }
     }
    },
    "required": [
     "section",
     "on",
     "students"
    ]
   },
   "QuranToday": {
    "type": "object",
    "properties": {
     "on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "halaqas": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "label": {
         "type": "string",
         "maxLength": 60
        },
        "students": {
         "type": "integer"
        },
        "set": {
         "type": "integer"
        },
        "heard": {
         "type": "integer"
        },
        "repeat": {
         "type": "integer"
        },
        "attention": {
         "type": "integer"
        }
       },
       "required": [
        "id",
        "label",
        "students",
        "set",
        "heard",
        "repeat",
        "attention"
       ]
      }
     },
     "inbox": {
      "type": "object",
      "properties": {
       "count": {
        "type": "integer"
       },
       "available": {
        "type": "boolean",
        "description": "False until recitations (phase 4)."
       }
      },
      "required": [
       "count",
       "available"
      ]
     },
     "attention": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/QuranAttentionRow"
      }
     },
     "enrolled": {
      "type": "integer"
     }
    },
    "required": [
     "on",
     "halaqas",
     "inbox",
     "attention",
     "enrolled"
    ]
   },
   "QuranSettings": {
    "type": "object",
    "properties": {
     "attention_manzil_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 60
     },
     "attention_repeat_days": {
      "type": "integer",
      "minimum": 2,
      "maximum": 30
     },
     "attention_practice_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 30
     },
     "attention_rules": {
      "type": "object",
      "additionalProperties": {
       "type": "boolean"
      },
      "description": "manzil_overdue, ready_for_test, repeat_streak, practice_dropped: on or off."
     },
     "recitation_retention_days": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 90
     },
     "recitation_max_seconds": {
      "type": "integer",
      "minimum": 60,
      "maximum": 3600
     },
     "summary_weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7,
      "description": "ISO weekday the family's weekly summary goes out (1 Monday to 7 Sunday)."
     },
     "summary_time": {
      "type": "string",
      "description": "HH:MM:SS, school time.",
      "maxLength": 8
     },
     "summary_on": {
      "type": "boolean"
     },
     "summary_last_sent_week": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "attention_manzil_days",
     "attention_repeat_days",
     "attention_practice_days",
     "attention_rules",
     "recitation_max_seconds"
    ]
   },
   "Riwayah": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "maxLength": 30,
      "pattern": "^[a-z][a-z0-9_]{1,29}$"
     },
     "name": {
      "type": "string",
      "maxLength": 60
     },
     "sort_order": {
      "type": "integer"
     },
     "requires_key": {
      "type": [
       "string",
       "null"
      ],
      "description": "The riwayah a student completes first, or null."
     },
     "requires_test": {
      "type": "boolean",
      "description": "Every juz of the required riwayah tested before this one opens."
     },
     "enabled": {
      "type": "boolean"
     },
     "students": {
      "type": "integer",
      "description": "Students enrolled in it now."
     }
    },
    "required": [
     "key",
     "name",
     "sort_order",
     "requires_test",
     "enabled"
    ]
   },
   "RiwayahInput": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "maxLength": 30
     },
     "name": {
      "type": "string",
      "maxLength": 60
     },
     "sort_order": {
      "type": "integer"
     },
     "requires_key": {
      "type": [
       "string",
       "null"
      ]
     },
     "requires_test": {
      "type": "boolean"
     },
     "enabled": {
      "type": "boolean"
     }
    }
   },
   "NamedPerson": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": "string",
      "maxLength": 120
     }
    },
    "required": [
     "user_id"
    ]
   },
   "Ijazah": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "number": {
      "type": "string",
      "description": "IJZ-<year>-<nnnn>, per school.",
      "maxLength": 20
     },
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       },
       "first_name": {
        "type": "string",
        "maxLength": 60
       },
       "admission_no": {
        "type": [
         "string",
         "null"
        ]
       }
      },
      "required": [
       "id",
       "full_name"
      ]
     },
     "riwayah": {
      "type": "string",
      "maxLength": 30
     },
     "riwayah_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "teacher": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/NamedPerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "granted_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "sanad_text": {
      "type": "string",
      "description": "The teacher's sanad as it stood on the day; never changes.",
      "maxLength": 4000
     },
     "witness": {
      "type": [
       "string",
       "null"
      ]
     },
     "annulled_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "annul_reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "annulled_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/NamedPerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "school": {
      "type": "object",
      "properties": {
       "name": {
        "type": "string",
        "maxLength": 120
       }
      }
     }
    },
    "required": [
     "id",
     "number",
     "student",
     "riwayah",
     "granted_on",
     "sanad_text"
    ]
   },
   "QiraatPathRow": {
    "type": "object",
    "properties": {
     "riwayah": {
      "type": "string",
      "maxLength": 30
     },
     "name": {
      "type": "string",
      "maxLength": 60
     },
     "state": {
      "type": "string",
      "enum": [
       "locked",
       "unlocked",
       "in_progress",
       "completed",
       "ijazah"
      ]
     },
     "started_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "completed_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "requires": {
      "type": [
       "string",
       "null"
      ]
     },
     "requires_test": {
      "type": "boolean"
     },
     "juz_tested": {
      "type": "integer"
     },
     "ijazah": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "number": {
          "type": "string",
          "maxLength": 20
         },
         "granted_on": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01"
         },
         "annulled": {
          "type": "boolean"
         }
        },
        "required": [
         "id",
         "number",
         "granted_on",
         "annulled"
        ]
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "riwayah",
     "name",
     "state",
     "requires_test",
     "juz_tested"
    ]
   },
   "QiraatPath": {
    "type": "object",
    "properties": {
     "paths": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/QiraatPathRow"
      }
     },
     "ijazahs": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Ijazah"
      }
     }
    },
    "required": [
     "paths"
    ]
   },
   "Sanad": {
    "type": "object",
    "properties": {
     "teacher": {
      "$ref": "#/components/schemas/NamedPerson"
     },
     "sanad_text": {
      "type": [
       "string",
       "null"
      ]
     },
     "confirmed": {
      "type": "boolean"
     },
     "confirmed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "confirmed_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/NamedPerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "ijazahs_granted": {
      "type": "integer"
     }
    },
    "required": [
     "teacher",
     "confirmed",
     "ijazahs_granted"
    ]
   },
   "QuranCertificate": {
    "type": "object",
    "description": "kind ijazah: an Ijazah with kind. kind juz: the tested juz with student, teacher, grade, date and school.",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "ijazah",
       "juz"
      ]
     }
    },
    "required": [
     "kind"
    ],
    "additionalProperties": true
   },
   "QuranWeek": {
    "type": "object",
    "properties": {
     "from": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "to": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "first_name": {
        "type": "string",
        "maxLength": 60
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       }
      },
      "required": [
       "id",
       "first_name"
      ]
     },
     "parts": {
      "type": "object",
      "properties": {
       "passed": {
        "type": "integer"
       },
       "repeat": {
        "type": "integer"
       },
       "absent": {
        "type": "integer"
       },
       "days": {
        "type": "integer"
       }
      },
      "required": [
       "passed",
       "repeat",
       "absent",
       "days"
      ]
     },
     "sabaq": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/QuranLessonPart"
      }
     },
     "juz_tested": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "juz": {
         "type": "integer"
        },
        "grade": {
         "type": [
          "string",
          "null"
         ]
        },
        "on": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        }
       },
       "required": [
        "juz",
        "on"
       ]
      }
     },
     "juz_memorized": {
      "type": "integer"
     },
     "practice": {
      "type": "object",
      "properties": {
       "ayahs": {
        "type": "integer"
       },
       "minutes_estimated": {
        "type": "integer"
       },
       "days": {
        "type": "integer"
       }
      },
      "required": [
       "ayahs",
       "minutes_estimated",
       "days"
      ]
     },
     "notes": {
      "type": "integer"
     },
     "latest_recitation": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Recitation"
       },
       {
        "type": "null"
       }
      ]
     },
     "ijazahs": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Ijazah"
      }
     },
     "teacher_message": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "from",
     "to",
     "student",
     "parts",
     "sabaq",
     "juz_tested",
     "juz_memorized",
     "practice",
     "notes",
     "ijazahs"
    ]
   },
   "QuranReport": {
    "type": "object",
    "description": "Numbers for the range with the list behind each: enrolled (total, by_program, by_riwayah, list), lessons (days, passed, repeat), juz (memorized, tested, list), practice (linked, ayahs, minutes_estimated, by_halaqa), recitations (received, reviewed, waiting, median_wait_hours), ijazahs (granted, list), attention (by rule).",
    "properties": {
     "from": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "to": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "section": {
      "type": [
       "object",
       "null"
      ]
     },
     "enrolled": {
      "type": "object"
     },
     "lessons": {
      "type": "object"
     },
     "juz": {
      "type": "object"
     },
     "practice": {
      "type": "object"
     },
     "recitations": {
      "type": "object"
     },
     "ijazahs": {
      "type": "object"
     },
     "attention": {
      "type": "object"
     }
    },
    "required": [
     "from",
     "to",
     "enrolled",
     "lessons",
     "juz",
     "practice",
     "recitations",
     "ijazahs",
     "attention"
    ],
    "additionalProperties": true
   },
   "PartnerKey": {
    "type": "object",
    "properties": {
     "key_id": {
      "type": "string",
      "format": "uuid"
     },
     "key_prefix": {
      "type": "string",
      "maxLength": 16
     },
     "secret": {
      "type": "string",
      "description": "The partner key, shown once.",
      "maxLength": 80
     },
     "scopes": {
      "type": "array",
      "items": {
       "type": "string"
      }
     }
    },
    "required": [
     "key_id",
     "key_prefix",
     "secret",
     "scopes"
    ]
   },
   "Partner": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "maxLength": 40
     },
     "slug": {
      "type": "string",
      "maxLength": 30
     },
     "manifest_url": {
      "type": [
       "string",
       "null"
      ]
     },
     "manifest_checked_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "manifest_error": {
      "type": [
       "string",
       "null"
      ]
     },
     "capabilities": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "verified": {
      "type": "boolean"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "revoked_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "webhook": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "url": {
          "type": "string"
         },
         "is_active": {
          "type": "boolean"
         },
         "last_delivery_at": {
          "type": [
           "string",
           "null"
          ],
          "format": "date-time"
         },
         "failing_since": {
          "type": [
           "string",
           "null"
          ],
          "format": "date-time"
         }
        },
        "required": [
         "id",
         "url",
         "is_active"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "keys": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "key_prefix": {
         "type": "string",
         "maxLength": 16
        },
        "created_at": {
         "type": "string",
         "format": "date-time"
        },
        "last_used_at": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        },
        "expires_at": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        }
       },
       "required": [
        "id",
        "key_prefix",
        "created_at"
       ]
      }
     },
     "links": {
      "type": "object",
      "properties": {
       "active": {
        "type": "integer"
       },
       "pending": {
        "type": "integer"
       }
      },
      "required": [
       "active",
       "pending"
      ]
     }
    },
    "required": [
     "id",
     "name",
     "slug",
     "capabilities",
     "verified",
     "created_at",
     "keys",
     "links"
    ]
   },
   "PartnerList": {
    "type": "object",
    "properties": {
     "partners": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Partner"
      }
     },
     "directory": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "slug": {
         "type": "string",
         "maxLength": 30
        },
        "name": {
         "type": "string",
         "maxLength": 40
        },
        "manifest_url": {
         "type": "string"
        },
        "blurb": {
         "type": [
          "string",
          "null"
         ]
        },
        "verified": {
         "type": "boolean"
        }
       },
       "required": [
        "slug",
        "name",
        "manifest_url",
        "verified"
       ]
      }
     }
    },
    "required": [
     "partners",
     "directory"
    ]
   },
   "PartnerCreated": {
    "type": "object",
    "properties": {
     "partner": {
      "$ref": "#/components/schemas/Partner"
     },
     "key": {
      "$ref": "#/components/schemas/PartnerKey"
     },
     "webhook_secret": {
      "type": "string",
      "description": "The webhook signing secret, shown once.",
      "maxLength": 80
     }
    },
    "required": [
     "partner",
     "key",
     "webhook_secret"
    ]
   },
   "ClassCode": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "section": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "label": {
        "type": "string",
        "maxLength": 60
       }
      },
      "required": [
       "id",
       "label"
      ]
     },
     "code": {
      "type": "string",
      "maxLength": 6,
      "example": "K7PX2M"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "ended_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "uses": {
      "type": "integer"
     },
     "max_uses": {
      "type": [
       "integer",
       "null"
      ]
     },
     "open": {
      "type": "boolean"
     },
     "created_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "id",
     "section",
     "code",
     "created_at",
     "expires_at",
     "uses",
     "open"
    ]
   },
   "ClassCodeCard": {
    "type": "object",
    "properties": {
     "code": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/ClassCode"
       },
       {
        "type": "null"
       }
      ]
     },
     "partners": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "name": {
         "type": "string",
         "maxLength": 40
        }
       },
       "required": [
        "id",
        "name"
       ]
      }
     },
     "students": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "first_name": {
         "type": "string",
         "maxLength": 60
        },
        "full_name": {
         "type": "string",
         "maxLength": 120
        },
        "link": {
         "oneOf": [
          {
           "type": "object",
           "properties": {
            "id": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string",
             "maxLength": 20
            },
            "partner": {
             "type": "string",
             "maxLength": 40
            },
            "requested_at": {
             "type": "string",
             "format": "date-time"
            },
            "linked_at": {
             "type": [
              "string",
              "null"
             ],
             "format": "date-time"
            }
           },
           "required": [
            "id",
            "status",
            "partner"
           ]
          },
          {
           "type": "null"
          }
         ]
        }
       },
       "required": [
        "id",
        "first_name",
        "full_name"
       ]
      }
     }
    },
    "required": [
     "code",
     "partners",
     "students"
    ]
   },
   "QuranLink": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "first_name": {
        "type": "string",
        "maxLength": 60
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       },
       "halaqa": {
        "oneOf": [
         {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "label": {
            "type": "string",
            "maxLength": 60
           }
          },
          "required": [
           "id",
           "label"
          ]
         },
         {
          "type": "null"
         }
        ]
       }
      },
      "required": [
       "id",
       "first_name",
       "full_name"
      ]
     },
     "partner": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string",
        "maxLength": 40
       },
       "slug": {
        "type": "string",
        "maxLength": 30
       }
      },
      "required": [
       "id",
       "name",
       "slug"
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "active",
       "revoked",
       "expired"
      ]
     },
     "account_hint": {
      "type": [
       "string",
       "null"
      ],
      "description": "The first two characters of the app account, for support; the account itself is never stored."
     },
     "display": {
      "type": [
       "string",
       "null"
      ]
     },
     "requested_at": {
      "type": "string",
      "format": "date-time"
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "approved_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "linked_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "revoked_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "revoked_by": {
      "type": [
       "string",
       "null"
      ]
     },
     "revoke_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "student_requested_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "practice_deleted_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "approved_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "shares": {
      "type": "array",
      "items": {
       "type": "string"
      }
     }
    },
    "required": [
     "id",
     "student",
     "partner",
     "status",
     "requested_at",
     "expires_at",
     "shares"
    ]
   },
   "Recitation": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "first_name": {
        "type": "string",
        "maxLength": 60
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       },
       "halaqa": {
        "oneOf": [
         {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "label": {
            "type": "string",
            "maxLength": 60
           }
          },
          "required": [
           "id",
           "label"
          ]
         },
         {
          "type": "null"
         }
        ]
       }
      },
      "required": [
       "id",
       "first_name",
       "full_name"
      ]
     },
     "kind": {
      "type": "string",
      "enum": [
       "student",
       "model",
       "juz_test"
      ]
     },
     "attempt": {
      "type": "integer"
     },
     "status": {
      "type": "string",
      "enum": [
       "uploading",
       "submitted",
       "reviewed",
       "deleted"
      ]
     },
     "riwayah": {
      "type": "string",
      "maxLength": 20
     },
     "surah": {
      "type": [
       "integer",
       "null"
      ]
     },
     "surah_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "ayah_from": {
      "type": [
       "integer",
       "null"
      ]
     },
     "ayah_to": {
      "type": [
       "integer",
       "null"
      ]
     },
     "juz": {
      "type": [
       "integer",
       "null"
      ]
     },
     "lesson": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/QuranLessonPart"
       },
       {
        "type": "null"
       }
      ]
     },
     "duration_seconds": {
      "type": "integer"
     },
     "content_type": {
      "type": "string",
      "maxLength": 20
     },
     "ayah_marks": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "ayah": {
         "type": "integer"
        },
        "at_seconds": {
         "type": "number"
        }
       },
       "required": [
        "ayah",
        "at_seconds"
       ]
      }
     },
     "submitted_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "waited_hours": {
      "type": [
       "integer",
       "null"
      ]
     },
     "reviewed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "reviewed_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "grade": {
      "type": [
       "string",
       "null"
      ]
     },
     "remark": {
      "type": [
       "string",
       "null"
      ]
     },
     "notes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TajweedNote"
      }
     },
     "model": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "duration_seconds": {
          "type": "integer"
         },
         "status": {
          "type": "string",
          "maxLength": 20
         }
        },
        "required": [
         "id",
         "duration_seconds",
         "status"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "pinned": {
      "type": "boolean"
     },
     "delete_after": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "deleted_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "playable": {
      "type": "boolean"
     },
     "plays": {
      "type": "integer"
     },
     "from_app": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "student",
     "kind",
     "attempt",
     "status",
     "duration_seconds",
     "content_type",
     "ayah_marks",
     "notes",
     "pinned",
     "playable",
     "plays"
    ]
   },
   "RecitationInbox": {
    "type": "object",
    "properties": {
     "waiting": {
      "type": "integer"
     },
     "items": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Recitation"
      }
     }
    },
    "required": [
     "waiting",
     "items"
    ]
   },
   "PlayLink": {
    "type": "object",
    "properties": {
     "url": {
      "type": "string",
      "format": "uri",
      "description": "Streams the audio for 10 minutes; every play is logged."
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "content_type": {
      "type": "string",
      "maxLength": 20
     },
     "duration_seconds": {
      "type": "integer"
     }
    },
    "required": [
     "url",
     "expires_at",
     "content_type",
     "duration_seconds"
    ]
   },
   "ReviewNoteInput": {
    "type": "object",
    "properties": {
     "surah": {
      "type": "integer",
      "minimum": 1,
      "maximum": 114
     },
     "ayah": {
      "type": "integer",
      "minimum": 1,
      "maximum": 286
     },
     "word_index": {
      "type": "integer",
      "minimum": 0,
      "maximum": 200
     },
     "category": {
      "type": "string",
      "enum": [
       "makharij",
       "madd",
       "ghunnah",
       "qalqalah",
       "waqf",
       "other"
      ]
     },
     "text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "at_seconds": {
      "type": "number",
      "minimum": 0,
      "maximum": 3600
     }
    },
    "required": [
     "ayah",
     "word_index",
     "category",
     "text"
    ]
   },
   "QuranPractice": {
    "type": "object",
    "properties": {
     "from": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "to": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "link": {
      "type": "object",
      "additionalProperties": true
     },
     "days": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "on": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "ayahs": {
         "type": "integer"
        },
        "minutes_estimated": {
         "type": "integer"
        },
        "ranges": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "surah": {
            "type": "integer"
           },
           "from": {
            "type": "integer"
           },
           "to": {
            "type": "integer"
           }
          },
          "required": [
           "surah",
           "from",
           "to"
          ]
         }
        }
       },
       "required": [
        "on",
        "ayahs",
        "minutes_estimated",
        "ranges"
       ]
      }
     }
    },
    "required": [
     "from",
     "to",
     "link",
     "days"
    ]
   },
   "LearnSlot": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7,
      "description": "1 Monday to 7 Sunday."
     },
     "start": {
      "type": "string",
      "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
      "example": "12:30",
      "maxLength": 5
     },
     "end": {
      "type": "string",
      "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
      "example": "14:30",
      "maxLength": 5
     },
     "kind": {
      "type": "string",
      "enum": [
       "lecture",
       "lab"
      ]
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "instructor": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     }
    },
    "required": [
     "weekday",
     "start",
     "end",
     "kind"
    ]
   },
   "LearnSlotInput": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7
     },
     "start": {
      "type": "string",
      "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
      "maxLength": 5
     },
     "end": {
      "type": "string",
      "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
      "maxLength": 5
     },
     "kind": {
      "type": "string",
      "enum": [
       "lecture",
       "lab"
      ]
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "instructor": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     }
    },
    "required": [
     "weekday",
     "start",
     "end"
    ]
   },
   "LearnCourseWhere": {
    "type": "object",
    "properties": {
     "course_id": {
      "type": "string",
      "format": "uuid"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "school_id": {
      "type": "string",
      "format": "uuid"
     },
     "as": {
      "type": "string",
      "enum": [
       "student",
       "incharge"
      ],
      "description": "student: the caller is the scholar (open /family/learn). incharge: her school's admin or a super admin (open her Learn record on the admin side)."
     }
    },
    "required": [
     "course_id",
     "student_id",
     "school_id",
     "as"
    ]
   },
   "LearnCourse": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string",
      "maxLength": 120
     },
     "code": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20
     },
     "instructor": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "term": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "language": {
      "type": "string",
      "enum": [
       "english",
       "urdu",
       "mixed"
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "finished",
       "archived"
      ]
     },
     "subject": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "section_subject_id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string",
          "maxLength": 60
         }
        },
        "required": [
         "section_subject_id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The gradebook subject of the scholar's own section this course sits on (L53), or null."
     },
     "days_of_notes": {
      "type": "integer",
      "description": "Days with at least one picture."
     },
     "last_note_date": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "recent_dates": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01"
      }
     },
     "slots": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnSlot"
      }
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "student_id",
     "title",
     "language",
     "status",
     "days_of_notes",
     "slots"
    ]
   },
   "LearnCourseWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120
     },
     "code": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20
     },
     "instructor": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "term": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "language": {
      "type": "string",
      "enum": [
       "english",
       "urdu",
       "mixed"
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "finished",
       "archived"
      ]
     },
     "section_subject_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "maxLength": 40,
      "description": "A subject of the scholar's own section (L53); NOT_FOUND otherwise."
     }
    }
   },
   "LearnHome": {
    "type": "object",
    "properties": {
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string",
        "maxLength": 120
       },
       "admission_no": {
        "type": "string",
        "maxLength": 20
       }
      },
      "required": [
       "id",
       "full_name"
      ]
     },
     "today": {
      "type": "string",
      "description": "Today in the school's time zone.",
      "format": "date",
      "example": "2026-09-01"
     },
     "weekday": {
      "type": "integer"
     },
     "timezone": {
      "type": "string",
      "maxLength": 60
     },
     "staff_access": {
      "type": "boolean",
      "description": "Whether the school's admins (the incharges) can see notes (L54)."
     },
     "courses": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnCourse"
      }
     }
    },
    "required": [
     "student",
     "today",
     "weekday",
     "staff_access",
     "courses"
    ]
   },
   "LearnWeek": {
    "type": "object",
    "properties": {
     "today": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "weekday": {
      "type": "integer"
     },
     "slots": {
      "type": "array",
      "items": {
       "allOf": [
        {
         "$ref": "#/components/schemas/LearnSlot"
        },
        {
         "type": "object",
         "properties": {
          "course_id": {
           "type": "string",
           "format": "uuid"
          },
          "course_title": {
           "type": "string",
           "maxLength": 120
          }
         }
        }
       ]
      }
     }
    },
    "required": [
     "today",
     "weekday",
     "slots"
    ]
   },
   "LearnNoteDay": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "note_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "summary_set": {
      "type": "boolean"
     },
     "pictures": {
      "type": "integer"
     }
    },
    "required": [
     "id",
     "note_date",
     "pictures"
    ]
   },
   "LearnPicture": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "maxLength": 200
     },
     "content_type": {
      "type": "string",
      "enum": [
       "image/jpeg",
       "image/png",
       "image/webp",
       "application/pdf"
      ]
     },
     "size_bytes": {
      "type": "integer"
     },
     "position": {
      "type": "integer"
     },
     "label": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     },
     "added_by_kind": {
      "type": "string",
      "enum": [
       "student",
       "incharge",
       "coordinator"
      ],
      "description": "incharge: added by a school admin for the scholar (L54); coordinator: by a person named her coordinator (L83)."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "sha256": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^[0-9a-f]{64}$",
      "description": "The picture's fingerprint (SHA-256 of the file): the same picture is never added to a day twice (L85)."
     },
     "url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "A link valid 10 minutes."
     }
    },
    "required": [
     "id",
     "name",
     "content_type",
     "size_bytes",
     "added_by_kind",
     "url"
    ]
   },
   "LearnNote": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "course_id": {
      "type": "string",
      "format": "uuid"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "note_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "summary": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "status": {
      "type": "string",
      "enum": [
       "draft",
       "queued",
       "reading",
       "extracted",
       "reviewed",
       "drafted",
       "published"
      ]
     },
     "pictures": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnPicture"
      }
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "guide": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LearnGuide"
       },
       {
        "type": "null"
       }
      ],
      "description": "The note's study guide (L55), or null."
     },
     "share_link": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LearnShareLink"
       },
       {
        "type": "null"
       }
      ],
      "description": "The guide's active share link (L62), or null. The token and url only for the incharge."
     },
     "guide_source_hash": {
      "type": "string",
      "description": "Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58).",
      "maxLength": 64
     },
     "picture_count": {
      "type": "integer",
      "description": "How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83)."
     },
     "fingerprints": {
      "type": "array",
      "items": {
       "type": "string",
       "pattern": "^[0-9a-f]{64}$",
       "maxLength": 64
      },
      "description": "The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85)."
     },
     "duplicate_of": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Only when adding a picture: it was already on this day (the same file), so nothing was added; the id of the one there (L85). The answer is then 200, not 201."
     },
     "guide_request": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LearnGuideRequest"
       },
       {
        "type": "null"
       }
      ],
      "description": "Whether a study guide was asked for and is being made (L97), or the day was marked as needing none (L98). Not in a coordinator's view."
     }
    },
    "required": [
     "id",
     "course_id",
     "student_id",
     "note_date",
     "status",
     "pictures"
    ]
   },
   "LearnGuideRequest": {
    "type": "object",
    "properties": {
     "waiting": {
      "type": "boolean",
      "description": "Asked for with Make the study guide now (or Make it again) and not made since: the guide run makes it, usually within the hour (L97)."
     },
     "requested_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "skipped": {
      "type": "boolean",
      "description": "Marked as needing no guide for the pictures the day holds now; a picture added or removed ends it (L98)."
     },
     "skip_reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "skipped_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "kind": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "fix",
       "improve",
       "expand",
       null
      ],
      "description": "What kind of new version was asked for (L100)."
     },
     "what": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     },
     "section": {
      "type": [
       "integer",
       "null"
      ]
     },
     "asked_by_kind": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "incharge",
       "student",
       null
      ]
     },
     "versions": {
      "type": "integer",
      "minimum": 0,
      "description": "How many versions the guide has (L99)."
     }
    },
    "required": [
     "waiting",
     "skipped"
    ]
   },
   "LearnGuideSkip": {
    "type": "object",
    "properties": {
     "reason": {
      "type": "string",
      "description": "Why the day needs no study guide, in a few words (1 to 200 characters): for example, only the title slide of the lecture.",
      "minLength": 1,
      "maxLength": 200
     }
    },
    "required": [
     "reason"
    ]
   },
   "LearnUpload": {
    "type": "object",
    "properties": {
     "upload_id": {
      "type": "string",
      "format": "uuid"
     },
     "url": {
      "type": "string",
      "format": "uri",
      "maxLength": 2000
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "upload_id",
     "url",
     "expires_at"
    ]
   },
   "LearnUploadRequest": {
    "type": "object",
    "properties": {
     "file_name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "content_type": {
      "type": "string",
      "enum": [
       "image/jpeg",
       "image/png",
       "image/webp",
       "application/pdf",
       "application/zip"
      ],
      "description": "application/zip only for a guide's ZIP (kind guide, role bundle_zip)."
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 20971520,
      "description": "Pictures up to 20 MB; a PDF up to 10 MB (a guide's PDF up to 20 MB); a guide's ZIP up to 20 MB."
     },
     "kind": {
      "type": "string",
      "enum": [
       "note",
       "guide"
      ],
      "description": "guide: a study guide's file (L55), uploaded by the incharge only; it never counts towards the 12 pictures a day. Default note."
     },
     "role": {
      "type": "string",
      "enum": [
       "page",
       "thumbnail",
       "thumbnail_wide",
       "bundle_pdf",
       "bundle_zip"
      ],
      "description": "For kind guide: page, thumbnail (1080 square), thumbnail_wide (1200 by 630), bundle_pdf or bundle_zip."
     },
     "ordinal": {
      "type": "integer",
      "minimum": 1,
      "maximum": 40,
      "description": "For kind guide: the page number; 1 for the other roles."
     },
     "width": {
      "type": "integer",
      "minimum": 1,
      "maximum": 4000
     },
     "height": {
      "type": "integer",
      "minimum": 1,
      "maximum": 4000
     }
    },
    "required": [
     "file_name",
     "content_type",
     "size_bytes"
    ]
   },
   "LearnPictureAdd": {
    "type": "object",
    "properties": {
     "upload_id": {
      "type": "string",
      "format": "uuid"
     },
     "label": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     }
    },
    "required": [
     "upload_id"
    ]
   },
   "LearnCoordinator": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "role": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "is_scholar": {
      "type": "boolean",
      "description": "The coordinator is a scholar of the school herself."
     },
     "email_copy": {
      "type": "boolean",
      "description": "She gets a copy of the scholar's evening email: the study guides with a share link, never the notes (L87)."
     },
     "has_email": {
      "type": "boolean",
      "description": "She has an email address, so a copy can reach her."
     },
     "added_at": {
      "type": "string",
      "format": "date-time"
     },
     "added_by": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 120
         }
        },
        "required": [
         "user_id"
        ]
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "user_id",
     "added_at"
    ]
   },
   "LearnCoordinatorAdd": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "description": "The person of the school to name (a profile's user id, as in GET /admin/people).",
      "format": "uuid"
     }
    },
    "required": [
     "user_id"
    ]
   },
   "LearnCoordinatorChange": {
    "type": "object",
    "properties": {
     "email_copy": {
      "type": "boolean",
      "description": "Turn her copy of the scholar's evening email on or off (L87)."
     }
    },
    "required": [
     "email_copy"
    ]
   },
   "LearnMaterial": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "course_id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "content_type": {
      "type": "string",
      "enum": [
       "application/pdf",
       "application/vnd.openxmlformats-officedocument.presentationml.presentation",
       "application/vnd.ms-powerpoint",
       "image/jpeg",
       "image/png",
       "image/webp"
      ],
      "description": "A PDF, PowerPoint or picture (L88)."
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 20971520
     },
     "sha256": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^[0-9a-f]{64}$"
     },
     "added_by_kind": {
      "type": "string",
      "enum": [
       "student",
       "incharge",
       "coordinator"
      ],
      "description": "Who added it: her, her incharge or her coordinator."
     },
     "mine": {
      "type": "boolean",
      "description": "The caller added it (a coordinator or incharge removes only these, L90)."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "maxLength": 2000,
      "description": "A 10-minute download link."
     },
     "duplicate_of": {
      "type": "string",
      "format": "uuid",
      "description": "On add: the same file is already in the course; nothing was added (L89)."
     }
    },
    "required": [
     "id",
     "course_id",
     "title",
     "content_type",
     "size_bytes",
     "added_by_kind",
     "created_at"
    ]
   },
   "LearnMaterialUploadRequest": {
    "type": "object",
    "properties": {
     "file_name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "content_type": {
      "type": "string",
      "enum": [
       "application/pdf",
       "application/vnd.openxmlformats-officedocument.presentationml.presentation",
       "application/vnd.ms-powerpoint",
       "image/jpeg",
       "image/png",
       "image/webp"
      ]
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 20971520
     }
    },
    "required": [
     "file_name",
     "content_type",
     "size_bytes"
    ]
   },
   "LearnMaterialAdd": {
    "type": "object",
    "properties": {
     "upload_id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120,
      "description": "Defaults to the file name without its extension."
     }
    },
    "required": [
     "upload_id"
    ]
   },
   "LearnFeedback": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "note_id": {
      "type": "string",
      "format": "uuid"
     },
     "rating": {
      "type": "integer",
      "minimum": 1,
      "maximum": 5
     },
     "suggestion": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     },
     "status": {
      "type": "string",
      "enum": [
       "none",
       "open",
       "changed",
       "kept"
      ],
      "description": "none: a rating only; open: a suggestion waiting for her incharge; changed or kept: answered (L93)."
     },
     "answer": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300
     },
     "answered_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "student_id": {
      "type": "string",
      "description": "Her incharge's view only.",
      "format": "uuid"
     },
     "first_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     },
     "course_id": {
      "type": "string",
      "format": "uuid"
     },
     "course": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "note_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "guide_title": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     }
    },
    "required": [
     "id",
     "note_id",
     "rating",
     "status",
     "updated_at"
    ]
   },
   "LearnFeedbackWrite": {
    "type": "object",
    "properties": {
     "rating": {
      "type": "integer",
      "minimum": 1,
      "maximum": 5
     },
     "suggestion": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000,
      "description": "What should change, in her words. Empty for a rating only."
     }
    },
    "required": [
     "rating"
    ]
   },
   "LearnFeedbackAnswer": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "changed",
       "kept"
      ],
      "description": "changed: the guide was made again; kept: it stays as it is."
     },
     "answer": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "A short note she reads under her rating."
     }
    },
    "required": [
     "status"
    ]
   },
   "LearnGuideQueueItem": {
    "type": "object",
    "properties": {
     "note_id": {
      "type": "string",
      "format": "uuid"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "first_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     },
     "scholar": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "course_id": {
      "type": "string",
      "format": "uuid"
     },
     "course": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "note_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "pictures": {
      "type": "integer",
      "minimum": 1
     },
     "reason": {
      "type": "string",
      "enum": [
       "new",
       "stale",
       "suggestion",
       "requested"
      ],
      "description": "new: pictures and no guide; stale: pictures changed since the guide; suggestion: an open suggestion (L94); requested: her incharge asked for it to be made again (L97)."
     },
     "requested_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Set when her incharge asked for the guide now and it is not made yet (L97). These come first, oldest ask first."
     },
     "ask": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "kind": {
          "type": [
           "string",
           "null"
          ]
         },
         "what": {
          "type": [
           "string",
           "null"
          ]
         },
         "section": {
          "type": [
           "integer",
           "null"
          ]
         },
         "by": {
          "type": [
           "string",
           "null"
          ]
         }
        }
       },
       {
        "type": "null"
       }
      ],
      "description": "What was asked for the new version, and by whom (L100, L101)."
     },
     "versions": {
      "type": "integer",
      "minimum": 0,
      "description": "Versions so far; read them with GET /admin/learn/notes/{note_id}/guide-versions before making the next (L105)."
     },
     "guide_title": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "feedback_id": {
      "oneOf": [
       {
        "type": "string",
        "format": "uuid"
       },
       {
        "type": "null"
       }
      ]
     },
     "suggestion": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     },
     "materials": {
      "type": "integer",
      "minimum": 0,
      "description": "Materials on the course, to read with the pictures."
     }
    },
    "required": [
     "note_id",
     "student_id",
     "course_id",
     "note_date",
     "pictures",
     "reason"
    ]
   },
   "LearnCoordinatorRemove": {
    "type": "object",
    "properties": {
     "reason": {
      "type": "string",
      "description": "Why, in a few words (1 to 200 characters).",
      "minLength": 1,
      "maxLength": 200
     }
    },
    "required": [
     "reason"
    ]
   },
   "LearnCoordinated": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": "string",
      "maxLength": 120
     },
     "admission_no": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "since": {
      "type": "string",
      "format": "date-time"
     },
     "today": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "courses": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "title": {
         "type": "string",
         "maxLength": 120
        },
        "status": {
         "type": "string",
         "maxLength": 20
        }
       },
       "required": [
        "id",
        "title"
       ]
      }
     }
    },
    "required": [
     "student_id",
     "full_name",
     "courses"
    ]
   },
   "LearnEvent": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer"
     },
     "kind": {
      "type": "string",
      "enum": [
       "learn_opened",
       "course_added",
       "course_changed",
       "week_set",
       "note_added",
       "note_changed",
       "pictures_added",
       "picture_removed",
       "staff_read",
       "staff_access_on",
       "staff_access_off",
       "guide_added",
       "guide_updated",
       "guide_removed",
       "share_link_created",
       "share_link_revoked",
       "share_link_viewed",
       "share_name_changed",
       "daily_email_changed",
       "coordinator_added",
       "coordinator_removed",
       "coordinator_email_changed",
       "material_added",
       "material_removed",
       "guide_rated",
       "feedback_answered",
       "first_run_seen",
       "first_run_reset",
       "guide_requested",
       "guide_skipped",
       "guide_version_restored"
      ]
     },
     "actor_kind": {
      "type": "string",
      "enum": [
       "student",
       "incharge",
       "system",
       "coordinator"
      ],
      "description": "incharge: a school admin of the school, shown to the scholar as by your incharge; coordinator: a person her incharge named (L82), shown as by your coordinator."
     },
     "actor": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 120
         }
        },
        "required": [
         "user_id"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "course_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "note_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "detail": {
      "type": "object",
      "additionalProperties": true
     },
     "at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "kind",
     "actor_kind",
     "at"
    ]
   },
   "LearnSettings": {
    "type": "object",
    "properties": {
     "staff_access": {
      "type": "boolean"
     },
     "staff_access_changed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "staff_access_changed_by": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 120
         }
        },
        "required": [
         "user_id"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "scholars_share_guides": {
      "type": "boolean",
      "description": "Scholars may make share links for their own guides (L69). Off; not yet changeable."
     },
     "daily_email": {
      "type": "boolean",
      "description": "The evening email to scholars (L71 to L77). On by default; off stops it for the whole school."
     },
     "daily_email_cc_on": {
      "type": "boolean",
      "description": "Copy every evening email to daily_email_cc (L79). Off by default."
     },
     "daily_email_cc": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "email",
       "maxLength": 254
      },
      "maxItems": 5,
      "description": "Up to five addresses the evening emails are copied to when daily_email_cc_on (L79)."
     },
     "daily_email_cc_changed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "daily_email_cc_changed_by": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 120
         }
        },
        "required": [
         "user_id"
        ]
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "staff_access"
    ]
   },
   "LearnGuideSection": {
    "type": "object",
    "properties": {
     "heading": {
      "type": "string",
      "minLength": 2,
      "maxLength": 80
     },
     "emoji": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 8
     },
     "hook": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 160
     },
     "lines": {
      "type": "array",
      "items": {
       "type": "string",
       "minLength": 1,
       "maxLength": 200
      },
      "minItems": 2,
      "maxItems": 8
     },
     "spot": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^spot-[a-z0-9-]{1,40}$",
      "maxLength": 45
     }
    },
    "required": [
     "heading",
     "lines"
    ]
   },
   "LearnGuideFormula": {
    "type": "object",
    "properties": {
     "label": {
      "type": "string",
      "minLength": 1,
      "maxLength": 60
     },
     "expr": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 160
     },
     "section": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 12,
      "description": "The 1-based section it belongs on, or null."
     }
    },
    "required": [
     "label",
     "expr"
    ]
   },
   "LearnGuideTable": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
     },
     "columns": {
      "type": "array",
      "items": {
       "type": "string",
       "minLength": 1,
       "maxLength": 40
      },
      "minItems": 2,
      "maxItems": 6
     },
     "rows": {
      "type": "array",
      "items": {
       "type": "array",
       "items": {
        "type": "string",
        "maxLength": 40
       },
       "maxItems": 6
      },
      "maxItems": 20
     },
     "section": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 12,
      "description": "The 1-based section it belongs on, or null."
     }
    },
    "required": [
     "title",
     "columns",
     "rows"
    ]
   },
   "LearnGuideCheck": {
    "type": "object",
    "properties": {
     "says": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "correct": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "where": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     },
     "section": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 12,
      "description": "The 1-based section it belongs on, or null."
     }
    },
    "required": [
     "says",
     "correct"
    ]
   },
   "LearnGuideWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "big_idea": {
      "type": "string",
      "minLength": 3,
      "maxLength": 240
     },
     "quote": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "text": {
          "type": "string",
          "minLength": 1,
          "maxLength": 240
         },
         "by": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 60
         }
        },
        "required": [
         "text"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "A line from her incharge for the cover."
     },
     "subject": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^[a-z]{1,20}$",
      "maxLength": 20,
      "description": "Chooses the colours: chemistry, physics, english; anything else is ink."
     },
     "sections": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideSection"
      },
      "minItems": 1,
      "maxItems": 12
     },
     "formulas": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideFormula"
      },
      "maxItems": 12
     },
     "tables": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideTable"
      },
      "maxItems": 6
     },
     "checks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideCheck"
      },
      "maxItems": 20
     },
     "practice": {
      "type": "array",
      "items": {
       "type": "string",
       "minLength": 1,
       "maxLength": 240
      },
      "maxItems": 12
     },
     "style": {
      "type": "string",
      "enum": [
       "poster-v2"
      ]
     },
     "show_checks_publicly": {
      "type": "boolean",
      "description": "Show check these on the public page and in the pictures (L61). Default false."
     },
     "pages": {
      "type": "integer",
      "minimum": 0,
      "maximum": 40,
      "description": "How many page pictures make the guide; pages beyond it are removed (LEARN_GUIDE_PICTURES_MISSING when one is missing). Default: keep what is there."
     },
     "notify": {
      "type": "boolean",
      "description": "Put a replaced guide in her evening email (L74). A new guide is always in it; nothing is emailed at once."
     },
     "source_hash": {
      "type": "string",
      "description": "The note's guide_source_hash the guide was made from; LEARN_GUIDE_STALE when her pictures changed since.",
      "pattern": "^[0-9a-f]{64}$",
      "maxLength": 64
     },
     "change_kind": {
      "type": "string",
      "enum": [
       "fix",
       "improve",
       "expand"
      ],
      "description": "For a guide that already has a version: what kind of change this version is (L99). Default: the waiting ask's kind, else improve."
     },
     "change_note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500,
      "description": "What changed from the version before, in a sentence or two (L104). The guide run always writes it."
     }
    },
    "required": [
     "title",
     "big_idea",
     "sections"
    ]
   },
   "LearnGuideAsk": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "fix",
       "improve",
       "expand"
      ],
      "description": "fix: a mistake to put right (say what); improve: make it better; expand: go deeper on one idea (pick the section or say which)."
     },
     "what": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000,
      "description": "What to change, in her or your words."
     },
     "section": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 12,
      "description": "The section to go deeper on (its number in the guide)."
     }
    }
   },
   "LearnGuideVersion": {
    "type": "object",
    "properties": {
     "number": {
      "type": "integer",
      "minimum": 1
     },
     "current": {
      "type": "boolean"
     },
     "change_kind": {
      "type": "string",
      "enum": [
       "first",
       "fix",
       "improve",
       "expand",
       "restore",
       "earlier"
      ],
      "description": "earlier: a version from before versions were kept; only its pictures were kept."
     },
     "ask": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     },
     "ask_section": {
      "type": [
       "integer",
       "null"
      ]
     },
     "asked_by_kind": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "incharge",
       "student",
       null
      ]
     },
     "asked_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "change_note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500
     },
     "restored_from": {
      "type": [
       "integer",
       "null"
      ]
     },
     "text_kept": {
      "type": "boolean"
     },
     "title": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "pages": {
      "type": "integer",
      "minimum": 0
     },
     "rating": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 5,
      "description": "Her stars for this version (L104)."
     },
     "guide": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "title": {
          "type": "string",
          "minLength": 3,
          "maxLength": 120
         },
         "big_idea": {
          "type": "string",
          "minLength": 3,
          "maxLength": 240
         },
         "quote": {
          "oneOf": [
           {
            "type": "object",
            "properties": {
             "text": {
              "type": "string",
              "minLength": 1,
              "maxLength": 240
             },
             "by": {
              "type": [
               "string",
               "null"
              ],
              "maxLength": 60
             }
            },
            "required": [
             "text"
            ]
           },
           {
            "type": "null"
           }
          ],
          "description": "A line from her incharge for the cover."
         },
         "subject": {
          "type": [
           "string",
           "null"
          ],
          "pattern": "^[a-z]{1,20}$",
          "maxLength": 20,
          "description": "Chooses the colours: chemistry, physics, english; anything else is ink."
         },
         "sections": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/LearnGuideSection"
          },
          "minItems": 1,
          "maxItems": 12
         },
         "formulas": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/LearnGuideFormula"
          },
          "maxItems": 12
         },
         "tables": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/LearnGuideTable"
          },
          "maxItems": 6
         },
         "checks": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/LearnGuideCheck"
          },
          "maxItems": 20
         },
         "practice": {
          "type": "array",
          "items": {
           "type": "string",
           "minLength": 1,
           "maxLength": 240
          },
          "maxItems": 12
         },
         "style": {
          "type": "string",
          "enum": [
           "poster-v2"
          ]
         }
        },
        "required": [
         "title",
         "big_idea",
         "sections"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "Only when reading one version; null when its text was not kept."
     },
     "page_files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideFile"
      }
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideFile"
      }
     },
     "rating_suggestion": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     }
    },
    "required": [
     "number",
     "current",
     "change_kind",
     "text_kept",
     "created_at"
    ]
   },
   "LearnGuideFile": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "role": {
      "type": "string",
      "enum": [
       "page",
       "thumbnail",
       "thumbnail_wide",
       "bundle_pdf",
       "bundle_zip"
      ]
     },
     "ordinal": {
      "type": "integer"
     },
     "content_type": {
      "type": "string",
      "maxLength": 40
     },
     "size_bytes": {
      "type": "integer"
     },
     "width": {
      "type": [
       "integer",
       "null"
      ]
     },
     "height": {
      "type": [
       "integer",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "A link valid 10 minutes."
     }
    },
    "required": [
     "id",
     "role",
     "ordinal",
     "content_type",
     "url"
    ]
   },
   "LearnGuide": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "big_idea": {
      "type": "string",
      "minLength": 3,
      "maxLength": 240
     },
     "quote": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "text": {
          "type": "string",
          "minLength": 1,
          "maxLength": 240
         },
         "by": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 60
         }
        },
        "required": [
         "text"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "A line from her incharge for the cover."
     },
     "subject": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^[a-z]{1,20}$",
      "maxLength": 20,
      "description": "Chooses the colours: chemistry, physics, english; anything else is ink."
     },
     "sections": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideSection"
      },
      "minItems": 1,
      "maxItems": 12
     },
     "formulas": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideFormula"
      },
      "maxItems": 12
     },
     "tables": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideTable"
      },
      "maxItems": 6
     },
     "checks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideCheck"
      },
      "maxItems": 20
     },
     "practice": {
      "type": "array",
      "items": {
       "type": "string",
       "minLength": 1,
       "maxLength": 240
      },
      "maxItems": 12
     },
     "style": {
      "type": "string",
      "enum": [
       "poster-v2"
      ]
     },
     "show_checks_publicly": {
      "type": "boolean"
     },
     "hidden_by_scholar": {
      "type": "boolean",
      "description": "The scholar hid it from her own note page (L60)."
     },
     "stale": {
      "type": "boolean",
      "description": "Her pictures changed since the guide was made (L58)."
     },
     "source_hash": {
      "type": "string",
      "maxLength": 64
     },
     "pages": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideFile"
      }
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnGuideFile"
      }
     },
     "created_by": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 120
         }
        },
        "required": [
         "user_id"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "title",
     "big_idea",
     "sections",
     "show_checks_publicly",
     "hidden_by_scholar",
     "stale",
     "pages",
     "files"
    ]
   },
   "LearnShareLink": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "active": {
      "type": "boolean"
     },
     "token": {
      "type": "string",
      "description": "22 URL-safe characters. Only for the incharge.",
      "pattern": "^[A-Za-z0-9_-]{22}$",
      "maxLength": 22
     },
     "url": {
      "type": "string",
      "description": "The public page, on the school's own address. Only for the incharge.",
      "format": "uri",
      "maxLength": 300
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "expires_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "view_count": {
      "type": "integer"
     },
     "last_viewed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "active",
     "created_at",
     "view_count"
    ]
   },
   "LearnScholarSettings": {
    "type": "object",
    "properties": {
     "share_name_publicly": {
      "type": "boolean",
      "description": "Her first name on shared study guides (L66). Off: \"A saMas scholar\"."
     },
     "changed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "changed_by": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 120
         }
        },
        "required": [
         "user_id"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "changed_by_kind": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "student",
       "incharge",
       null
      ]
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300
     },
     "daily_email": {
      "type": "boolean",
      "description": "Email me my notes each evening (L77). On by default."
     },
     "daily_email_changed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "daily_email_changed_by_kind": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "student",
       "incharge",
       null
      ]
     }
    },
    "required": [
     "share_name_publicly",
     "daily_email"
    ]
   },
   "LearnPublicPicture": {
    "type": "object",
    "properties": {
     "ordinal": {
      "type": "integer"
     },
     "url": {
      "type": "string",
      "description": "A link valid 15 minutes.",
      "format": "uri",
      "maxLength": 2000
     },
     "width": {
      "type": [
       "integer",
       "null"
      ]
     },
     "height": {
      "type": [
       "integer",
       "null"
      ]
     },
     "content_type": {
      "type": "string",
      "maxLength": 40
     },
     "alt": {
      "type": "string",
      "maxLength": 300
     }
    },
    "required": [
     "ordinal",
     "url"
    ]
   },
   "LearnPublicGuide": {
    "type": "object",
    "properties": {
     "school": {
      "type": "object",
      "properties": {
       "name": {
        "type": "string",
        "maxLength": 200
       },
       "crest_url": {
        "type": [
         "string",
         "null"
        ],
        "format": "uri"
       }
      },
      "required": [
       "name"
      ]
     },
     "course_name": {
      "type": "string",
      "maxLength": 120
     },
     "note_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "scholar_label": {
      "type": "string",
      "description": "Her first name when she agreed (L66), else \"A saMas scholar\".",
      "maxLength": 80
     },
     "guide": {
      "type": "object",
      "properties": {
       "title": {
        "type": "string",
        "minLength": 3,
        "maxLength": 120
       },
       "big_idea": {
        "type": "string",
        "minLength": 3,
        "maxLength": 240
       },
       "quote": {
        "oneOf": [
         {
          "type": "object",
          "properties": {
           "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 240
           },
           "by": {
            "type": [
             "string",
             "null"
            ],
            "maxLength": 60
           }
          },
          "required": [
           "text"
          ]
         },
         {
          "type": "null"
         }
        ],
        "description": "A line from her incharge for the cover."
       },
       "subject": {
        "type": [
         "string",
         "null"
        ],
        "pattern": "^[a-z]{1,20}$",
        "maxLength": 20,
        "description": "Chooses the colours: chemistry, physics, english; anything else is ink."
       },
       "sections": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/LearnGuideSection"
        },
        "minItems": 1,
        "maxItems": 12
       },
       "formulas": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/LearnGuideFormula"
        },
        "maxItems": 12
       },
       "tables": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/LearnGuideTable"
        },
        "maxItems": 6
       },
       "practice": {
        "type": "array",
        "items": {
         "type": "string",
         "minLength": 1,
         "maxLength": 240
        },
        "maxItems": 12
       },
       "style": {
        "type": "string",
        "maxLength": 20
       },
       "checks": {
        "oneOf": [
         {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/LearnGuideCheck"
          }
         },
         {
          "type": "null"
         }
        ],
        "description": "Only when the incharge turned them on (L61); null otherwise."
       }
      },
      "required": [
       "title",
       "big_idea",
       "sections"
      ]
     },
     "pictures": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnPublicPicture"
      }
     },
     "thumbnail": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LearnPublicPicture"
       },
       {
        "type": "null"
       }
      ]
     },
     "originals": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LearnPublicPicture"
      }
     },
     "downloads": {
      "type": "object",
      "properties": {
       "pdf": {
        "type": "boolean"
       },
       "zip": {
        "type": "boolean"
       }
      },
      "required": [
       "pdf",
       "zip"
      ]
     },
     "version": {
      "type": "object",
      "properties": {
       "number": {
        "type": "integer"
       },
       "current": {
        "type": "integer"
       },
       "text_kept": {
        "type": "boolean",
        "description": "False for an earlier version whose text was not kept: guide is then the current text; show the pictures only."
       }
      }
     },
     "versions": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "number": {
         "type": "integer"
        },
        "change_kind": {
         "type": "string",
         "maxLength": 20
        },
        "change_note": {
         "type": [
          "string",
          "null"
         ],
         "maxLength": 500
        },
        "text_kept": {
         "type": "boolean"
        },
        "created_at": {
         "type": "string",
         "format": "date-time"
        }
       }
      }
     }
    },
    "required": [
     "school",
     "course_name",
     "note_date",
     "scholar_label",
     "guide",
     "pictures",
     "originals",
     "downloads"
    ]
   },
   "LearnCourseCreate": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120
     },
     "code": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20
     },
     "instructor": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "term": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "language": {
      "type": "string",
      "enum": [
       "english",
       "urdu",
       "mixed"
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "finished",
       "archived"
      ]
     },
     "section_subject_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "maxLength": 40,
      "description": "A subject of the scholar's own section (L53); NOT_FOUND otherwise."
     }
    },
    "required": [
     "title"
    ]
   },
   "ExamTermCreate": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 3,
      "maxLength": 80
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": "string",
      "description": "At most 60 days after the first day.",
      "format": "date",
      "example": "2026-09-01"
     },
     "instructions": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     }
    },
    "required": [
     "name",
     "starts_on",
     "ends_on"
    ]
   },
   "ExamPaperCreate": {
    "type": "object",
    "properties": {
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "subject_id": {
      "type": "string",
      "format": "uuid"
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "starts_at": {
      "type": "string",
      "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
      "example": "09:00"
     },
     "duration_min": {
      "type": "integer",
      "minimum": 15,
      "maximum": 300
     },
     "room": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "invigilator": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "max_marks": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
     }
    },
    "required": [
     "class_id",
     "subject_id",
     "on_date",
     "starts_at",
     "duration_min",
     "max_marks"
    ]
   },
   "AdmissionStatus": {
    "type": "string",
    "enum": [
     "applied",
     "under_review",
     "test_booked",
     "offered",
     "accepted",
     "enrolled",
     "waitlisted",
     "rejected",
     "withdrawn"
    ],
    "description": "applied, under review, test or interview booked, offered, accepted, enrolled; side exits waitlisted, rejected, withdrawn."
   },
   "AdmissionGuardian": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 2,
      "maxLength": 100,
      "example": "Ahmed Khan"
     },
     "relation": {
      "type": "string",
      "enum": [
       "father",
       "mother",
       "guardian",
       "grandparent",
       "other"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^\\+[1-9][0-9]{6,14}$",
      "example": "+923001234567"
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "maxLength": 200
     }
    },
    "required": [
     "name",
     "relation"
    ]
   },
   "Admission": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "application_no": {
      "type": "string",
      "example": "APP-2026-0007"
     },
     "academic_year": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string",
        "example": "2026-27"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "class": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string",
        "example": "Grade 1"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "child_name": {
      "type": "string",
      "example": "Ahmad Khan"
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "male",
       "female",
       "other",
       null
      ]
     },
     "previous_school": {
      "type": [
       "string",
       "null"
      ]
     },
     "guardians": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/AdmissionGuardian"
      }
     },
     "contact_email": {
      "type": [
       "string",
       "null"
      ]
     },
     "contact_phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "notes": {
      "type": [
       "string",
       "null"
      ]
     },
     "documents": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "source": {
      "type": "string",
      "enum": [
       "office",
       "online"
      ]
     },
     "status": {
      "$ref": "#/components/schemas/AdmissionStatus"
     },
     "test_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "decision_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "student_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Set once enrolled."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "application_no",
     "academic_year",
     "class",
     "child_name",
     "date_of_birth",
     "guardians",
     "documents",
     "source",
     "status",
     "created_at"
    ]
   },
   "AdmissionEvent": {
    "type": "object",
    "properties": {
     "from_status": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/AdmissionStatus"
       },
       {
        "type": "null"
       }
      ]
     },
     "to_status": {
      "$ref": "#/components/schemas/AdmissionStatus"
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "to_status",
     "at"
    ]
   },
   "AdmissionWrite": {
    "type": "object",
    "properties": {
     "academic_year_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "child_name": {
      "type": "string",
      "pattern": "^[A-Za-z'\\- ]{2,80}$",
      "maxLength": 80
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "male",
       "female",
       "other",
       null
      ]
     },
     "previous_school": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "guardians": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/AdmissionGuardian"
      },
      "minItems": 1,
      "maxItems": 3
     },
     "contact_email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "maxLength": 200
     },
     "contact_phone": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^\\+[1-9][0-9]{6,14}$",
      "example": "+923001234567"
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     },
     "documents": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 6
     }
    }
   },
   "AdmissionMove": {
    "type": "object",
    "properties": {
     "to": {
      "type": "string",
      "enum": [
       "under_review",
       "test_booked",
       "offered",
       "accepted",
       "waitlisted",
       "rejected",
       "withdrawn"
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500
     },
     "test_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "override": {
      "type": "boolean",
      "description": "Offer beyond the seat limit (needs a note)."
     }
    },
    "required": [
     "to"
    ]
   },
   "AdmissionMoveResult": {
    "type": "object",
    "properties": {
     "application": {
      "$ref": "#/components/schemas/Admission"
     },
     "next_waitlisted": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "application_no": {
          "type": "string"
         },
         "child_name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "application_no",
         "child_name"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "When a seat just freed: the first waitlisted child for that class to offer next."
     }
    },
    "required": [
     "application"
    ]
   },
   "AdmissionEnrolResult": {
    "type": "object",
    "properties": {
     "application": {
      "$ref": "#/components/schemas/Admission"
     },
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "admission_no": {
      "type": "string"
     },
     "guardians_linked": {
      "type": "integer"
     },
     "guardians_created": {
      "type": "integer"
     },
     "invoices": {
      "type": "integer",
      "description": "Invoices raised now (only when the class's invoices for this month already exist)."
     }
    },
    "required": [
     "application",
     "student_id",
     "admission_no"
    ]
   },
   "AdmissionPipelineClass": {
    "type": "object",
    "properties": {
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_name": {
      "type": "string"
     },
     "seats": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Null when no limit is set."
     },
     "taken": {
      "type": "integer",
      "description": "Offered, accepted and enrolled."
     },
     "counts": {
      "type": "object",
      "additionalProperties": {
       "type": "integer"
      },
      "description": "Applications at each step, by status."
     }
    },
    "required": [
     "class_id",
     "class_name",
     "taken",
     "counts"
    ]
   },
   "AdmissionSeats": {
    "type": "object",
    "properties": {
     "academic_year_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "seats": {
      "type": "integer",
      "minimum": 0,
      "maximum": 2000
     }
    },
    "required": [
     "academic_year_id",
     "class_id",
     "seats"
    ]
   },
   "AdmissionOpen": {
    "type": "object",
    "properties": {
     "school_name": {
      "type": "string"
     },
     "school_code": {
      "type": "string"
     },
     "options": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "academic_year_id": {
         "type": "string",
         "format": "uuid"
        },
        "academic_year": {
         "type": "string"
        },
        "class_id": {
         "type": "string",
         "format": "uuid"
        },
        "class_name": {
         "type": "string"
        },
        "min_age": {
         "type": [
          "integer",
          "null"
         ]
        },
        "max_age": {
         "type": [
          "integer",
          "null"
         ]
        }
       },
       "required": [
        "academic_year_id",
        "academic_year",
        "class_id",
        "class_name"
       ]
      }
     }
    },
    "required": [
     "school_name",
     "school_code",
     "options"
    ]
   },
   "MyEmail": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "type": "string",
      "example": "cover_assigned"
     },
     "what": {
      "type": "string",
      "description": "Plain words: what the email was about, for example Cover for an absent teacher."
     },
     "school_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "sent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "status": {
      "type": "string",
      "enum": [
       "sent",
       "on_the_way",
       "not_sent",
       "failed"
      ],
      "description": "sent: handed to the mail service. on_the_way: waiting for the next run, within a minute or two. not_sent: held back, see reason. failed: the mail service did not take it after 5 tries."
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "description": "Plain words: why it was not sent and what to do."
     },
     "can_resend": {
      "type": "boolean",
      "description": "True when POST /me/emails/{id}/resend would put it back in the queue."
     }
    },
    "required": [
     "id",
     "kind",
     "what",
     "created_at",
     "status",
     "can_resend"
    ]
   },
   "WelcomeLetter": {
    "type": "object",
    "properties": {
     "subject": {
      "type": "string",
      "maxLength": 120
     },
     "kicker": {
      "type": "string",
      "description": "A short line above the headline.",
      "maxLength": 80
     },
     "headline": {
      "type": "string",
      "maxLength": 140
     },
     "headline_em": {
      "type": "string",
      "description": "The end of the headline, set in italics.",
      "maxLength": 60
     },
     "lede": {
      "type": "string",
      "description": "The opening paragraph.",
      "maxLength": 700
     },
     "body": {
      "type": "string",
      "description": "A second paragraph.",
      "maxLength": 700
     },
     "studies": {
      "type": "string",
      "description": "Under the picture: where and what they study, for example Lahore Garrison University · BS Computer Science · First year.",
      "maxLength": 140
     },
     "local_line": {
      "type": "string",
      "description": "One line in the person's own language, near the top.",
      "maxLength": 240
     },
     "local_lang": {
      "type": "string",
      "description": "Its language, for example ur or lg.",
      "pattern": "^[a-z]{2,3}$"
     },
     "local_translation": {
      "type": "string",
      "description": "The line in English, shown small under it.",
      "maxLength": 240
     },
     "details": {
      "type": "string",
      "description": "What to check in their details.",
      "maxLength": 320
     },
     "guardian_line": {
      "type": "string",
      "maxLength": 200
     },
     "finish_when": {
      "type": "string",
      "maxLength": 40
     },
     "finish_title": {
      "type": "string",
      "maxLength": 120
     },
     "finish_line": {
      "type": "string",
      "maxLength": 240
     },
     "wish": {
      "type": "string",
      "description": "The closing wish above the signature.",
      "maxLength": 160
     },
     "sign_title": {
      "type": "string",
      "description": "Under the sender's name, for example Your incharge.",
      "maxLength": 60
     },
     "picture": {
      "type": "string",
      "enum": [
       "sunrise",
       "uganda"
      ],
      "description": "The picture at the top."
     },
     "picture_image_upload_id": {
      "type": "string",
      "description": "Your own picture at the top instead (wide, about 2 to 1), uploaded with POST /admin/branding/uploads (kind letter_image).",
      "maxLength": 64
     },
     "flag": {
      "type": "string",
      "enum": [
       "ug"
      ],
      "description": "A thin flag band above the picture."
     },
     "unit": {
      "type": "string",
      "enum": [
       "lecture",
       "class"
      ],
      "description": "University (lecture) or school and college (class) wording."
     },
     "google": {
      "type": "boolean",
      "description": "True when the address is a Google account: the email shows the one-tap Continue with Google steps."
     },
     "translation_lang": {
      "type": "string",
      "description": "The whole letter in another language: its language.",
      "pattern": "^[a-z]{2,3}$"
     },
     "translation_text": {
      "type": "string",
      "description": "The whole letter in that language, as text (for screen readers and the plain version).",
      "maxLength": 6000
     },
     "translation_image_upload_id": {
      "type": "string",
      "description": "The same text set as a picture, uploaded with POST /admin/branding/uploads (kind letter_image).",
      "maxLength": 64
     }
    }
   },
   "PersonEmail": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "type": "string",
      "example": "role_welcome"
     },
     "what": {
      "type": "string",
      "example": "Welcome to a new role"
     },
     "subject": {
      "type": [
       "string",
       "null"
      ]
     },
     "to_email": {
      "type": [
       "string",
       "null"
      ]
     },
     "cc": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "has_letter": {
      "type": "boolean"
     },
     "status": {
      "type": "string",
      "enum": [
       "sent",
       "failed",
       "not_sent",
       "on_the_way"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "sent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "opened_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When it was first opened (its pictures loaded). Some mail apps load pictures before the person reads, so signed_in is the surer sign."
     },
     "open_count": {
      "type": "integer"
     },
     "signed_in": {
      "type": "boolean",
      "description": "The person has signed in to saMas School at least once."
     },
     "has_copy": {
      "type": "boolean",
      "description": "The exact email is kept; read it with GET /admin/people/{id}/emails/{email_id}."
     },
     "html": {
      "type": [
       "string",
       "null"
      ],
      "description": "The email exactly as sent (only on the single read)."
     }
    },
    "required": [
     "id",
     "kind",
     "what",
     "status",
     "created_at",
     "open_count",
     "signed_in",
     "has_copy"
    ]
   },
   "HelpDailyReport": {
    "type": "object",
    "properties": {
     "since": {
      "type": "string",
      "format": "date-time"
     },
     "until": {
      "type": "string",
      "format": "date-time"
     },
     "totals": {
      "type": "object",
      "properties": {
       "questions": {
        "type": "integer"
       },
       "people": {
        "type": "integer"
       },
       "yes": {
        "type": "integer"
       },
       "not_yet": {
        "type": "integer"
       },
       "no_vote": {
        "type": "integer"
       },
       "fallbacks": {
        "type": "integer"
       },
       "no_guide": {
        "type": "integer"
       },
       "handoffs": {
        "type": "integer"
       },
       "marked_wrong": {
        "type": "integer"
       },
       "cost_usd": {
        "type": "number"
       },
       "unblocked_pct": {
        "type": [
         "number",
         "null"
        ]
       },
       "fallback_pct": {
        "type": [
         "number",
         "null"
        ]
       }
      }
     },
     "week": {
      "type": "object",
      "properties": {
       "questions_per_day": {
        "type": [
         "number",
         "null"
        ]
       },
       "unblocked_pct": {
        "type": [
         "number",
         "null"
        ]
       },
       "fallback_pct": {
        "type": [
         "number",
         "null"
        ]
       }
      }
     },
     "schools": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "school_name": {
         "type": "string"
        },
        "school_code": {
         "type": [
          "string",
          "null"
         ]
        },
        "questions": {
         "type": "integer"
        },
        "people": {
         "type": "integer"
        },
        "yes": {
         "type": "integer"
        },
        "not_yet": {
         "type": "integer"
        },
        "fallbacks": {
         "type": "integer"
        },
        "handoffs": {
         "type": "integer"
        },
        "cost_usd": {
         "type": "number"
        }
       }
      }
     },
     "attention": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "asked_at": {
         "type": "string",
         "format": "date-time"
        },
        "school_name": {
         "type": "string"
        },
        "role": {
         "type": [
          "string",
          "null"
         ]
        },
        "screen": {
         "type": [
          "string",
          "null"
         ]
        },
        "question": {
         "type": [
          "string",
          "null"
         ]
        },
        "answer_shown": {
         "type": [
          "string",
          "null"
         ]
        },
        "vote": {
         "type": [
          "string",
          "null"
         ],
         "enum": [
          "yes",
          "not_yet",
          null
         ]
        },
        "fallback": {
         "type": "boolean"
        },
        "no_guide": {
         "type": "boolean"
        },
        "handed_off": {
         "type": "boolean"
        },
        "marked_wrong": {
         "type": "boolean"
        },
        "fallback_reason": {
         "type": [
          "string",
          "null"
         ]
        },
        "second_try": {
         "oneOf": [
          {
           "type": "object",
           "properties": {
            "passed": {
             "type": "boolean"
            },
            "answer": {
             "type": [
              "string",
              "null"
             ]
            },
            "model": {
             "type": [
              "string",
              "null"
             ]
            },
            "needs_data": {
             "type": "boolean"
            },
            "reason": {
             "type": [
              "string",
              "null"
             ]
            }
           }
          },
          {
           "type": "null"
          }
         ]
        }
       }
      }
     },
     "helped": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "asked_at": {
         "type": "string",
         "format": "date-time"
        },
        "school_name": {
         "type": "string"
        },
        "role": {
         "type": [
          "string",
          "null"
         ]
        },
        "question": {
         "type": [
          "string",
          "null"
         ]
        }
       }
      }
     },
     "second_tries": {
      "type": "object",
      "properties": {
       "tried": {
        "type": "integer"
       },
       "passed": {
        "type": "integer"
       },
       "cost_usd": {
        "type": "number"
       }
      }
     },
     "eval": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "status": {
          "type": "string"
         },
         "note": {
          "type": [
           "string",
           "null"
          ]
         },
         "ran_at": {
          "type": "string",
          "format": "date-time"
         }
        }
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "since",
     "until",
     "totals",
     "schools",
     "attention"
    ]
   },
   "MyEmails": {
    "type": "object",
    "properties": {
     "email": {
      "type": [
       "string",
       "null"
      ],
      "description": "The address on your profile, where every email goes. Null when there is none."
     },
     "items": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/MyEmail"
      }
     }
    },
    "required": [
     "email",
     "items"
    ]
   },
   "MyApplication": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "application_no": {
      "type": "string"
     },
     "school_name": {
      "type": "string"
     },
     "child_name": {
      "type": "string"
     },
     "class_name": {
      "type": "string"
     },
     "academic_year": {
      "type": "string"
     },
     "status": {
      "$ref": "#/components/schemas/AdmissionStatus"
     },
     "test_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "decision_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "application_no",
     "school_name",
     "child_name",
     "class_name",
     "academic_year",
     "status",
     "created_at"
    ]
   },
   "ApplyWrite": {
    "type": "object",
    "properties": {
     "school": {
      "type": "string",
      "description": "The school's address name (primary) or code.",
      "maxLength": 40
     },
     "academic_year_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "child_name": {
      "type": "string",
      "pattern": "^[A-Za-z'\\- ]{2,80}$",
      "maxLength": 80
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "male",
       "female",
       "other",
       null
      ]
     },
     "previous_school": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "guardians": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/AdmissionGuardian"
      },
      "minItems": 1,
      "maxItems": 3
     },
     "contact_phone": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^\\+[1-9][0-9]{6,14}$",
      "example": "+923001234567"
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     }
    },
    "required": [
     "school",
     "academic_year_id",
     "class_id",
     "child_name",
     "date_of_birth",
     "guardians"
    ]
   },
   "FileUpload": {
    "type": "object",
    "properties": {
     "purpose": {
      "type": "string",
      "enum": [
       "admission",
       "staff",
       "leave"
      ],
      "description": "admission and staff documents are for the office; leave documents for the person asking."
     },
     "file_name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "content_type": {
      "type": "string",
      "enum": [
       "image/jpeg",
       "image/png",
       "image/webp",
       "application/pdf",
       "application/msword",
       "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
       "application/vnd.ms-excel",
       "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
       "application/vnd.ms-powerpoint",
       "application/vnd.openxmlformats-officedocument.presentationml.presentation"
      ]
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 20971520
     }
    },
    "required": [
     "purpose",
     "file_name",
     "content_type",
     "size_bytes"
    ]
   },
   "StaffMember": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "role": {
      "$ref": "#/components/schemas/Role"
     },
     "is_active": {
      "type": "boolean"
     },
     "designation": {
      "type": [
       "string",
       "null"
      ]
     },
     "department": {
      "type": [
       "string",
       "null"
      ]
     },
     "employment_type": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "full_time",
       "part_time",
       "contract",
       "visiting",
       null
      ]
     },
     "joined_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "left_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "signed_in": {
      "type": "boolean",
      "description": "False while the person is invited and has not signed in yet, as on GET /admin/people."
     }
    },
    "required": [
     "user_id",
     "role",
     "is_active"
    ]
   },
   "StaffRecord": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "role": {
      "$ref": "#/components/schemas/Role"
     },
     "is_active": {
      "type": "boolean"
     },
     "designation": {
      "type": [
       "string",
       "null"
      ]
     },
     "department": {
      "type": [
       "string",
       "null"
      ]
     },
     "employment_type": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "full_time",
       "part_time",
       "contract",
       "visiting",
       null
      ]
     },
     "joined_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "left_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "leaving_reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "qualifications": {
      "type": [
       "string",
       "null"
      ]
     },
     "emergency_contact_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "emergency_contact_phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "documents": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "updated_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "updated_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "user_id",
     "role",
     "is_active",
     "documents"
    ]
   },
   "StaffRecordWrite": {
    "type": "object",
    "properties": {
     "designation": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "department": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80
     },
     "employment_type": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "full_time",
       "part_time",
       "contract",
       "visiting",
       null
      ]
     },
     "joined_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "left_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "The account switches off on this day. Needs leaving_reason."
     },
     "leaving_reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300
     },
     "qualifications": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000
     },
     "emergency_contact_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 100
     },
     "emergency_contact_phone": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^\\+[1-9][0-9]{6,14}$",
      "example": "+923001234567"
     },
     "documents": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 10
     }
    }
   },
   "LeaveType": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "example": "Casual"
     },
     "yearly_days": {
      "type": "number",
      "example": 10
     },
     "carry_over": {
      "type": "boolean"
     },
     "needs_document": {
      "type": "boolean",
      "description": "More than 2 days needs a document."
     },
     "is_active": {
      "type": "boolean"
     },
     "sort_order": {
      "type": "integer"
     }
    },
    "required": [
     "id",
     "name",
     "yearly_days",
     "carry_over",
     "needs_document",
     "is_active"
    ]
   },
   "LeaveTypeWrite": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 2,
      "maxLength": 40
     },
     "yearly_days": {
      "type": "number",
      "minimum": 0,
      "maximum": 365,
      "multipleOf": 0.5
     },
     "carry_over": {
      "type": "boolean"
     },
     "needs_document": {
      "type": "boolean"
     },
     "is_active": {
      "type": "boolean"
     },
     "sort_order": {
      "type": "integer",
      "minimum": 0,
      "maximum": 100
     }
    }
   },
   "LeaveBalance": {
    "type": "object",
    "properties": {
     "leave_type_id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string"
     },
     "allowance": {
      "type": "number"
     },
     "carried_over": {
      "type": "number"
     },
     "taken": {
      "type": "number"
     },
     "pending": {
      "type": "number"
     },
     "left": {
      "type": "number"
     }
    },
    "required": [
     "leave_type_id",
     "name",
     "allowance",
     "taken",
     "pending",
     "left"
    ]
   },
   "LeaveRequest": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "person": {
      "$ref": "#/components/schemas/PersonRef"
     },
     "leave_type": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "half_day": {
      "type": "boolean"
     },
     "days": {
      "type": "number",
      "description": "Working days (Monday to Saturday, school holidays left out)."
     },
     "reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "documents": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "approved",
       "rejected",
       "cancelled"
      ]
     },
     "decided_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "decided_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "decision_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "person",
     "leave_type",
     "starts_on",
     "ends_on",
     "half_day",
     "days",
     "documents",
     "status",
     "created_at"
    ]
   },
   "LeaveRequestWrite": {
    "type": "object",
    "properties": {
     "leave_type_id": {
      "type": "string",
      "format": "uuid"
     },
     "starts_on": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "ends_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "half_day": {
      "type": "boolean"
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500
     },
     "documents": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     }
    },
    "required": [
     "leave_type_id",
     "starts_on"
    ]
   },
   "LeaveClash": {
    "type": "object",
    "properties": {
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "period_no": {
      "type": "integer"
     },
     "class_name": {
      "type": "string"
     },
     "section_name": {
      "type": "string"
     },
     "subject": {
      "type": "string"
     }
    },
    "required": [
     "on_date",
     "period_no",
     "class_name",
     "section_name",
     "subject"
    ]
   },
   "MyLeave": {
    "type": "object",
    "properties": {
     "balances": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LeaveBalance"
      }
     },
     "requests": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LeaveRequest"
      }
     },
     "year": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "starts_on": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01"
         },
         "ends_on": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01"
         }
        },
        "required": [
         "name",
         "starts_on",
         "ends_on"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The leave year (the academic year, or the calendar year when the school has none)."
     }
    },
    "required": [
     "balances",
     "requests"
    ]
   },
   "AdmissionCreate": {
    "type": "object",
    "properties": {
     "academic_year_id": {
      "type": "string",
      "format": "uuid"
     },
     "class_id": {
      "type": "string",
      "format": "uuid"
     },
     "child_name": {
      "type": "string",
      "pattern": "^[A-Za-z'\\- ]{2,80}$",
      "maxLength": 80
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "male",
       "female",
       "other",
       null
      ]
     },
     "previous_school": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "guardians": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/AdmissionGuardian"
      },
      "minItems": 1,
      "maxItems": 3
     },
     "contact_email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "maxLength": 200
     },
     "contact_phone": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^\\+[1-9][0-9]{6,14}$",
      "example": "+923001234567"
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     },
     "documents": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 6
     }
    },
    "required": [
     "academic_year_id",
     "class_id",
     "child_name",
     "date_of_birth",
     "guardians"
    ]
   },
   "LeaveTypeCreate": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 2,
      "maxLength": 40
     },
     "yearly_days": {
      "type": "number",
      "minimum": 0,
      "maximum": 365,
      "multipleOf": 0.5
     },
     "carry_over": {
      "type": "boolean"
     },
     "needs_document": {
      "type": "boolean"
     },
     "is_active": {
      "type": "boolean"
     },
     "sort_order": {
      "type": "integer",
      "minimum": 0,
      "maximum": 100
     }
    },
    "required": [
     "name",
     "yearly_days"
    ]
   },
   "AssignmentCreate": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "instructions": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 4000
     },
     "due_at": {
      "type": "string",
      "description": "In the future, within a year.",
      "format": "date-time"
     },
     "max_marks": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
     },
     "allow_late": {
      "type": "boolean"
     },
     "counts_to_grade": {
      "type": "boolean"
     },
     "attachments": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     },
     "set": {
      "type": "boolean",
      "description": "true sets it for the class (families are told); leave out to keep a draft."
     },
     "section_subject_id": {
      "type": "string",
      "format": "uuid"
     }
    },
    "required": [
     "section_subject_id",
     "title",
     "due_at",
     "max_marks"
    ]
   },
   "ImportKind": {
    "type": "string",
    "enum": [
     "students",
     "staff"
    ],
    "description": "students loads students with their guardians (the office); staff invites staff and fills their records (school admin only)."
   },
   "ImportRow": {
    "type": "object",
    "description": "One row of the spreadsheet, keyed by the template's column names (the web app maps a file's headers such as Admission No, DOB or Father name onto them). Every value is text as typed; blank cells may be left out. row is the line in the file, for the messages.",
    "properties": {
     "row": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 1000000
     },
     "address": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "admission_date": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "admission_no": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "class": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "date_of_birth": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "department": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "designation": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "emergency_contact_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "emergency_contact_phone": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "employment_type": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian1_email": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian1_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian1_phone": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian1_relation": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian2_email": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian2_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian2_phone": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "guardian2_relation": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "joined_on": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "phone": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "previous_school": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "role": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "roll_no": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "section": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     }
    }
   },
   "ImportOptions": {
    "type": "object",
    "properties": {
     "create_missing_classes": {
      "type": "boolean",
      "description": "Make classes and sections the file names that the school does not have yet (default true)."
     },
     "skip_error_rows": {
      "type": "boolean",
      "description": "Load the good rows and report the bad ones (default false: any error refuses the whole file with IMPORT_HAS_ERRORS)."
     },
     "send_welcome": {
      "type": "boolean",
      "description": "Email each person the import adds or gives a role here a welcome from the school (default true). The preview counts them in summary.welcome_emails."
     }
    }
   },
   "ImportPreviewWrite": {
    "type": "object",
    "properties": {
     "kind": {
      "$ref": "#/components/schemas/ImportKind"
     },
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ImportRow"
      },
      "minItems": 1,
      "maxItems": 2000,
      "x-too-many-code": "IMPORT_TOO_LARGE",
      "description": "Up to 2000 rows (IMPORT_TOO_LARGE)."
     },
     "options": {
      "$ref": "#/components/schemas/ImportOptions"
     }
    },
    "required": [
     "kind",
     "rows"
    ]
   },
   "ImportWrite": {
    "type": "object",
    "properties": {
     "kind": {
      "$ref": "#/components/schemas/ImportKind"
     },
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ImportRow"
      },
      "minItems": 1,
      "maxItems": 2000,
      "x-too-many-code": "IMPORT_TOO_LARGE",
      "description": "Up to 2000 rows (IMPORT_TOO_LARGE)."
     },
     "options": {
      "$ref": "#/components/schemas/ImportOptions"
     },
     "file_name": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "description": "The file's name, kept with the record."
     }
    },
    "required": [
     "kind",
     "rows"
    ]
   },
   "ImportRowResult": {
    "type": "object",
    "properties": {
     "row": {
      "type": "integer",
      "description": "The line in the file (the header is line 1)."
     },
     "status": {
      "type": "string",
      "enum": [
       "ok",
       "warning",
       "error",
       "exists"
      ],
      "description": "ok loads; warning loads with something to look at; error does not load; exists is already in the school and is skipped, never changed."
     },
     "key": {
      "type": [
       "string",
       "null"
      ],
      "description": "The admission number, or the staff email or phone."
     },
     "name": {
      "type": [
       "string",
       "null"
      ]
     },
     "messages": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "Plain words: what is wrong, or what will happen."
      }
     },
     "outcome": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "created",
       "skipped",
       "failed",
       null
      ],
      "description": "After a run: what happened to the row."
     }
    },
    "required": [
     "row",
     "status",
     "messages"
    ]
   },
   "ImportSummary": {
    "type": "object",
    "properties": {
     "rows": {
      "type": "integer"
     },
     "ok": {
      "type": "integer"
     },
     "warnings": {
      "type": "integer"
     },
     "errors": {
      "type": "integer"
     },
     "exists": {
      "type": "integer"
     },
     "students_new": {
      "type": "integer"
     },
     "students_existing": {
      "type": "integer"
     },
     "guardians_matched": {
      "type": "integer",
      "description": "Guardians that are parent accounts already at the school."
     },
     "guardians_created": {
      "type": "integer",
      "description": "Parent accounts the import makes (siblings sharing a phone or email share one)."
     },
     "guardians_not_linked": {
      "type": "integer",
      "description": "Guardian emails that belong to staff or to another school, left for the office to link by hand."
     },
     "classes_to_create": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "sections_to_create": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "staff_to_invite": {
      "type": "integer"
     },
     "staff_existing": {
      "type": "integer"
     },
     "welcome_emails": {
      "type": "integer",
      "description": "Welcome emails a run with these options would send (people with an email who get a role here for the first time). Previews only."
     }
    },
    "required": [
     "rows",
     "ok",
     "warnings",
     "errors",
     "exists",
     "students_new",
     "students_existing",
     "guardians_matched",
     "guardians_created",
     "guardians_not_linked",
     "classes_to_create",
     "sections_to_create",
     "staff_to_invite",
     "staff_existing"
    ]
   },
   "ImportPreview": {
    "type": "object",
    "properties": {
     "kind": {
      "$ref": "#/components/schemas/ImportKind"
     },
     "summary": {
      "$ref": "#/components/schemas/ImportSummary"
     },
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ImportRowResult"
      }
     }
    },
    "required": [
     "kind",
     "summary",
     "rows"
    ]
   },
   "Import": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "$ref": "#/components/schemas/ImportKind"
     },
     "file_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "row_count": {
      "type": "integer"
     },
     "created_count": {
      "type": "integer"
     },
     "updated_count": {
      "type": "integer"
     },
     "skipped_count": {
      "type": "integer"
     },
     "error_count": {
      "type": "integer"
     },
     "options": {
      "$ref": "#/components/schemas/ImportOptions"
     },
     "created_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "kind",
     "row_count",
     "created_count",
     "updated_count",
     "skipped_count",
     "error_count",
     "created_at"
    ]
   },
   "ImportResult": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "$ref": "#/components/schemas/ImportKind"
     },
     "file_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "row_count": {
      "type": "integer"
     },
     "created_count": {
      "type": "integer"
     },
     "updated_count": {
      "type": "integer"
     },
     "skipped_count": {
      "type": "integer"
     },
     "error_count": {
      "type": "integer"
     },
     "options": {
      "$ref": "#/components/schemas/ImportOptions"
     },
     "created_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "summary": {
      "$ref": "#/components/schemas/ImportSummary"
     },
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ImportRowResult"
      }
     },
     "truncated": {
      "type": "boolean",
      "description": "True when only the first 1000 row outcomes are kept."
     }
    },
    "required": [
     "id",
     "kind",
     "row_count",
     "created_count",
     "updated_count",
     "skipped_count",
     "error_count",
     "created_at",
     "summary",
     "rows"
    ]
   },
   "MessagePerson": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "role": {
      "type": "string",
      "example": "teacher"
     },
     "detail": {
      "type": [
       "string",
       "null"
      ],
      "description": "What they teach, or their relation to the child."
     },
     "in_thread_as": {
      "type": "string",
      "enum": [
       "staff",
       "office",
       "guardian",
       "student"
      ]
     }
    },
    "required": [
     "user_id",
     "full_name",
     "role"
    ]
   },
   "MessageContacts": {
    "type": "object",
    "properties": {
     "children": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "student_id": {
         "type": "string",
         "format": "uuid"
        },
        "full_name": {
         "type": "string"
        },
        "class_label": {
         "type": "string",
         "example": "Grade 5 A"
        },
        "student_can_write": {
         "type": "boolean",
         "description": "The school lets this child use messages."
        },
        "people": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/MessagePerson"
         }
        }
       },
       "required": [
        "student_id",
        "full_name",
        "class_label",
        "people"
       ]
      }
     },
     "staff": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/MessagePerson"
      }
     }
    },
    "required": [
     "children",
     "staff"
    ],
    "description": "Who the caller can start a conversation with. Parents: each child's teachers and the office. Students: their teachers. Teachers: the families of children they teach and other staff. The office: any family (search with q) and any staff."
   },
   "ThreadSummary": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "subject": {
      "type": "string"
     },
     "includes_student": {
      "type": "boolean"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_message_at": {
      "type": "string",
      "format": "date-time"
     },
     "student": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "full_name"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The child the conversation is about; null for staff conversations."
     },
     "people": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/MessagePerson"
      }
     },
     "last_message": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "sender": {
          "$ref": "#/components/schemas/MessagePerson"
         },
         "created_at": {
          "type": "string",
          "format": "date-time"
         },
         "preview": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 140
         },
         "removed": {
          "type": "boolean"
         },
         "files": {
          "type": "integer"
         }
        },
        "required": [
         "sender",
         "created_at",
         "removed"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "unread": {
      "type": "integer"
     },
     "open_reports": {
      "type": "integer",
      "description": "Reports not yet dealt with (the office acts on these)."
     }
    },
    "required": [
     "id",
     "subject",
     "people",
     "unread",
     "last_message_at"
    ]
   },
   "MessageReport": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "reporter": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "reason": {
      "type": "string"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "resolved": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "reporter",
     "reason",
     "created_at",
     "resolved"
    ]
   },
   "Message": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "sender": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "mine": {
      "type": "boolean"
     },
     "body": {
      "type": [
       "string",
       "null"
      ],
      "description": "Null when removed, except for the office."
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "deleted": {
      "type": "boolean",
      "description": "Deleted by the sender."
     },
     "hidden": {
      "type": "boolean",
      "description": "Removed by the school."
     },
     "hidden_reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "can_delete": {
      "type": "boolean",
      "description": "The sender, within 5 minutes."
     },
     "reports": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/MessageReport"
      }
     }
    },
    "required": [
     "id",
     "sender",
     "created_at",
     "mine",
     "files",
     "deleted",
     "hidden",
     "can_delete"
    ]
   },
   "ThreadDetail": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "subject": {
      "type": "string"
     },
     "includes_student": {
      "type": "boolean"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_message_at": {
      "type": "string",
      "format": "date-time"
     },
     "student": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "full_name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "people": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/MessagePerson"
      }
     },
     "last_message": {
      "oneOf": [
       {
        "type": "object"
       },
       {
        "type": "null"
       }
      ]
     },
     "unread": {
      "type": "integer"
     },
     "open_reports": {
      "type": "integer"
     },
     "access": {
      "type": "string",
      "enum": [
       "member",
       "office"
      ],
      "description": "office: the principal or school admin reading a conversation they are not in (logged)."
     },
     "can_reply": {
      "type": "boolean"
     },
     "messages": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Message"
      }
     }
    },
    "required": [
     "id",
     "subject",
     "people",
     "access",
     "can_reply",
     "messages"
    ]
   },
   "ThreadStart": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The child it is about. Leave out for a staff conversation."
     },
     "to": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "minItems": 1,
      "maxItems": 10,
      "description": "From the contacts list. The child's parents are always added to conversations about the child."
     },
     "subject": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120
     },
     "body": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     }
    },
    "required": [
     "to",
     "subject"
    ]
   },
   "MessageWrite": {
    "type": "object",
    "properties": {
     "body": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     }
    }
   },
   "MessagingPause": {
    "type": "object",
    "properties": {
     "person": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "reason": {
      "type": "string"
     },
     "paused_by": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "person",
     "reason",
     "created_at"
    ]
   },
   "ThreadRead": {
    "type": "object",
    "properties": {
     "actor": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "thread": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "subject": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "subject"
      ]
     },
     "student": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "full_name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "full_name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "read_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "actor",
     "thread",
     "read_at"
    ]
   },
   "CommSettings": {
    "type": "object",
    "properties": {
     "students_can_message": {
      "type": "boolean"
     },
     "students_min_class_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Students write from this class up; null for every class."
     },
     "quiet_start": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "example": "20:00"
     },
     "quiet_end": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "example": "07:00"
     },
     "blocked_words": {
      "type": "array",
      "items": {
       "type": "string",
       "minLength": 2,
       "maxLength": 40
      },
      "maxItems": 200
     },
     "slips_office_only": {
      "type": "boolean",
      "description": "true: only the principal and school admin send permission slips."
     }
    }
   },
   "GettingStartedItem": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "example": "timetable"
     },
     "title": {
      "type": "string"
     },
     "minutes": {
      "type": "integer",
      "description": "About how long it takes."
     },
     "scope": {
      "type": "string",
      "enum": [
       "you",
       "school"
      ],
      "description": "school: done by anyone in the school counts for everyone."
     },
     "role": {
      "type": "string",
      "description": "The role this item comes from (principal, school_admin, teacher or finance)."
     },
     "priority": {
      "type": "string",
      "enum": [
       "H",
       "M",
       "L"
      ],
      "description": "H before the first day of term, M in the first month, L when there is time."
     },
     "stop": {
      "type": "integer",
      "minimum": 0,
      "maximum": 4,
      "description": "Its stop on the road (info.stops)."
     },
     "check": {
      "type": "string",
      "enum": [
       "app",
       "you"
      ],
      "description": "app: ticked by itself when the app sees it done; you: the person ticks it."
     },
     "skippable": {
      "type": "boolean",
      "description": "Can be marked Not for us, with a reason."
     },
     "state": {
      "type": "string",
      "enum": [
       "todo",
       "app",
       "done",
       "again",
       "skipped",
       "na"
      ],
      "description": "todo; app (the app saw it done); done (ticked); again (was done, now not true); skipped (Not for us); na (turned off by a school setting)."
     },
     "at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "by": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "user_id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": [
           "string",
           "null"
          ]
         },
         "is_me": {
          "type": "boolean"
         }
        },
        "required": [
         "user_id",
         "name",
         "is_me"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "Who ticked or skipped it. Null when the app did."
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "description": "Why it was skipped, or why it does not apply."
     },
     "help": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "issue_id": {
          "type": "string",
          "format": "uuid"
         },
         "ref": {
          "type": [
           "string",
           "null"
          ],
          "example": "PRB-2026-0042"
         },
         "at": {
          "type": "string",
          "format": "date-time"
         }
        },
        "required": [
         "issue_id",
         "ref",
         "at"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "Set while a problem report sent from this item (Having trouble?) is open: the item needs help."
     },
     "do_route": {
      "type": [
       "string",
       "null"
      ],
      "description": "The screen where it is done."
     },
     "do_anchor": {
      "type": [
       "string",
       "null"
      ],
      "description": "The thing to press there (a help anchor)."
     },
     "guide": {
      "type": [
       "string",
       "null"
      ],
      "description": "The slug of the guide for it."
     }
    },
    "required": [
     "key",
     "title",
     "minutes",
     "scope",
     "role",
     "priority",
     "stop",
     "check",
     "skippable",
     "state",
     "at",
     "by",
     "reason",
     "help",
     "do_route",
     "do_anchor",
     "guide"
    ]
   },
   "GettingStartedProgress": {
    "type": "object",
    "properties": {
     "done": {
      "type": "integer"
     },
     "total": {
      "type": "integer",
      "description": "Skipped items and items that do not apply are left out."
     },
     "high_done": {
      "type": "integer"
     },
     "high_total": {
      "type": "integer"
     }
    },
    "required": [
     "done",
     "total",
     "high_done",
     "high_total"
    ]
   },
   "GettingStarted": {
    "type": "object",
    "properties": {
     "roles": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "role": {
      "type": [
       "string",
       "null"
      ],
      "description": "The role whose road is shown (the most senior one). Null for people with no checklist, such as parents."
     },
     "preparing_for": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Roads whose school-wide items are on this list on behalf of others: the finance in-charge prepares the school, so she sees the principal's, the school admin's and the teachers' school items and may tick or skip them. Empty for everyone else. Since 2026-10-15."
     },
     "info": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "lead": {
          "type": "string"
         },
         "goal": {
          "type": "string"
         },
         "art": {
          "type": "string"
         },
         "stops": {
          "type": "array",
          "items": {
           "type": "string"
          }
         }
        },
        "required": [
         "title",
         "lead",
         "goal",
         "art",
         "stops"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "items": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/GettingStartedItem"
      }
     },
     "progress": {
      "$ref": "#/components/schemas/GettingStartedProgress"
     },
     "seen_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "hidden_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "roles",
     "role",
     "info",
     "items",
     "progress",
     "seen_at",
     "hidden_at"
    ]
   },
   "TeamProgress": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Null for someone invited who has not signed in yet."
     },
     "name": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "roles": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "signed_in": {
      "type": "boolean"
     },
     "invited_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "progress": {
      "$ref": "#/components/schemas/GettingStartedProgress"
     },
     "now_at": {
      "type": [
       "string",
       "null"
      ],
      "description": "The first stop with something left to do."
     },
     "needs_attention": {
      "type": "integer",
      "description": "Items that were done and stopped being true."
     },
     "hidden": {
      "type": "boolean"
     },
     "last_progress_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "user_id",
     "name",
     "email",
     "roles",
     "signed_in",
     "invited_at",
     "progress",
     "now_at",
     "needs_attention",
     "hidden",
     "last_progress_at"
    ]
   },
   "HelpEvent": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "panel_opened",
       "guide_opened",
       "helpful",
       "not_helpful",
       "broken",
       "search_empty",
       "tour_abandoned",
       "report_opened",
       "stuck_offered",
       "stuck_accepted",
       "stuck_dismissed",
       "walk_abandoned",
       "ask_repeat_refusal",
       "save",
       "easy",
       "not_easy"
      ],
      "description": "What happened. report_opened: the Report a problem form was opened (screen says from where). The buddy's own (TDD 6A.6, 6A.5c): stuck_offered, stuck_accepted and stuck_dismissed for the stuck bubble, walk_abandoned when a Do it walk was left without an answer to Did this unblock you, ask_repeat_refusal when the same refusal came back three times after an answer; screen says where. The form system's own (PRD 5.12, TDD 6E.3): save when a form was saved (screen says which; the office home counts them), easy and not_easy for Was this easy? after the first saves on a reshaped screen."
     },
     "guide": {
      "type": "string",
      "description": "The guide's slug. Needed for guide_opened, helpful, not_helpful and tour_abandoned.",
      "pattern": "^[a-z0-9-]{2,60}$",
      "example": "post-an-announcement"
     },
     "screen": {
      "type": "string",
      "description": "The screen key. Needed for panel_opened, save, easy and not_easy.",
      "pattern": "^[a-z0-9-]{2,60}$",
      "example": "admin-timetable"
     },
     "anchor": {
      "type": "string",
      "description": "The anchor a guide or tour could not find on the screen. Needed for broken.",
      "pattern": "^[a-z0-9]+(-[a-z0-9]+)*(\\.[a-z0-9]+(-[a-z0-9]+)*)+$",
      "example": "admin-timetable.publish"
     },
     "query": {
      "type": "string",
      "description": "The words of a search that found nothing. Needed for search_empty.",
      "minLength": 1,
      "maxLength": 80
     }
    },
    "required": [
     "kind"
    ]
   },
   "HelpHealth": {
    "type": "object",
    "properties": {
     "since": {
      "type": "string",
      "format": "date-time"
     },
     "totals": {
      "type": "object",
      "additionalProperties": {
       "type": "integer"
      },
      "description": "Events by kind."
     },
     "schools": {
      "type": "integer",
      "description": "Schools with any help event in the period."
     },
     "guides": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "guide": {
         "type": "string"
        },
        "opened": {
         "type": "integer"
        },
        "helpful": {
         "type": "integer"
        },
        "not_helpful": {
         "type": "integer"
        },
        "broken": {
         "type": "integer"
        },
        "tour_abandoned": {
         "type": "integer"
        },
        "last_broken_at": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        },
        "broken_anchors": {
         "type": "array",
         "items": {
          "type": "string"
         }
        }
       },
       "required": [
        "guide",
        "opened",
        "helpful",
        "not_helpful",
        "broken",
        "tour_abandoned",
        "last_broken_at",
        "broken_anchors"
       ]
      }
     },
     "broken_without_guide": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "anchor": {
         "type": "string"
        },
        "screen": {
         "type": [
          "string",
          "null"
         ]
        },
        "count": {
         "type": "integer"
        },
        "last_at": {
         "type": "string",
         "format": "date-time"
        }
       },
       "required": [
        "anchor",
        "screen",
        "count",
        "last_at"
       ]
      }
     },
     "panels": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "screen": {
         "type": "string"
        },
        "opened": {
         "type": "integer"
        }
       },
       "required": [
        "screen",
        "opened"
       ]
      }
     },
     "searches_empty": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "query": {
         "type": "string"
        },
        "count": {
         "type": "integer"
        }
       },
       "required": [
        "query",
        "count"
       ]
      }
     }
    },
    "required": [
     "since",
     "totals",
     "schools",
     "guides",
     "broken_without_guide",
     "panels",
     "searches_empty"
    ]
   },
   "HelpContextId": {
    "type": "string",
    "description": "An id from this build's help graph or the app: a screen key or route, a task id, an error code or a step.",
    "pattern": "^[A-Za-z0-9][A-Za-z0-9._:#/*-]{0,119}$",
    "example": "training.teacher.mark_homework"
   },
   "HelpAsk": {
    "type": "object",
    "properties": {
     "question": {
      "type": "string",
      "description": "What the person asked, in their own words.",
      "minLength": 1,
      "maxLength": 2000,
      "example": "I can't find the Mark button"
     },
     "context": {
      "type": "object",
      "properties": {
       "screen": {
        "type": [
         "string",
         "null"
        ],
        "pattern": "^[A-Za-z0-9][A-Za-z0-9._:#/*-]{0,119}$",
        "maxLength": 120,
        "description": "The screen key the person is on (the same key as its ? panel), or its route."
       },
       "task": {
        "type": [
         "string",
         "null"
        ],
        "pattern": "^[A-Za-z0-9][A-Za-z0-9._:#/*-]{0,119}$",
        "maxLength": 120,
        "description": "The training task or Getting started item the person is on (training.<role>.<key> or gs.<key>)."
       },
       "last_code": {
        "type": [
         "string",
         "null"
        ],
        "pattern": "^[A-Z][A-Z0-9_]{1,39}$",
        "maxLength": 40,
        "description": "The last API error code the app hit on this screen."
       },
       "code_count_today": {
        "type": "integer",
        "minimum": 0,
        "maximum": 1000,
        "description": "How many times that code came back today."
       },
       "progress": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/HelpContextId"
        },
        "maxItems": 200,
        "description": "Ids of the tasks already ticked."
       }
      },
      "description": "Where the person is, as ids only. Anything else is refused (UNKNOWN_FIELD)."
     }
    },
    "required": [
     "question"
    ]
   },
   "HelpBlock": {
    "oneOf": [
     {
      "type": "object",
      "properties": {
       "type": {
        "type": "string",
        "const": "text"
       },
       "text": {
        "type": "string",
        "description": "Plain words. A button name is quoted with double stars (**Mark**) and must be a label on a cited step."
       }
      },
      "required": [
       "type",
       "text"
      ]
     },
     {
      "type": "object",
      "properties": {
       "type": {
        "type": "string",
        "const": "table"
       },
       "columns": {
        "type": "array",
        "items": {
         "type": "string"
        }
       },
       "rows": {
        "type": "array",
        "items": {
         "type": "array",
         "items": {
          "type": [
           "string",
           "number",
           "null"
          ]
         }
        }
       },
       "from": {
        "type": "string",
        "description": "The lookup the rows came from."
       }
      },
      "required": [
       "type",
       "columns",
       "rows",
       "from"
      ]
     },
     {
      "type": "object",
      "properties": {
       "type": {
        "type": "string",
        "const": "chart"
       },
       "kind": {
        "type": "string",
        "enum": [
         "bar",
         "line"
        ]
       },
       "title": {
        "type": "string"
       },
       "points": {
        "type": "array",
        "items": {
         "type": "array",
         "items": {
          "type": [
           "string",
           "number"
          ]
         }
        }
       },
       "from": {
        "type": "string",
        "description": "The lookup the points came from."
       }
      },
      "required": [
       "type",
       "kind",
       "title",
       "points",
       "from"
      ]
     },
     {
      "type": "object",
      "properties": {
       "type": {
        "type": "string",
        "const": "picture"
       },
       "guide": {
        "type": "string",
        "description": "A guide slug; the client shows that guide's picture."
       },
       "caption": {
        "type": [
         "string",
         "null"
        ]
       }
      },
      "required": [
       "type",
       "guide",
       "caption"
      ]
     },
     {
      "type": "object",
      "properties": {
       "type": {
        "type": "string",
        "const": "emoji"
       },
       "text": {
        "type": "string"
       }
      },
      "required": [
       "type",
       "text"
      ]
     }
    ],
    "description": "One piece of the answer: text (at most 10 table rows and 12 chart points per answer; every row and point comes from a lookup named in lookups)."
   },
   "HelpAnswer": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "The question's id, for the vote and the hand-off.",
      "format": "uuid"
     },
     "answer": {
      "type": "string",
      "description": "The one-line answer."
     },
     "blocks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpBlock"
      }
     },
     "path": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "A node id from this build's help graph."
      }
     },
     "doit": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "Step ids (guide#n) the client can walk through."
      }
     },
     "lookups": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "The read-only tools that ran, as the person."
      }
     },
     "done_task_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "Tasks a lookup proved done; the client ticks them with an undo."
      }
     },
     "unblocked_check": {
      "type": "boolean",
      "description": "Always true: the client asks Did this unblock you?"
     },
     "confidence": {
      "type": "number",
      "minimum": 0,
      "maximum": 1
     },
     "fallback": {
      "type": "boolean",
      "description": "True when the plain answer (the cited steps and the guide, no model prose) was given: the model's reply failed a rule, timed out, errored, or the school is past its monthly cap."
     },
     "remaining_today": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Questions left today, null for super admins."
     },
     "buddy": {
      "type": "string",
      "enum": [
       "mithoo",
       "hoot"
      ],
      "description": "Which character answers: Mithoo for students up to grade 7, Smarty the owl (id hoot) for everyone else."
     },
     "tone_plain": {
      "type": "boolean",
      "description": "Less jokes is on for this person."
     }
    },
    "required": [
     "id",
     "answer",
     "blocks",
     "path",
     "doit",
     "lookups",
     "done_task_ids",
     "unblocked_check",
     "confidence",
     "fallback",
     "remaining_today",
     "buddy",
     "tone_plain"
    ]
   },
   "HelpVote": {
    "type": "object",
    "properties": {
     "vote": {
      "type": "string",
      "enum": [
       "yes",
       "not_yet"
      ]
     }
    },
    "required": [
     "vote"
    ]
   },
   "HelpVoteResult": {
    "type": "object",
    "properties": {
     "next_answer": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/HelpAnswer"
       },
       {
        "type": "null"
       }
      ],
      "description": "On not_yet, when the hard-case council sat and one of its answers passed the checks: the new answer, shown in place before any hand-off. Null otherwise (yes, the council off for this trigger, past its budget, or no seat passed)."
     }
    },
    "required": [
     "next_answer"
    ]
   },
   "HelpBuddyStatus": {
    "type": "object",
    "properties": {
     "enabled": {
      "type": "boolean",
      "description": "False when the buddy is off everywhere, off for this person's audience at their school, or the person is viewing as someone else: the client hides the button."
     },
     "buddy": {
      "type": "string",
      "enum": [
       "mithoo",
       "hoot"
      ],
      "description": "Which character this person gets: Mithoo for students up to grade 7, Smarty the owl (id hoot) for everyone else."
     },
     "remaining_today": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Questions left today, null for super admins."
     },
     "tone_plain": {
      "type": "boolean",
      "description": "Less jokes is on for this person."
     },
     "replies_unread": {
      "type": "integer",
      "description": "Replies from the saMas help team waiting to be read: the badge on the button."
     }
    },
    "required": [
     "enabled",
     "buddy",
     "remaining_today",
     "tone_plain",
     "replies_unread"
    ]
   },
   "WhatsNewItem": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Stable id of the item.",
      "maxLength": 64
     },
     "date": {
      "type": "string",
      "description": "The day it shipped.",
      "format": "date",
      "example": "2026-09-01"
     },
     "title": {
      "type": "string",
      "maxLength": 60
     },
     "text": {
      "type": "string",
      "description": "One plain sentence.",
      "maxLength": 200
     },
     "add_on": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "madrasa",
       "learn",
       "charity",
       "live_sabaq",
       "toefl",
       "flashcards",
       null
      ],
      "description": "The add-on the item is about; the item is only returned when the school has it on. Null when it needs none."
     },
     "try_it": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "route": {
          "type": "string",
          "description": "A screen of the web app, for example /theme.",
          "maxLength": 200
         },
         "anchor": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 120,
          "description": "The data-help anchor to point at on that screen (the app adds ?help=<anchor>)."
         }
        },
        "required": [
         "route",
         "anchor"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "Try it now: where to go to use it. Null when there is no screen for it yet."
     }
    },
    "required": [
     "id",
     "date",
     "title",
     "text",
     "add_on",
     "try_it"
    ],
    "description": "One What's new line on the Help page (PRD H6), from the help catalog kept with the app."
   },
   "HelpTone": {
    "type": "object",
    "properties": {
     "plain": {
      "type": "boolean",
      "description": "True turns Less jokes on for 30 days; false turns it off."
     }
    },
    "required": [
     "plain"
    ]
   },
   "HelpToneResult": {
    "type": "object",
    "properties": {
     "plain": {
      "type": "boolean"
     },
     "until": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "The last day plain answers apply, null when off."
     }
    },
    "required": [
     "plain",
     "until"
    ]
   },
   "HelpBuddySettings": {
    "type": "object",
    "properties": {
     "staff": {
      "type": "boolean",
      "description": "Staff see the buddy."
     },
     "parents": {
      "type": "boolean"
     },
     "students": {
      "type": "boolean"
     }
    },
    "required": [
     "staff",
     "parents",
     "students"
    ]
   },
   "HelpBuddySettingsWrite": {
    "type": "object",
    "properties": {
     "staff": {
      "type": "boolean"
     },
     "parents": {
      "type": "boolean"
     },
     "students": {
      "type": "boolean"
     }
    },
    "description": "A switch left out keeps its value."
   },
   "HelpReport": {
    "type": "object",
    "properties": {
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000,
      "description": "The person's own words, left out for none."
     },
     "severity": {
      "$ref": "#/components/schemas/IssueSeverity"
     }
    },
    "required": [
     "severity"
    ]
   },
   "HelpReportResult": {
    "type": "object",
    "properties": {
     "issue_id": {
      "type": "string",
      "format": "uuid"
     },
     "ref": {
      "type": "string",
      "example": "PRB-2026-0007"
     },
     "owner": {
      "type": "string",
      "enum": [
       "samas",
       "school"
      ],
      "description": "Who got it: the school office (a school permission) or saMas triage."
     }
    },
    "required": [
     "issue_id",
     "ref",
     "owner"
    ]
   },
   "HelpReply": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "question_id": {
      "type": "string",
      "format": "uuid"
     },
     "question": {
      "type": [
       "string",
       "null"
      ],
      "description": "The question as asked, while it is still kept (30 days)."
     },
     "body": {
      "type": "string"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "read_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "question_id",
     "question",
     "body",
     "created_at",
     "read_at"
    ]
   },
   "HelpQuota": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "daily_limit": {
      "type": "integer"
     },
     "default_limit": {
      "type": "integer"
     },
     "until": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "When the raised limit ends; null when the default applies."
     },
     "used_today": {
      "type": "integer"
     },
     "remaining_today": {
      "type": "integer"
     },
     "set_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssuePerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "set_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "user_id",
     "daily_limit",
     "default_limit",
     "until",
     "used_today",
     "remaining_today",
     "set_by",
     "set_at"
    ]
   },
   "HelpQuotaWrite": {
    "type": "object",
    "properties": {
     "person": {
      "type": "string",
      "description": "The person's user id.",
      "format": "uuid"
     },
     "limit": {
      "type": "integer",
      "minimum": 20,
      "maximum": 50
     },
     "until": {
      "type": "string",
      "description": "The last day the raised limit applies, at most a year away.",
      "format": "date",
      "example": "2026-09-01"
     }
    },
    "required": [
     "person",
     "limit",
     "until"
    ]
   },
   "HelpStuckRow": {
    "type": "object",
    "properties": {
     "task": {
      "type": [
       "string",
       "null"
      ]
     },
     "screen": {
      "type": [
       "string",
       "null"
      ]
     },
     "code": {
      "type": [
       "string",
       "null"
      ]
     },
     "questions": {
      "type": "integer"
     },
     "fallbacks": {
      "type": "integer"
     },
     "unblocked": {
      "type": "integer"
     },
     "not_yet": {
      "type": "integer"
     },
     "handed_off": {
      "type": "integer"
     },
     "schools": {
      "type": "integer"
     },
     "top_path": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "description": "The path the buddy took most often for this moment."
     },
     "last_at": {
      "type": "string",
      "format": "date-time"
     },
     "fix": {
      "type": [
       "string",
       "null"
      ],
      "description": "The suggested fix read off the top path: the refusal's what-to-do-next, else the first cited step's words."
     }
    },
    "required": [
     "task",
     "screen",
     "code",
     "questions",
     "fallbacks",
     "unblocked",
     "not_yet",
     "handed_off",
     "schools",
     "top_path",
     "last_at",
     "fix"
    ]
   },
   "HelpOpenQuestion": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "asked_at": {
      "type": "string",
      "format": "date-time"
     },
     "role": {
      "type": [
       "string",
       "null"
      ]
     },
     "screen": {
      "type": [
       "string",
       "null"
      ]
     },
     "task": {
      "type": [
       "string",
       "null"
      ]
     },
     "code": {
      "type": [
       "string",
       "null"
      ]
     },
     "question": {
      "type": [
       "string",
       "null"
      ],
      "description": "Cleared after 30 days."
     },
     "path": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "fallback": {
      "type": "boolean"
     },
     "vote": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "yes",
       "not_yet",
       null
      ]
     },
     "school": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "report": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "issue_id": {
          "type": "string",
          "format": "uuid"
         },
         "ref": {
          "type": "string"
         }
        },
        "required": [
         "issue_id",
         "ref"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "replied_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "answer": {
      "type": [
       "string",
       "null"
      ],
      "description": "The one-line answer shown, while it is kept."
     },
     "model": {
      "type": [
       "string",
       "null"
      ]
     },
     "confidence": {
      "type": [
       "number",
       "null"
      ]
     },
     "cost_usd": {
      "type": "number"
     },
     "outcome": {
      "type": "string",
      "enum": [
       "answered",
       "plain",
       "council",
       "handed_off",
       "refusal"
      ],
      "description": "answered: a model's answer from a guide. plain: the plain steps. council: the hard-case council's answer. handed_off: sent on as a report. refusal: a rule answered (an honesty question, a serious matter, a refusal)."
     }
    },
    "required": [
     "id",
     "asked_at",
     "role",
     "screen",
     "task",
     "code",
     "question",
     "path",
     "fallback",
     "vote",
     "school",
     "report",
     "replied_at",
     "answer",
     "model",
     "confidence",
     "cost_usd",
     "outcome"
    ]
   },
   "HelpSummaryFigures": {
    "type": "object",
    "properties": {
     "questions": {
      "type": "integer"
     },
     "votes": {
      "type": "integer",
      "description": "Answers to Did this unblock you?"
     },
     "unblocked": {
      "type": "integer",
      "description": "Yes votes."
     },
     "unblocked_rate": {
      "type": [
       "number",
       "null"
      ],
      "description": "Yes votes over all votes, 0 to 1; null without votes."
     },
     "hand_off_rate": {
      "type": "number",
      "description": "Questions sent on as a report, over questions."
     },
     "no_guide_rate": {
      "type": "number",
      "description": "Questions with no guide to cite, over questions."
     },
     "fallback_rate": {
      "type": "number",
      "description": "Plain answers (the model's reply thrown away or never asked), over questions."
     },
     "median_unblock_seconds": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Median time from the question to a Yes vote; null without Yes votes."
     },
     "cost_usd": {
      "type": "number",
      "description": "Model cost, USD."
     }
    },
    "required": [
     "questions",
     "votes",
     "unblocked",
     "unblocked_rate",
     "hand_off_rate",
     "no_guide_rate",
     "fallback_rate",
     "median_unblock_seconds",
     "cost_usd"
    ]
   },
   "HelpSummarySchool": {
    "type": "object",
    "properties": {
     "school": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "figures": {
      "$ref": "#/components/schemas/HelpSummaryFigures"
     }
    },
    "required": [
     "school",
     "figures"
    ]
   },
   "HelpSummaryRole": {
    "type": "object",
    "properties": {
     "role": {
      "type": "string",
      "example": "teacher"
     },
     "questions": {
      "type": "integer"
     },
     "votes": {
      "type": "integer"
     },
     "unblocked_rate": {
      "type": [
       "number",
       "null"
      ]
     }
    },
    "required": [
     "role",
     "questions",
     "votes",
     "unblocked_rate"
    ]
   },
   "HelpSummaryPeriod": {
    "type": "object",
    "properties": {
     "days": {
      "type": "integer"
     },
     "totals": {
      "$ref": "#/components/schemas/HelpSummaryFigures"
     },
     "schools": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpSummarySchool"
      }
     },
     "roles": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpSummaryRole"
      }
     }
    },
    "required": [
     "days",
     "totals",
     "schools",
     "roles"
    ]
   },
   "HelpSummaryDay": {
    "type": "object",
    "properties": {
     "day": {
      "type": "string",
      "description": "The UTC day.",
      "format": "date",
      "example": "2026-09-01"
     },
     "answered": {
      "type": "integer",
      "description": "A model's answer from a guide."
     },
     "plain": {
      "type": "integer",
      "description": "The plain steps."
     },
     "council": {
      "type": "integer",
      "description": "The hard-case council's answer."
     },
     "handed_off": {
      "type": "integer",
      "description": "Sent on as a report."
     },
     "refusal": {
      "type": "integer",
      "description": "A rule answered."
     }
    },
    "required": [
     "day",
     "answered",
     "plain",
     "council",
     "handed_off",
     "refusal"
    ]
   },
   "HelpSummary": {
    "type": "object",
    "properties": {
     "last_7_days": {
      "$ref": "#/components/schemas/HelpSummaryPeriod"
     },
     "last_30_days": {
      "$ref": "#/components/schemas/HelpSummaryPeriod"
     },
     "by_day": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpSummaryDay"
      }
     }
    },
    "required": [
     "last_7_days",
     "last_30_days",
     "by_day"
    ],
    "description": "The numbers watched (PRD 5.8.5), for the last 7 and 30 days, in all and per school, and the last 14 days by outcome, oldest first. Questions the nightly eval asks are left out."
   },
   "HelpFixRow": {
    "type": "object",
    "properties": {
     "guide": {
      "type": [
       "string",
       "null"
      ],
      "description": "The guide slug, null when no guide was cited."
     },
     "step": {
      "type": [
       "string",
       "null"
      ],
      "description": "The step id (guide#n) most often on the path, null when none."
     },
     "screen": {
      "type": [
       "string",
       "null"
      ]
     },
     "task": {
      "type": [
       "string",
       "null"
      ]
     },
     "code": {
      "type": [
       "string",
       "null"
      ]
     },
     "questions": {
      "type": "integer"
     },
     "not_yet": {
      "type": "integer"
     },
     "fallbacks": {
      "type": "integer"
     },
     "handed_off": {
      "type": "integer"
     },
     "no_guide": {
      "type": "integer"
     },
     "change": {
      "type": "string",
      "description": "The suggested change, in plain words."
     }
    },
    "required": [
     "guide",
     "step",
     "screen",
     "task",
     "code",
     "questions",
     "not_yet",
     "fallbacks",
     "handed_off",
     "no_guide",
     "change"
    ]
   },
   "HelpFixList": {
    "type": "object",
    "properties": {
     "week_start": {
      "type": [
       "string",
       "null"
      ],
      "format": "date",
      "description": "The Monday of the week the list is for (Pacific time); null when nothing has been built yet."
     },
     "built_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpFixRow"
      }
     }
    },
    "required": [
     "week_start",
     "built_at",
     "rows"
    ]
   },
   "HelpEvalRun": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "ran_at": {
      "type": "string",
      "format": "date-time"
     },
     "status": {
      "type": "string",
      "enum": [
       "passed",
       "failed",
       "skipped"
      ],
      "description": "skipped: no model key, or the run is off for this environment."
     },
     "cases": {
      "type": "integer"
     },
     "passed": {
      "type": "integer"
     },
     "failed": {
      "type": "integer"
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "failures": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "description": "The case id in api/test/help-ask.eval.json."
        },
        "reason": {
         "type": "string"
        }
       },
       "required": [
        "id",
        "reason"
       ]
      }
     },
     "cost_usd": {
      "type": "number"
     }
    },
    "required": [
     "id",
     "ran_at",
     "status",
     "cases",
     "passed",
     "failed",
     "note",
     "failures",
     "cost_usd"
    ]
   },
   "HelpModelString": {
    "type": "string",
    "description": "A gateway model string, provider/model: for example workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast, google-ai-studio/gemini-2.5-flash-lite, openai/gpt-4.1-mini, anthropic/claude-haiku-4.5.",
    "minLength": 3,
    "maxLength": 120,
    "pattern": "^[A-Za-z0-9._@/:-]+$",
    "example": "openai/gpt-4.1-mini"
   },
   "HelpQualityFloor": {
    "type": "object",
    "properties": {
     "eval": {
      "type": "number",
      "minimum": 0,
      "maximum": 1,
      "description": "The rolling 7-night pass rate a model needs to enter the pool."
     },
     "adversarial": {
      "type": "number",
      "minimum": 0,
      "maximum": 1,
      "description": "The share of adversarial cases a model must refuse; 1 means every one."
     }
    },
    "required": [
     "eval",
     "adversarial"
    ]
   },
   "HelpSettings": {
    "type": "object",
    "properties": {
     "enabled": {
      "type": "boolean",
      "description": "The platform switch: off hides the buddy everywhere within a minute."
     },
     "model_small": {
      "$ref": "#/components/schemas/HelpModelString"
     },
     "model_large": {
      "type": [
       "string",
       "null"
      ],
      "description": "Tried through the gateway when the small tier is not sure; null for never."
     },
     "cost_cap_usd": {
      "type": "number",
      "description": "Per school per month; past it the buddy answers from the graph alone. The same cap is set on the gateway as a spend limit by school."
     },
     "gateway_account": {
      "type": "string",
      "description": "The Cloudflare account id the AI Gateway lives in (32 hex characters); blank until the gateway is set up, and the buddy then uses the direct fallback or the recorded replies."
     },
     "gateway_id": {
      "type": "string",
      "description": "The gateway's name.",
      "example": "samas-school-help"
     },
     "model_pool": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "council_models": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "quality_floor": {
      "$ref": "#/components/schemas/HelpQualityFloor"
     },
     "council_cases_per_night": {
      "type": "integer",
      "description": "How many eval cases the council asks each model a night (the adversarial cases every night, the rest in rotation)."
     },
     "council_cap_usd": {
      "type": "number",
      "description": "The council stops for the night once its spend passes this."
     },
     "council_hard": {
      "$ref": "#/components/schemas/HelpHardCouncil"
     }
    },
    "required": [
     "enabled",
     "model_small",
     "model_large",
     "cost_cap_usd",
     "gateway_account",
     "gateway_id",
     "model_pool",
     "council_models",
     "quality_floor",
     "council_cases_per_night",
     "council_cap_usd",
     "council_hard"
    ],
    "description": "The small tier is the pool: one model is called directly; more than one goes through the gateway's dynamic route built from the pool (cheapest first with fallbacks). The council rewrites the pool every night."
   },
   "HelpSettingsWrite": {
    "type": "object",
    "properties": {
     "enabled": {
      "type": "boolean"
     },
     "model_small": {
      "$ref": "#/components/schemas/HelpModelString"
     },
     "model_large": {
      "type": [
       "string",
       "null"
      ],
      "minLength": 3,
      "maxLength": 120
     },
     "cost_cap_usd": {
      "type": "number",
      "minimum": 0,
      "maximum": 10000
     },
     "gateway_account": {
      "type": "string",
      "description": "32 hex characters, or blank to switch the gateway off.",
      "maxLength": 32,
      "pattern": "^([0-9a-f]{32})?$"
     },
     "gateway_id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "pattern": "^[a-z0-9-]+$"
     },
     "model_pool": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      },
      "maxItems": 20
     },
     "council_models": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      },
      "maxItems": 20
     },
     "quality_floor": {
      "type": "object",
      "properties": {
       "eval": {
        "type": "number",
        "minimum": 0,
        "maximum": 1
       },
       "adversarial": {
        "type": "number",
        "minimum": 0,
        "maximum": 1
       }
      },
      "description": "Either value; one left out keeps its value."
     },
     "council_cases_per_night": {
      "type": "integer",
      "minimum": 1,
      "maximum": 500
     },
     "council_cap_usd": {
      "type": "number",
      "minimum": 0,
      "maximum": 1000
     },
     "council_hard": {
      "$ref": "#/components/schemas/HelpHardCouncilWrite"
     }
    },
    "description": "A field left out keeps its value."
   },
   "HelpHardTriggers": {
    "type": "object",
    "properties": {
     "unsure": {
      "type": "boolean",
      "description": "The first answer's confidence is under 0.6."
     },
     "failed_checks": {
      "type": "boolean",
      "description": "The first reply failed a rule (a node outside the excerpt, a button not on a step, a Hindi word)."
     },
     "not_yet": {
      "type": "boolean",
      "description": "The person voted Not yet; the council answers before any hand-off."
     },
     "no_guide": {
      "type": "boolean",
      "description": "The walk found no guide for the context; the council gets the question's words and a wider excerpt."
     }
    },
    "required": [
     "unsure",
     "failed_checks",
     "not_yet",
     "no_guide"
    ]
   },
   "HelpHardCouncil": {
    "type": "object",
    "properties": {
     "enabled": {
      "type": "boolean",
      "description": "The council sits on hard cases: two more models answer the same question in parallel and the validator-passing answer with the highest confidence is shown. Never for a refusal, an honesty question or a serious matter."
     },
     "triggers": {
      "$ref": "#/components/schemas/HelpHardTriggers"
     },
     "extra_seats": {
      "type": "integer",
      "description": "Models asked besides the pool model's own reply (seat 1)."
     },
     "order": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "per_question_usd": {
      "type": "number",
      "description": "The council does not sit when its seats would cost more than this for one question."
     },
     "per_day_usd": {
      "type": "number",
      "description": "Past this spend in a day (UTC) the council does not sit and the single answer stands."
     },
     "tie": {
      "type": "string",
      "enum": [
       "cheaper",
       "faster"
      ],
      "description": "Which passing answer is shown when two have the same confidence."
     },
     "pinned": {
      "type": "boolean",
      "description": "The nightly council may not reorder the seats."
     }
    },
    "required": [
     "enabled",
     "triggers",
     "extra_seats",
     "order",
     "per_question_usd",
     "per_day_usd",
     "tie",
     "pinned"
    ],
    "description": "The hard-case council. The seats are the first extra_seats models in order that are not the pool model, a different provider first when possible; the rest of the order is the bench. The nightly council reorders it from the sittings record unless pinned."
   },
   "HelpHardCouncilWrite": {
    "type": "object",
    "properties": {
     "enabled": {
      "type": "boolean"
     },
     "triggers": {
      "type": "object",
      "properties": {
       "unsure": {
        "type": "boolean"
       },
       "failed_checks": {
        "type": "boolean"
       },
       "not_yet": {
        "type": "boolean"
       },
       "no_guide": {
        "type": "boolean"
       }
      },
      "description": "A trigger left out keeps its value."
     },
     "extra_seats": {
      "type": "integer",
      "minimum": 1,
      "maximum": 3
     },
     "order": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      },
      "maxItems": 20
     },
     "per_question_usd": {
      "type": "number",
      "minimum": 0,
      "maximum": 10
     },
     "per_day_usd": {
      "type": "number",
      "minimum": 0,
      "maximum": 1000
     },
     "tie": {
      "type": "string",
      "enum": [
       "cheaper",
       "faster"
      ]
     },
     "pinned": {
      "type": "boolean"
     }
    },
    "description": "A field left out keeps its value."
   },
   "HelpCouncilSeat": {
    "type": "object",
    "properties": {
     "seat": {
      "type": "integer",
      "description": "1 is the pool model's own reply."
     },
     "model": {
      "$ref": "#/components/schemas/HelpModelString"
     },
     "passed": {
      "type": "boolean",
      "description": "The reply passed every check."
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "description": "Why the reply failed, or why the seat was not asked (late, budget). Never the reply itself."
     },
     "confidence": {
      "type": [
       "number",
       "null"
      ]
     },
     "ms": {
      "type": [
       "integer",
       "null"
      ]
     },
     "cost_usd": {
      "type": "number"
     },
     "shown": {
      "type": "boolean",
      "description": "This seat's answer was the one shown."
     }
    },
    "required": [
     "seat",
     "model",
     "passed",
     "reason",
     "confidence",
     "ms",
     "cost_usd",
     "shown"
    ]
   },
   "HelpCouncilSitting": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "question_id": {
      "type": "string",
      "format": "uuid"
     },
     "sat_at": {
      "type": "string",
      "format": "date-time"
     },
     "trigger": {
      "type": "string",
      "enum": [
       "unsure",
       "failed_checks",
       "not_yet",
       "no_guide"
      ]
     },
     "seats": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpCouncilSeat"
      }
     },
     "shown_model": {
      "type": [
       "string",
       "null"
      ],
      "description": "The model whose answer was shown; null when no seat passed."
     },
     "outcome": {
      "type": "string",
      "enum": [
       "shown",
       "plain",
       "handed_off"
      ],
      "description": "shown: a seat's answer was shown. plain: none passed and the plain steps stood. handed_off: none passed on a Not yet and the hand-off was offered."
     },
     "cost_usd": {
      "type": "number"
     },
     "ms": {
      "type": "integer",
      "description": "From the sitting's start to the pick."
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "description": "Why the sitting was cut short (a budget) or why the first answer stood."
     },
     "role": {
      "type": [
       "string",
       "null"
      ]
     },
     "school": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "id",
     "question_id",
     "sat_at",
     "trigger",
     "seats",
     "shown_model",
     "outcome",
     "cost_usd",
     "ms",
     "note",
     "role",
     "school"
    ]
   },
   "HelpCouncilSittings": {
    "type": "object",
    "properties": {
     "days": {
      "type": "integer"
     },
     "questions": {
      "type": "integer",
      "description": "Questions asked in the period, for the share."
     },
     "sittings": {
      "type": "integer"
     },
     "rescued": {
      "type": "integer",
      "description": "Sittings that showed a full answer instead of the plain steps."
     },
     "handed_off": {
      "type": "integer",
      "description": "Sittings where no seat passed."
     },
     "cost_usd": {
      "type": "number"
     },
     "spent_today_usd": {
      "type": "number"
     },
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpCouncilSitting"
      }
     }
    },
    "required": [
     "days",
     "questions",
     "sittings",
     "rescued",
     "handed_off",
     "cost_usd",
     "spent_today_usd",
     "rows"
    ]
   },
   "HelpCouncilCompare": {
    "type": "object",
    "properties": {
     "a": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "b": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 30,
      "default": 7,
      "description": "The hard cases of the last days: every question the council sat on or the team marked wrong, while its text is kept."
     },
     "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 50,
      "default": 25,
      "description": "At most this many cases, newest first; the cost grows with it."
     }
    },
    "required": [
     "a",
     "b"
    ],
    "description": "Two formations (each an order of models: the first is the pool model, the next ones the seats) to replay the hard cases through."
   },
   "HelpCouncilCompareSide": {
    "type": "object",
    "properties": {
     "order": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "cases": {
      "type": "integer"
     },
     "rescued": {
      "type": "integer",
      "description": "Cases where a seat's answer passed the checks."
     },
     "urdu_rate": {
      "type": [
       "number",
       "null"
      ],
      "description": "Replies with no Hindi word, over replies; null without replies."
     },
     "cost_per_sitting_usd": {
      "type": [
       "number",
       "null"
      ]
     },
     "p90_ms": {
      "type": [
       "integer",
       "null"
      ]
     },
     "week_usd": {
      "type": [
       "number",
       "null"
      ],
      "description": "What a week of sittings at the last 7 days' rate would cost with this formation."
     }
    },
    "required": [
     "order",
     "cases",
     "rescued",
     "urdu_rate",
     "cost_per_sitting_usd",
     "p90_ms",
     "week_usd"
    ]
   },
   "HelpCouncilCompareResult": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "ran",
       "skipped"
      ],
      "description": "skipped: the Worker cannot ask as a test person, or no hard case has its text."
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "cases": {
      "type": "integer"
     },
     "cost_usd": {
      "type": "number",
      "description": "What the comparison itself cost."
     },
     "a": {
      "$ref": "#/components/schemas/HelpCouncilCompareSide"
     },
     "b": {
      "$ref": "#/components/schemas/HelpCouncilCompareSide"
     }
    },
    "required": [
     "status",
     "note",
     "cases",
     "cost_usd",
     "a",
     "b"
    ]
   },
   "HelpQuestionRecord": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "asked_at": {
      "type": "string",
      "format": "date-time"
     },
     "answered_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "role": {
      "type": [
       "string",
       "null"
      ]
     },
     "screen": {
      "type": [
       "string",
       "null"
      ]
     },
     "task": {
      "type": [
       "string",
       "null"
      ]
     },
     "code": {
      "type": [
       "string",
       "null"
      ]
     },
     "question": {
      "type": [
       "string",
       "null"
      ],
      "description": "Cleared after 30 days."
     },
     "answer": {
      "type": [
       "string",
       "null"
      ],
      "description": "The one-line answer shown, cleared with the question after 30 days."
     },
     "path": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "lookups": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "done_task_ids": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "confidence": {
      "type": [
       "number",
       "null"
      ]
     },
     "model": {
      "type": [
       "string",
       "null"
      ],
      "description": "The model whose answer was shown; null for the plain steps or a rule."
     },
     "model_tier": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "small",
       "large",
       "none",
       null
      ]
     },
     "fallback": {
      "type": "boolean"
     },
     "fallback_reason": {
      "type": [
       "string",
       "null"
      ]
     },
     "cost_usd": {
      "type": "number"
     },
     "ms": {
      "type": [
       "integer",
       "null"
      ],
      "description": "From the question to the answer."
     },
     "vote": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "yes",
       "not_yet",
       null
      ]
     },
     "marked_wrong_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "The team marked the shown answer wrong; the question joins the hard cases the council is scored on."
     },
     "school": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "report": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "issue_id": {
          "type": "string",
          "format": "uuid"
         },
         "ref": {
          "type": "string"
         }
        },
        "required": [
         "issue_id",
         "ref"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "replies": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpReply"
      }
     },
     "sitting": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/HelpCouncilSitting"
       },
       {
        "type": "null"
       }
      ]
     },
     "events": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "at": {
         "type": "string",
         "format": "date-time"
        },
        "kind": {
         "type": "string"
        }
       },
       "required": [
        "at",
        "kind"
       ]
      }
     },
     "outcome": {
      "type": "string",
      "enum": [
       "answered",
       "plain",
       "council",
       "handed_off",
       "refusal"
      ]
     }
    },
    "required": [
     "id",
     "asked_at",
     "answered_at",
     "role",
     "screen",
     "task",
     "code",
     "question",
     "answer",
     "path",
     "lookups",
     "done_task_ids",
     "confidence",
     "model",
     "model_tier",
     "fallback",
     "fallback_reason",
     "cost_usd",
     "ms",
     "vote",
     "marked_wrong_at",
     "school",
     "report",
     "replies",
     "sitting",
     "events",
     "outcome"
    ],
    "description": "One question's full record for the saMas team: what was asked, what was shown, which model, why it fell back, the council sitting if one sat, the votes and the hand-off. Never names or field values."
   },
   "HelpQuestionMark": {
    "type": "object",
    "properties": {
     "wrong": {
      "type": "boolean",
      "description": "True marks the shown answer wrong; false clears the mark."
     }
    },
    "required": [
     "wrong"
    ]
   },
   "HelpModelRun": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "night": {
      "type": "string",
      "description": "The night (UTC date) the council sat.",
      "format": "date",
      "example": "2026-09-01"
     },
     "ran_at": {
      "type": "string",
      "format": "date-time"
     },
     "model": {
      "$ref": "#/components/schemas/HelpModelString"
     },
     "cases": {
      "type": "integer"
     },
     "passed": {
      "type": "integer"
     },
     "pass_rate": {
      "type": [
       "number",
       "null"
      ]
     },
     "adversarial_total": {
      "type": "integer"
     },
     "adversarial_passed": {
      "type": "integer",
      "description": "Adversarial cases the model refused as it should."
     },
     "median_ms": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Median time from the question to the answer."
     },
     "cost_usd": {
      "type": "number"
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "description": "For example that the budget stopped the night early."
     }
    },
    "required": [
     "id",
     "night",
     "ran_at",
     "model",
     "cases",
     "passed",
     "pass_rate",
     "adversarial_total",
     "adversarial_passed",
     "median_ms",
     "cost_usd",
     "note"
    ]
   },
   "HelpCouncilStanding": {
    "type": "object",
    "properties": {
     "model": {
      "$ref": "#/components/schemas/HelpModelString"
     },
     "cases": {
      "type": "integer"
     },
     "passed": {
      "type": "integer"
     },
     "pass_rate": {
      "type": [
       "number",
       "null"
      ],
      "description": "Over the last 7 nights."
     },
     "adversarial_total": {
      "type": "integer"
     },
     "adversarial_passed": {
      "type": "integer"
     },
     "adversarial_rate": {
      "type": [
       "number",
       "null"
      ]
     },
     "median_ms": {
      "type": [
       "integer",
       "null"
      ]
     },
     "cost_usd": {
      "type": "number"
     },
     "cost_per_100": {
      "type": [
       "number",
       "null"
      ],
      "description": "Cost of 100 answers, USD, from the council's own asks."
     },
     "nights": {
      "type": "integer"
     },
     "last_night": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "qualifies": {
      "type": "boolean",
      "description": "Clears the floor over the last 7 nights."
     },
     "in_pool": {
      "type": "boolean"
     }
    },
    "required": [
     "model",
     "cases",
     "passed",
     "pass_rate",
     "adversarial_total",
     "adversarial_passed",
     "adversarial_rate",
     "median_ms",
     "cost_usd",
     "cost_per_100",
     "nights",
     "last_night",
     "qualifies",
     "in_pool"
    ]
   },
   "HelpCouncil": {
    "type": "object",
    "properties": {
     "pool": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "council_models": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "quality_floor": {
      "$ref": "#/components/schemas/HelpQualityFloor"
     },
     "cases_per_night": {
      "type": "integer"
     },
     "cap_usd": {
      "type": "number"
     },
     "gateway": {
      "type": "object",
      "properties": {
       "configured": {
        "type": "boolean",
        "description": "The account id is set and the Worker has its gateway token."
       },
       "account": {
        "type": "string"
       },
       "gateway_id": {
        "type": "string"
       },
       "route": {
        "type": "string",
        "description": "The dynamic route the small tier calls when the pool has more than one model."
       },
       "token_set": {
        "type": "boolean"
       }
      },
      "required": [
       "configured",
       "account",
       "gateway_id",
       "route",
       "token_set"
      ]
     },
     "empty_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When the council last found no model above the floor and kept the previous pool; null once a model qualifies again."
     },
     "next_run_at": {
      "type": "string",
      "description": "The next nightly sitting, 21:00 UTC.",
      "format": "date-time"
     },
     "standings": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpCouncilStanding"
      }
     },
     "nights": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelRun"
      }
     }
    },
    "required": [
     "pool",
     "council_models",
     "quality_floor",
     "cases_per_night",
     "cap_usd",
     "gateway",
     "empty_at",
     "next_run_at",
     "standings",
     "nights"
    ],
    "description": "The daily model council: every candidate's last 7 nights, the pool it produced, the floor, and the last 14 nights per model."
   },
   "HelpCouncilRun": {
    "type": "object",
    "properties": {
     "cases_per_model": {
      "type": "integer",
      "minimum": 1,
      "maximum": 40,
      "default": 3,
      "description": "A small budget for a run during the day; the night uses council_cases_per_night."
     }
    }
   },
   "HelpCouncilRunResult": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "ran",
       "skipped"
      ],
      "description": "skipped: the Worker is not set up to ask as a test person (HELP_EVAL_SUBJECT)."
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "models": {
      "type": "integer"
     },
     "cases": {
      "type": "integer",
      "description": "Questions asked in all."
     },
     "cost_usd": {
      "type": "number"
     },
     "pool": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelString"
      }
     },
     "empty": {
      "type": "boolean",
      "description": "No model cleared the floor; the previous pool stays."
     },
     "runs": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HelpModelRun"
      }
     },
     "route_synced": {
      "type": "boolean",
      "description": "The gateway's dynamic route was updated to match the pool (needs the Worker's Cloudflare token)."
     }
    },
    "required": [
     "status",
     "note",
     "models",
     "cases",
     "cost_usd",
     "pool",
     "empty",
     "runs",
     "route_synced"
    ]
   },
   "IssuePerson": {
    "type": "object",
    "properties": {
     "user_id": {
      "type": "string",
      "format": "uuid"
     },
     "full_name": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "user_id",
     "full_name"
    ]
   },
   "IssueKind": {
    "type": "string",
    "enum": [
     "app",
     "school",
     "idea"
    ],
    "description": "app: something is broken in the app (goes to the saMas team). school: a question or matter for the school office. idea: a suggestion (goes to the saMas team)."
   },
   "IssueSeverity": {
    "type": "string",
    "enum": [
     "blocked",
     "slowed",
     "minor"
    ],
    "description": "How much it stops the person: blocked (cannot carry on), slowed (can work around it), minor (a small thing)."
   },
   "IssueStatus": {
    "type": "string",
    "enum": [
     "new",
     "looking",
     "fixed",
     "closed"
    ],
    "description": "new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done."
   },
   "IssueIncident": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "api",
       "network",
       "crash"
      ],
      "description": "api: the API answered an error; network: it could not be reached; crash: a screen broke in the browser."
     },
     "reference": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 64,
      "description": "The request id the API answered with (GET /ops/requests/{request_id} finds it), or the app's own INC- reference for a crash."
     },
     "code": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^[A-Z][A-Z0-9_]{1,39}$",
      "description": "The API error code, when there is one.",
      "maxLength": 80
     },
     "status": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 0,
      "maximum": 599
     },
     "method": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "GET",
       "POST",
       "PUT",
       "PATCH",
       "DELETE",
       null
      ]
     },
     "route": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "description": "The API route as a pattern, ids replaced by {id}."
     },
     "message": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "The message the person saw, scrubbed."
     },
     "at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "maxLength": 40
     },
     "stack": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 300
      },
      "maxItems": 20,
      "description": "For a crash: the frames of our own code, as <function> /assets/<file>.js:<line>:<column>. The saMas team reads them with the build's private source maps."
     }
    },
    "required": [
     "kind"
    ],
    "description": "The error the person hit just before reporting, shown to them before sending.",
    "additionalProperties": false
   },
   "IssueConsoleLine": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "error",
       "warning",
       "policy"
      ],
      "description": "policy: something the site's security policy blocked."
     },
     "text": {
      "type": "string",
      "description": "Scrubbed: no email addresses, phone or identity numbers.",
      "maxLength": 300
     },
     "count": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100000
     },
     "first_at": {
      "type": "string",
      "format": "date-time",
      "maxLength": 40
     },
     "last_at": {
      "type": "string",
      "format": "date-time",
      "maxLength": 40
     },
     "stack": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 300
      },
      "maxItems": 20,
      "description": "For an error: our own frames, as in IssueIncident.stack."
     }
    },
    "required": [
     "kind",
     "text",
     "count",
     "first_at",
     "last_at"
    ],
    "description": "One distinct thing the browser said, kept once with how many times.",
    "additionalProperties": false
   },
   "IssueRequestSeen": {
    "type": "object",
    "properties": {
     "method": {
      "type": "string",
      "enum": [
       "GET",
       "POST",
       "PUT",
       "PATCH",
       "DELETE"
      ]
     },
     "route": {
      "type": "string",
      "description": "The API route as a pattern, ids replaced by {id}.",
      "maxLength": 200
     },
     "status": {
      "type": "integer",
      "minimum": 0,
      "maximum": 599,
      "description": "0 when the API could not be reached."
     },
     "ms": {
      "type": "integer",
      "minimum": 0,
      "maximum": 600000
     },
     "reference": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 64
     },
     "at": {
      "type": "string",
      "format": "date-time",
      "maxLength": 40
     }
    },
    "required": [
     "method",
     "route",
     "status",
     "ms",
     "at"
    ],
    "description": "One of the app's last calls to the API. Never what was sent or received.",
    "additionalProperties": false
   },
   "IssueDevice": {
    "type": "object",
    "properties": {
     "viewport": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20,
      "example": "1280x800"
     },
     "zoom": {
      "type": [
       "number",
       "null"
      ],
      "minimum": 0.1,
      "maximum": 10
     },
     "touch": {
      "type": [
       "boolean",
       "null"
      ]
     },
     "online": {
      "type": [
       "boolean",
       "null"
      ]
     },
     "connection": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20,
      "description": "The browser's estimate, such as 4g or 3g."
     },
     "time_zone": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 64,
      "example": "Asia/Karachi"
     },
     "clock_offset_s": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": -864000,
      "maximum": 864000,
      "description": "How far the device clock is from the server's, in seconds."
     },
     "language": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 20
     }
    },
    "description": "What the device looked like. Nothing that identifies it.",
    "additionalProperties": false
   },
   "IssueContext": {
    "type": "object",
    "properties": {
     "screen": {
      "type": [
       "string",
       "null"
      ],
      "description": "The route the person was on."
     },
     "role": {
      "type": [
       "string",
       "null"
      ]
     },
     "school_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "error_codes": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "An API error code the app hit shortly before, as a code only."
      }
     },
     "console_errors": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "A browser error message the app saw shortly before, scrubbed."
      }
     },
     "app_version": {
      "type": [
       "string",
       "null"
      ]
     },
     "browser": {
      "type": [
       "string",
       "null"
      ]
     },
     "incident": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssueIncident"
       },
       {
        "type": "null"
       }
      ]
     },
     "requests": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IssueRequestSeen"
      }
     },
     "steps": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "A step before the report: screen:<route pattern> or tap:<help anchor>. Never what was typed."
      }
     },
     "device": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssueDevice"
       },
       {
        "type": "null"
       }
      ]
     },
     "console": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IssueConsoleLine"
      }
     }
    },
    "description": "What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers."
   },
   "IssueScreenshot": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "content_type": {
      "type": "string"
     },
     "size_bytes": {
      "type": "integer"
     },
     "url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "A short-lived link to the picture."
     },
     "removed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Only the saMas team sees a removed screenshot, with when it was removed."
     }
    },
    "required": [
     "id",
     "name",
     "content_type",
     "size_bytes",
     "url",
     "removed_at"
    ]
   },
   "Issue": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "ref": {
      "type": "string",
      "example": "PRB-2026-0042"
     },
     "kind": {
      "$ref": "#/components/schemas/IssueKind"
     },
     "severity": {
      "$ref": "#/components/schemas/IssueSeverity"
     },
     "status": {
      "$ref": "#/components/schemas/IssueStatus"
     },
     "duplicate_of": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one."
     },
     "owner": {
      "type": "string",
      "enum": [
       "samas",
       "school"
      ],
      "description": "Who is looking after it now: the saMas team or the school office."
     },
     "priority": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "P1",
       "P2",
       "P3",
       null
      ],
      "description": "Set by the saMas team."
     },
     "assignee": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssuePerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "reporter": {
      "allOf": [
       {
        "$ref": "#/components/schemas/IssuePerson"
       },
       {
        "type": "object",
        "properties": {
         "role": {
          "type": [
           "string",
           "null"
          ]
         },
         "is_me": {
          "type": "boolean"
         }
        }
       }
      ]
     },
     "school": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "body": {
      "type": "string",
      "description": "What happened, in the reporter's words."
     },
     "context": {
      "$ref": "#/components/schemas/IssueContext"
     },
     "journey_item_key": {
      "type": [
       "string",
       "null"
      ],
      "description": "The getting started item it was sent from, if any."
     },
     "screenshot": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssueScreenshot"
       },
       {
        "type": "null"
       }
      ]
     },
     "screenshot_removed": {
      "type": "boolean"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "first_reply_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "fixed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "closed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "reopened_count": {
      "type": "integer"
     },
     "messages_count": {
      "type": "integer"
     },
     "last_message_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "my_side": {
      "type": "string",
      "enum": [
       "reporter",
       "school",
       "samas"
      ],
      "description": "Which side the caller stands on for this report."
     },
     "private_to_samas": {
      "type": "boolean",
      "description": "The reporter ticked Only the saMas team should see this: the school's office does not see the report and is not copied on its emails."
     },
     "closed_reason": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "stale",
       "office",
       "confirmed",
       "released",
       null
      ],
      "description": "Why a closed report closed: the reporter confirmed the fix, the school office closed it, the saMas team closed it after 14 days without word from the reporter (stale), or the roadmap item it joined was released. Null while open, and for a report the saMas team closed by hand."
     },
     "roadmap_item": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "title": {
          "type": "string"
         },
         "stage": {
          "$ref": "#/components/schemas/RoadmapStage"
         },
         "target": {
          "type": [
           "string",
           "null"
          ]
         }
        },
        "required": [
         "id",
         "title",
         "stage",
         "target"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "For an idea: the roadmap item it joined (a merged item answers as the item it joined), or null."
     },
     "priority_word": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "being looked at",
       "planned",
       "later",
       null
      ],
      "description": "Only for the school office on a report with the saMas team: the priority in words. The office never sees P1 to P3 or who it is assigned to (priority and assignee are null for it)."
     }
    },
    "required": [
     "id",
     "ref",
     "kind",
     "severity",
     "status",
     "owner",
     "priority",
     "assignee",
     "reporter",
     "school",
     "body",
     "context",
     "journey_item_key",
     "screenshot",
     "screenshot_removed",
     "created_at",
     "updated_at",
     "first_reply_at",
     "fixed_at",
     "closed_at",
     "reopened_count",
     "messages_count",
     "last_message_at",
     "my_side",
     "private_to_samas",
     "closed_reason"
    ]
   },
   "IssueMessage": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "side": {
      "type": "string",
      "enum": [
       "reporter",
       "school",
       "samas"
      ]
     },
     "author": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssuePerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "body": {
      "type": "string"
     },
     "pictures": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "content_type": {
         "type": "string"
        },
        "url": {
         "type": [
          "string",
          "null"
         ],
         "format": "uri",
         "description": "A short-lived link to the picture."
        }
       },
       "required": [
        "id",
        "name",
        "content_type",
        "url"
       ]
      },
      "description": "Pictures with the message: one the writer attached, or the evidence when a report is marked fixed."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "side",
     "author",
     "body",
     "created_at"
    ]
   },
   "IssueMessageWrite": {
    "type": "object",
    "properties": {
     "body": {
      "type": "string",
      "minLength": 1,
      "maxLength": 2000
     },
     "picture_upload_id": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 64,
      "description": "One picture with the message, from POST /me/issues/upload-url (the reporter) or POST /platform/issues/{id}/upload-url (the saMas team); NOT_FOUND when unknown or expired."
     }
    },
    "required": [
     "body"
    ]
   },
   "IssueFixedWrite": {
    "type": "object",
    "properties": {
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000,
      "description": "What was fixed, for the reporter."
     },
     "picture_upload_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 64
      },
      "maxItems": 3,
      "description": "Evidence of the fix, from POST /platform/issues/{id}/upload-url. Sent with the email as attachments and kept on the message."
     }
    }
   },
   "IssueEvent": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "created",
       "replied",
       "looking",
       "priority",
       "assigned",
       "passed_to_samas",
       "sent_to_school",
       "fixed",
       "confirmed",
       "reopened",
       "closed",
       "linked",
       "unlinked",
       "roadmap"
      ]
     },
     "side": {
      "type": [
       "string",
       "null"
      ]
     },
     "by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssuePerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "detail": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true
     },
     "at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "kind",
     "side",
     "by",
     "note",
     "detail",
     "at"
    ]
   },
   "IssueDetail": {
    "allOf": [
     {
      "$ref": "#/components/schemas/Issue"
     },
     {
      "type": "object",
      "properties": {
       "messages": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/IssueMessage"
        }
       },
       "events": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/IssueEvent"
        }
       }
      },
      "required": [
       "messages",
       "events"
      ]
     }
    ]
   },
   "SweepRow": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "ref": {
      "type": "string"
     },
     "school": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       },
       "code": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "kind": {
      "$ref": "#/components/schemas/IssueKind"
     },
     "severity": {
      "$ref": "#/components/schemas/IssueSeverity"
     },
     "status": {
      "$ref": "#/components/schemas/IssueStatus"
     },
     "priority": {
      "type": [
       "string",
       "null"
      ]
     },
     "age_hours": {
      "type": "integer"
     },
     "replied": {
      "type": "boolean"
     },
     "duplicate_of": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "screen": {
      "type": [
       "string",
       "null"
      ]
     },
     "error_codes": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "reference": {
      "type": [
       "string",
       "null"
      ]
     },
     "route": {
      "type": [
       "string",
       "null"
      ]
     },
     "has_trace": {
      "type": "boolean"
     },
     "body": {
      "type": "string"
     },
     "pile": {
      "type": "string",
      "enum": [
       "bug_with_trace",
       "bug_with_reference",
       "bug_without_trace",
       "idea",
       "school"
      ]
     },
     "private_to_samas": {
      "type": "boolean",
      "description": "The reporter kept it between them and the saMas team."
     },
     "roadmap_item_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Always null on the sweep: an idea that joined a roadmap item leaves the idea pile."
     }
    },
    "required": [
     "id",
     "ref",
     "school",
     "kind",
     "severity",
     "status",
     "age_hours",
     "replied",
     "has_trace",
     "body",
     "pile",
     "private_to_samas"
    ]
   },
   "FixerRun": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "status": {
      "type": "string",
      "enum": [
       "requested",
       "running",
       "done",
       "failed"
      ]
     },
     "environment": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "demo",
       "beta",
       "production",
       null
      ]
     },
     "requested_by": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "started_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "finished_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "summary": {
      "type": "object",
      "additionalProperties": true,
      "description": "What the run did: fixed (refs), shipped (commit), soaking (refs), waiting (refs with the branch and why), replied (refs), ideas_linked and ideas_proposed (refs), closed_stale (refs), notes, and for a failed run reason (one line: the gate, the network, the tests)."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "status",
     "summary",
     "created_at"
    ]
   },
   "ReviewRow": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "description": "Stable across calls, so a poller keeps one copy per row: branch:<branch>, report:<report id>, run:<run id>, gate:production-fixes, proposal:<item id>, proposal-stale:<item id>.",
      "maxLength": 200
     },
     "kind": {
      "type": "string",
      "enum": [
       "waiting_branch",
       "report_stuck",
       "run_failed",
       "switch",
       "roadmap_proposal",
       "roadmap_proposal_stale"
      ]
     },
     "title": {
      "type": "string",
      "maxLength": 200
     },
     "why": {
      "type": "string",
      "description": "Why it needs a person, in a sentence.",
      "maxLength": 300
     },
     "link": {
      "type": "string",
      "description": "A path in the web app that opens it.",
      "maxLength": 300
     },
     "since": {
      "type": "string",
      "format": "date-time"
     },
     "done": {
      "type": "boolean",
      "description": "True once it no longer needs anyone; done rows of the last 30 days are kept so a poller can tick its copy. A done row may be one that never needed anyone."
     },
     "environment": {
      "type": [
       "string",
       "null"
      ],
      "description": "The environment that answered: production, beta or demo."
     }
    },
    "required": [
     "key",
     "kind",
     "title",
     "why",
     "link",
     "since",
     "done",
     "environment"
    ]
   },
   "IssuesOverview": {
    "type": "object",
    "properties": {
     "window_days": {
      "type": [
       "integer",
       "null"
      ],
      "enum": [
       7,
       30,
       90,
       null
      ],
      "description": "The window asked for; null for all time."
     },
     "as_of": {
      "type": "string",
      "format": "date-time"
     },
     "tiles": {
      "type": "object",
      "properties": {
       "open": {
        "type": "integer"
       },
       "blocked": {
        "type": "integer"
       },
       "new_in_window": {
        "type": "integer"
       },
       "fixed_in_window": {
        "type": "integer"
       },
       "confirmed_in_window": {
        "type": "integer"
       },
       "median_first_reply_hours": {
        "type": [
         "number",
         "null"
        ]
       },
       "median_fixed_days": {
        "type": [
         "number",
         "null"
        ]
       },
       "needs_person": {
        "type": "integer"
       },
       "unanswered_in_window": {
        "type": "integer",
        "description": "Reports made in the window, not closed, with no first reply yet: the ones the median first reply cannot count."
       }
      },
      "required": [
       "open",
       "blocked",
       "new_in_window",
       "fixed_in_window",
       "confirmed_in_window",
       "median_first_reply_hours",
       "median_fixed_days",
       "needs_person"
      ]
     },
     "by_school": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "school": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          },
          "name": {
           "type": "string"
          },
          "code": {
           "type": "string"
          }
         },
         "required": [
          "id",
          "name",
          "code"
         ]
        },
        "open": {
         "type": "integer"
        },
        "blocked": {
         "type": "integer"
        },
        "app": {
         "type": "integer"
        },
        "idea": {
         "type": "integer"
        },
        "school_kind": {
         "type": "integer",
         "description": "Open school matters at this school (the office's)."
        },
        "private_count": {
         "type": "integer",
         "description": "Open reports kept private to the saMas team, as a number only."
        },
        "oldest_open_at": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        },
        "last_report_at": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        },
        "median_first_reply_hours": {
         "type": [
          "number",
          "null"
         ]
        }
       },
       "required": [
        "school",
        "open",
        "blocked",
        "app",
        "idea",
        "school_kind",
        "private_count",
        "oldest_open_at",
        "last_report_at",
        "median_first_reply_hours"
       ]
      }
     },
     "by_week": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "week_start": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        },
        "reported_app": {
         "type": "integer"
        },
        "reported_idea": {
         "type": "integer"
        },
        "fixed_app": {
         "type": "integer"
        },
        "released_idea": {
         "type": "integer"
        }
       },
       "required": [
        "week_start",
        "reported_app",
        "reported_idea",
        "fixed_app",
        "released_idea"
       ]
      }
     },
     "aging": {
      "type": "object",
      "properties": {
       "under_1d": {
        "type": "integer"
       },
       "d1_3": {
        "type": "integer"
       },
       "d3_7": {
        "type": "integer"
       },
       "over_7": {
        "type": "integer"
       }
      },
      "required": [
       "under_1d",
       "d1_3",
       "d3_7",
       "over_7"
      ]
     },
     "by_screen": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "screen": {
         "type": "string"
        },
        "count": {
         "type": "integer"
        }
       },
       "required": [
        "screen",
        "count"
       ]
      }
     },
     "by_role": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "role": {
         "type": "string"
        },
        "count": {
         "type": "integer"
        }
       },
       "required": [
        "role",
        "count"
       ]
      }
     },
     "needs_person": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ReviewRow"
      }
     }
    },
    "required": [
     "window_days",
     "as_of",
     "tiles",
     "by_school",
     "by_week",
     "aging",
     "by_screen",
     "by_role",
     "needs_person"
    ]
   },
   "RoadmapStage": {
    "type": "string",
    "enum": [
     "proposed",
     "considering",
     "planned",
     "building",
     "released",
     "not_now",
     "declined"
    ],
    "description": "proposed: the fixer suggested it and a person has not decided; considering, planned, building; released (final: the linked ideas were marked fixed and every school was told); not_now; declined (a proposal turned down)."
   },
   "RoadmapItem": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string",
      "maxLength": 80
     },
     "summary": {
      "type": "string",
      "description": "What a school will be able to do.",
      "maxLength": 400
     },
     "stage": {
      "$ref": "#/components/schemas/RoadmapStage"
     },
     "target": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60,
      "description": "Free text, like November or after the pilot."
     },
     "design_id": {
      "type": [
       "string",
       "null"
      ],
      "description": "A Labs design."
     },
     "prd_refs": {
      "type": "array",
      "items": {
       "type": "string",
       "example": "TT12"
      }
     },
     "proposed_by": {
      "type": "string",
      "enum": [
       "person",
       "fixer"
      ]
     },
     "proposed_from": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "issue_id": {
          "type": "string",
          "format": "uuid"
         },
         "ref": {
          "type": "string"
         },
         "school": {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "name": {
            "type": "string"
           }
          },
          "required": [
           "id",
           "name"
          ]
         }
        },
        "required": [
         "issue_id",
         "ref",
         "school"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The idea it was made from."
     },
     "weight": {
      "type": "integer",
      "description": "How many schools' ideas joined it."
     },
     "ideas_count": {
      "type": "integer"
     },
     "merged_into": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Set when this item was merged into another (it answers as that one)."
     },
     "released_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "released_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "public": {
      "type": "boolean",
      "description": "Shown on samas.school/features (and, once released, /new). RM13a."
     },
     "public_votes": {
      "type": "integer",
      "description": "Anonymous \"me too\" from the public site; never part of the weight."
     },
     "public_wishes": {
      "type": "integer",
      "description": "Wishes from the public site that joined it."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "history_count": {
      "type": "integer"
     }
    },
    "required": [
     "id",
     "title",
     "summary",
     "stage",
     "target",
     "design_id",
     "prd_refs",
     "proposed_by",
     "proposed_from",
     "weight",
     "ideas_count",
     "merged_into",
     "released_at",
     "created_at",
     "updated_at",
     "history_count"
    ]
   },
   "RoadmapChange": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "at": {
      "type": "string",
      "format": "date-time"
     },
     "by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssuePerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "by_fixer": {
      "type": "boolean",
      "description": "Made by the fixer's key."
     },
     "kind": {
      "type": "string",
      "enum": [
       "created",
       "edited",
       "stage",
       "linked",
       "unlinked",
       "merged",
       "released",
       "declined",
       "public"
      ]
     },
     "from": {
      "type": [
       "string",
       "null"
      ]
     },
     "to": {
      "type": [
       "string",
       "null"
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "id",
     "at",
     "by",
     "by_fixer",
     "kind",
     "from",
     "to",
     "note"
    ]
   },
   "RoadmapIdea": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "ref": {
      "type": "string"
     },
     "school": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "reporter_role": {
      "type": [
       "string",
       "null"
      ]
     },
     "first_line": {
      "type": "string"
     },
     "status": {
      "$ref": "#/components/schemas/IssueStatus"
     },
     "private_to_samas": {
      "type": "boolean"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "ref",
     "school",
     "reporter_role",
     "first_line",
     "status",
     "private_to_samas",
     "created_at"
    ]
   },
   "RoadmapItemDetail": {
    "allOf": [
     {
      "$ref": "#/components/schemas/RoadmapItem"
     },
     {
      "type": "object",
      "properties": {
       "ideas": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/RoadmapIdea"
        }
       },
       "history": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/RoadmapChange"
        }
       },
       "pictures": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          },
          "name": {
           "type": "string"
          },
          "content_type": {
           "type": "string"
          }
         },
         "required": [
          "id",
          "name",
          "content_type"
         ]
        }
       }
      },
      "required": [
       "ideas",
       "history",
       "pictures"
      ]
     }
    ]
   },
   "RoadmapItemWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 80
     },
     "summary": {
      "type": "string",
      "description": "What a school will be able to do, in plain words; no dates promised.",
      "minLength": 3,
      "maxLength": 400
     },
     "stage": {
      "type": "string",
      "enum": [
       "considering",
       "planned",
       "building"
      ],
      "description": "A person's new item; a key's is always proposed."
     },
     "target": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     },
     "design_id": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80,
      "pattern": "^[A-Za-z0-9_-]{1,80}$"
     },
     "prd_refs": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 8,
       "pattern": "^[A-Z]{1,4}[0-9]{1,3}[a-z]?$"
      },
      "maxItems": 20
     },
     "issue_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The idea it comes from; it joins the item and its reporter is told once. Required for a key."
     }
    },
    "required": [
     "title",
     "summary"
    ]
   },
   "RoadmapItemPatch": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 80
     },
     "summary": {
      "type": "string",
      "minLength": 3,
      "maxLength": 400
     },
     "target": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     },
     "design_id": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 80,
      "pattern": "^[A-Za-z0-9_-]{1,80}$"
     },
     "prd_refs": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 8,
       "pattern": "^[A-Z]{1,4}[0-9]{1,3}[a-z]?$"
      },
      "maxItems": 20
     }
    }
   },
   "RoadmapRelease": {
    "type": "object",
    "properties": {
     "note": {
      "type": "string",
      "description": "What is new, for the reporters and every school.",
      "minLength": 3,
      "maxLength": 1000
     },
     "picture_upload_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 64
      },
      "minItems": 1,
      "maxItems": 3,
      "description": "From POST /platform/roadmap/{id}/release/upload-url."
     },
     "link": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "pattern": "^/[A-Za-z0-9/_?=&.%-]{0,200}$",
      "description": "The page to try it on; default /whats-new."
     }
    },
    "required": [
     "note",
     "picture_upload_ids"
    ]
   },
   "RoadmapReleaseResult": {
    "type": "object",
    "properties": {
     "item": {
      "$ref": "#/components/schemas/RoadmapItemDetail"
     },
     "ideas_fixed": {
      "type": "integer"
     },
     "schools_told": {
      "type": "integer"
     },
     "emails": {
      "type": "integer",
      "description": "School update emails queued."
     },
     "skipped": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "school": {
         "type": "string"
        },
        "why": {
         "type": "string"
        }
       },
       "required": [
        "school",
        "why"
       ]
      }
     }
    },
    "required": [
     "item",
     "ideas_fixed",
     "schools_told",
     "emails",
     "skipped"
    ]
   },
   "RoadmapReleasePreview": {
    "type": "object",
    "properties": {
     "reporters": {
      "type": "integer"
     },
     "reporter_schools": {
      "type": "integer"
     },
     "active_schools": {
      "type": "integer"
     }
    },
    "required": [
     "reporters",
     "reporter_schools",
     "active_schools"
    ]
   },
   "RoadmapSchoolItem": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "summary": {
      "type": "string"
     },
     "stage": {
      "$ref": "#/components/schemas/RoadmapStage"
     },
     "target": {
      "type": [
       "string",
       "null"
      ]
     },
     "asked_by_you": {
      "type": "integer",
      "description": "Ideas from this school's people on it (never another school's)."
     },
     "released_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "title",
     "summary",
     "stage",
     "target",
     "asked_by_you",
     "released_at"
    ]
   },
   "AdminIssuesSummary": {
    "type": "object",
    "properties": {
     "open_office": {
      "type": "integer",
      "description": "School matters open with the office."
     },
     "waiting_on_samas": {
      "type": "integer",
      "description": "This school's reports open with the saMas team (not private ones)."
     },
     "fixed_this_month": {
      "type": "integer"
     },
     "ideas_on_roadmap": {
      "type": "integer",
      "description": "This school's ideas that joined a roadmap item."
     }
    },
    "required": [
     "open_office",
     "waiting_on_samas",
     "fixed_this_month",
     "ideas_on_roadmap"
    ]
   },
   "IssueWrite": {
    "type": "object",
    "properties": {
     "kind": {
      "$ref": "#/components/schemas/IssueKind"
     },
     "body": {
      "type": "string",
      "description": "What happened.",
      "minLength": 3,
      "maxLength": 2000
     },
     "severity": {
      "$ref": "#/components/schemas/IssueSeverity"
     },
     "screen": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "description": "The route the person was on."
     },
     "error_codes": {
      "type": "array",
      "items": {
       "type": "string",
       "pattern": "^[A-Z][A-Z0-9_]{1,39}$"
      },
      "maxItems": 3,
      "description": "The last API error codes the app saw, codes only."
     },
     "console_errors": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 300
      },
      "maxItems": 10,
      "description": "The browser's last error messages, scrubbed by the app (no email addresses, phone or identity numbers, no stacks), shown to the person before sending; left out or empty when they untick it."
     },
     "app_version": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "browser": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "item_key": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^[a-z][a-z0-9_]{1,40}$",
      "description": "The getting started item this was sent from (Having trouble?). That item shows needs help until the report is sorted."
     },
     "screenshot_upload_id": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 64,
      "description": "From POST /me/issues/upload-url; left out or null for no screenshot."
     },
     "incident": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssueIncident"
       },
       {
        "type": "null"
       }
      ]
     },
     "requests": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IssueRequestSeen"
      },
      "maxItems": 30,
      "description": "The app's last calls to the API, newest last."
     },
     "steps": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 120,
       "pattern": "^(screen|tap):[^\\s]{1,113}$"
      },
      "maxItems": 30,
      "description": "The last steps, newest last: screen:<route pattern> or tap:<help anchor>."
     },
     "device": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssueDevice"
       },
       {
        "type": "null"
       }
      ]
     },
     "console": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IssueConsoleLine"
      },
      "maxItems": 100,
      "description": "What the browser said: up to 100 distinct errors, warnings and blocked loads, each once with a count. Scrubbed by the app and shown to the person before sending."
     },
     "private_to_samas": {
      "type": "boolean",
      "description": "Only the saMas team should see this (an app problem or an idea; ignored for a school matter). The school's office will not see the report or be copied on its emails. Default false."
     }
    },
    "required": [
     "kind",
     "body",
     "severity"
    ]
   },
   "SitePicture": {
    "type": "object",
    "properties": {
     "url": {
      "type": "string",
      "description": "A link valid one day."
     },
     "name": {
      "type": "string"
     }
    },
    "required": [
     "url",
     "name"
    ]
   },
   "SitePublicItem": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "summary": {
      "type": "string"
     },
     "stage": {
      "type": "string",
      "enum": [
       "considering",
       "planned",
       "building",
       "released"
      ],
      "description": "released is shown as Live."
     },
     "weight_schools": {
      "type": "integer",
      "description": "How many schools asked (RM2)."
     },
     "public_votes": {
      "type": "integer",
      "description": "Visitors who said me too."
     },
     "released_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "pictures": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SitePicture"
      }
     }
    },
    "required": [
     "id",
     "title",
     "summary",
     "stage",
     "weight_schools",
     "public_votes",
     "released_on",
     "pictures"
    ],
    "description": "A public roadmap item (RM13a): never a school's name, never an idea."
   },
   "SiteHome": {
    "type": "object",
    "properties": {
     "schools": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string"
        },
        "web_url": {
         "type": [
          "string",
          "null"
         ]
        },
        "blurb": {
         "type": [
          "string",
          "null"
         ]
        }
       },
       "required": [
        "name",
        "web_url",
        "blurb"
       ]
      }
     },
     "school_count": {
      "type": "integer",
      "description": "Listed schools, computed (HM4)."
     },
     "quotes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "display_name": {
         "type": "string",
         "example": "Sample S."
        },
        "quote": {
         "type": "string"
        },
        "person_kind": {
         "type": "string",
         "enum": [
          "scholar",
          "admin",
          "parent",
          "teacher"
         ]
        },
        "school_name": {
         "type": [
          "string",
          "null"
         ]
        }
       },
       "required": [
        "display_name",
        "quote",
        "person_kind",
        "school_name"
       ]
      }
     },
     "updates": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SitePublicItem"
      }
     },
     "hero_names": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "display": {
         "type": "string",
         "description": "A first name and an initial, such as Fiza B.",
         "example": "Sample S.",
         "maxLength": 40
        }
       },
       "required": [
        "display"
       ]
      },
      "maxItems": 3,
      "description": "The hero register's names, in order, only for scholars who agreed (HM26); empty means the rows read [student]."
     },
     "brands": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SiteBrand"
      },
      "maxItems": 6,
      "description": "The listed schools' own looks, by name, six at most (HM42): the hero's chips and browser frame. Only schools that are listed, active, real and set up; empty when none is."
     }
    },
    "required": [
     "schools",
     "school_count",
     "quotes",
     "updates",
     "hero_names",
     "brands"
    ],
    "description": "What samas.school shows: listed real schools (HM5), quotes with consent (HM6), the five newest released public items (HM7), the hero register's agreed names (HM26). Empty lists, never placeholders."
   },
   "SiteBrand": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "The school's display name.",
      "maxLength": 80
     },
     "subdomain": {
      "type": "string",
      "maxLength": 63,
      "example": "nexus"
     },
     "web_url": {
      "type": "string",
      "description": "The school's own address.",
      "format": "uri",
      "maxLength": 200,
      "example": "https://nexus.samas.school"
     },
     "primary_color": {
      "type": "string",
      "pattern": "^#[0-9a-f]{6}$",
      "example": "#1e3a8a"
     },
     "accent_color": {
      "type": "string",
      "pattern": "^#[0-9a-f]{6}$",
      "example": "#f59e0b"
     },
     "font": {
      "$ref": "#/components/schemas/BrandFont"
     },
     "style": {
      "$ref": "#/components/schemas/BrandStyle"
     },
     "logo_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "maxLength": 400,
      "description": "The school's public crest, when it has one."
     }
    },
    "required": [
     "name",
     "subdomain",
     "web_url",
     "primary_color",
     "accent_color",
     "font",
     "style",
     "logo_url"
    ],
    "description": "One listed school's look for the samas.school hero (HM42)."
   },
   "SiteRoadmap": {
    "type": "object",
    "properties": {
     "items": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SitePublicItem"
      }
     }
    },
    "required": [
     "items"
    ]
   },
   "SiteUpdates": {
    "type": "object",
    "properties": {
     "updates": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SitePublicItem"
      }
     }
    },
    "required": [
     "updates"
    ]
   },
   "SizeBand": {
    "type": "string",
    "enum": [
     "under_50",
     "50_200",
     "200_800",
     "800_plus"
    ],
    "description": "Roughly how many students."
   },
   "SiteRequestWrite": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 2,
      "maxLength": 80
     },
     "school_name": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120
     },
     "city": {
      "type": "string",
      "minLength": 2,
      "maxLength": 60
     },
     "size_band": {
      "$ref": "#/components/schemas/SizeBand"
     },
     "contact": {
      "type": "string",
      "description": "An email (name@example.com) or a phone or WhatsApp number (+92300 0000000).",
      "minLength": 5,
      "maxLength": 200
     },
     "message": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 1000,
      "description": "What it should do first."
     },
     "answers": {
      "type": [
       "object",
       "null"
      ],
      "description": "The fit quiz's answers, q1 to q5, each up to 80 characters; anything else is dropped (HM16).",
      "properties": {
       "q1": {
        "type": "string",
        "maxLength": 80
       },
       "q2": {
        "type": "string",
        "maxLength": 80
       },
       "q3": {
        "type": "string",
        "maxLength": 80
       },
       "q4": {
        "type": "string",
        "maxLength": 80
       },
       "q5": {
        "type": "string",
        "maxLength": 80
       }
      },
      "additionalProperties": false
     },
     "website": {
      "type": "string",
      "description": "Leave empty. A form field people never see; a filled one is answered 204 and nothing is kept.",
      "maxLength": 200
     }
    },
    "required": [
     "name",
     "school_name",
     "city",
     "size_band",
     "contact"
    ],
    "additionalProperties": false
   },
   "SiteIdeaWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "body": {
      "type": "string",
      "description": "What it would let you do.",
      "minLength": 3,
      "maxLength": 1000
     },
     "requester_kind": {
      "type": "string",
      "enum": [
       "admin",
       "teacher",
       "finance",
       "parent",
       "student",
       "prospect"
      ]
     },
     "contact_email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "maxLength": 200,
      "description": "Optional: to hear when it is planned and when it is live. Used for nothing else (HM36)."
     },
     "website": {
      "type": "string",
      "description": "Leave empty (see SiteRequestWrite).",
      "maxLength": 200
     }
    },
    "required": [
     "title",
     "body",
     "requester_kind"
    ],
    "additionalProperties": false
   },
   "SiteRef": {
    "type": "object",
    "properties": {
     "ref": {
      "type": "string",
      "example": "REQ-2026-0001"
     }
    },
    "required": [
     "ref"
    ]
   },
   "SiteVote": {
    "type": "object",
    "properties": {
     "token": {
      "type": "string",
      "description": "32 hex characters the browser made once and keeps; the server keeps only a salted hash.",
      "pattern": "^[0-9a-f]{32}$",
      "maxLength": 80
     }
    },
    "required": [
     "token"
    ],
    "additionalProperties": false
   },
   "SiteWhoami": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "none",
       "admin",
       "scholar",
       "other"
      ],
      "description": "none: no valid sign-in was sent. admin: a school admin, principal or finance admin (the door reads Open my school). scholar: a student whose school has Learn on (Open my Learn). other: anyone else signed in; both doors stay as they are."
     },
     "school_subdomain": {
      "type": "string",
      "maxLength": 63,
      "description": "The school's address, for admin and scholar only."
     },
     "url": {
      "type": "string",
      "format": "uri",
      "maxLength": 200,
      "description": "Where the door goes: the school's admin site, or her Learn. For admin and scholar only."
     }
    },
    "required": [
     "kind"
    ]
   },
   "SiteVoteResult": {
    "type": "object",
    "properties": {
     "counted": {
      "type": "boolean",
      "description": "true: counted now; false: taken back."
     },
     "public_votes": {
      "type": "integer"
     }
    },
    "required": [
     "counted",
     "public_votes"
    ]
   },
   "SiteRequestStatus": {
    "type": "string",
    "enum": [
     "new",
     "contacted",
     "demo_made",
     "live",
     "declined"
    ]
   },
   "SiteRequest": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "ref": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "school_name": {
      "type": "string"
     },
     "city": {
      "type": "string"
     },
     "size_band": {
      "$ref": "#/components/schemas/SizeBand"
     },
     "contact": {
      "type": "string"
     },
     "message": {
      "type": [
       "string",
       "null"
      ]
     },
     "answers": {
      "type": [
       "object",
       "null"
      ]
     },
     "status": {
      "$ref": "#/components/schemas/SiteRequestStatus"
     },
     "notes": {
      "type": [
       "string",
       "null"
      ]
     },
     "organization": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The demo school made from it."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "ref",
     "name",
     "school_name",
     "city",
     "size_band",
     "contact",
     "message",
     "answers",
     "status",
     "notes",
     "organization",
     "created_at",
     "updated_at"
    ]
   },
   "SiteRequestPatch": {
    "type": "object",
    "properties": {
     "status": {
      "$ref": "#/components/schemas/SiteRequestStatus"
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     },
     "organization_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     }
    },
    "description": "Send any of the three."
   },
   "SiteHeroNames": {
    "type": "object",
    "properties": {
     "names": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "display": {
         "type": "string",
         "maxLength": 40
        },
        "agreed_on": {
         "type": "string",
         "format": "date",
         "example": "2026-09-01"
        }
       },
       "required": [
        "display",
        "agreed_on"
       ]
      },
      "maxItems": 3
     },
     "consent_note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "Who agreed, and how (a guardian too for anyone under eighteen)."
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_by": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "names",
     "consent_note",
     "updated_at",
     "updated_by"
    ]
   },
   "SiteHeroNamesWrite": {
    "type": "object",
    "properties": {
     "names": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "display": {
         "type": "string",
         "description": "A first name and an initial only, such as Fiza B.",
         "minLength": 2,
         "maxLength": 40
        }
       },
       "required": [
        "display"
       ]
      },
      "maxItems": 3,
      "description": "In the order the rows show them. Empty: every row reads [student] again (no note needed)."
     },
     "agreed_on": {
      "type": "string",
      "description": "The day they agreed. Needed when names is not empty; not a day still to come.",
      "format": "date",
      "example": "2026-09-01"
     },
     "consent_note": {
      "type": "string",
      "description": "Who agreed and how; a guardian too for anyone under eighteen. Needed when names is not empty.",
      "maxLength": 300
     }
    },
    "required": [
     "names"
    ]
   },
   "SiteQuote": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "display_name": {
      "type": "string"
     },
     "quote": {
      "type": "string"
     },
     "person_kind": {
      "type": "string",
      "enum": [
       "scholar",
       "admin",
       "parent",
       "teacher"
      ]
     },
     "minor": {
      "type": "boolean"
     },
     "organization": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "name": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "name"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "consent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "guardian_consent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "consent_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "hidden": {
      "type": "boolean"
     },
     "shown": {
      "type": "boolean",
      "description": "On samas.school now: consented (and the guardian too for a minor) and not hidden."
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "display_name",
     "quote",
     "person_kind",
     "minor",
     "organization",
     "consent_at",
     "guardian_consent_at",
     "consent_note",
     "hidden",
     "shown",
     "created_at"
    ]
   },
   "SiteQuoteWrite": {
    "type": "object",
    "properties": {
     "display_name": {
      "type": "string",
      "description": "A first name and an initial only, such as Sawaira S.",
      "minLength": 2,
      "maxLength": 40
     },
     "quote": {
      "type": "string",
      "minLength": 3,
      "maxLength": 240
     },
     "person_kind": {
      "type": "string",
      "enum": [
       "scholar",
       "admin",
       "parent",
       "teacher"
      ]
     },
     "minor": {
      "type": "boolean",
      "description": "Under eighteen: needs a guardian's consent too. Defaults to true."
     },
     "organization_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "consent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "guardian_consent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "consent_note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300
     }
    },
    "required": [
     "display_name",
     "quote",
     "person_kind"
    ]
   },
   "SiteQuotePatch": {
    "type": "object",
    "properties": {
     "consent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "guardian_consent_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "consent_note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300
     },
     "hidden": {
      "type": "boolean"
     }
    }
   },
   "PublicListingWrite": {
    "type": "object",
    "properties": {
     "public_listing": {
      "type": "boolean"
     },
     "public_blurb": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 160,
      "description": "One line under the name."
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "Needed to list: who at the school agreed in writing, and when."
     }
    },
    "required": [
     "public_listing"
    ]
   },
   "PublicListing": {
    "type": "object",
    "properties": {
     "public_listing": {
      "type": "boolean"
     },
     "public_blurb": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "public_listing",
     "public_blurb"
    ]
   },
   "RoadmapPublicWrite": {
    "type": "object",
    "properties": {
     "public": {
      "type": "boolean"
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300
     }
    },
    "required": [
     "public"
    ]
   },
   "OutsideReportRoadmap": {
    "type": "object",
    "properties": {
     "item_id": {
      "type": "string",
      "format": "uuid"
     },
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 500
     }
    },
    "required": [
     "item_id"
    ]
   },
   "Onboarding": {
    "type": "object",
    "properties": {
     "step": {
      "type": "integer",
      "minimum": 0,
      "maximum": 6,
      "description": "0 not started, 1 to 5 the step reached, 6 done."
     },
     "completed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "school": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       },
       "subdomain": {
        "type": [
         "string",
         "null"
        ]
       },
       "web_url": {
        "type": [
         "string",
         "null"
        ]
       },
       "admin_url": {
        "type": [
         "string",
         "null"
        ]
       },
       "accent_color": {
        "type": [
         "string",
         "null"
        ]
       }
      },
      "required": [
       "id",
       "name",
       "subdomain",
       "web_url",
       "admin_url",
       "accent_color"
      ]
     },
     "counts": {
      "type": "object",
      "properties": {
       "classes": {
        "type": "integer"
       },
       "sections": {
        "type": "integer"
       },
       "staff": {
        "type": "integer"
       },
       "students": {
        "type": "integer"
       },
       "sample_students": {
        "type": "integer"
       }
      },
      "required": [
       "classes",
       "sections",
       "staff",
       "students",
       "sample_students"
      ]
     },
     "samples_on": {
      "type": "boolean"
     },
     "email_receipts": {
      "type": "boolean",
      "description": "Parents are emailed a receipt for each payment (the school's email_receipts setting)."
     },
     "tour_seen_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When the school's admin finished or skipped the first-day tour (HM41); null until then."
     },
     "show_tour": {
      "type": "boolean",
      "description": "Set-up is finished and the tour has not been seen: the admin home shows the four coach marks."
     },
     "self_attendance": {
      "type": "boolean",
      "description": "The school's self_attendance setting (I'm here, HM40); the tour's third mark says how attendance is taken."
     }
    },
    "required": [
     "step",
     "completed_at",
     "school",
     "counts",
     "samples_on",
     "email_receipts",
     "tour_seen_at",
     "show_tour",
     "self_attendance"
    ],
    "description": "The school's set-up (HM28 to HM30) and its first-day tour (HM41)."
   },
   "TimetableLiveLinkWrite": {
    "type": "object",
    "properties": {
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7,
      "description": "1 is Monday."
     },
     "period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24
     },
     "shift_id": {
      "type": "string",
      "description": "The shift of the lesson; the class's own shift today when left out.",
      "format": "uuid"
     },
     "live_url": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "An https link on zoom.us, meet.google.com or meet.jit.si (or a subdomain of one), 300 characters at most; anything else is LIVE_LINK_HOST. null clears the link."
     }
    },
    "required": [
     "weekday",
     "period_no",
     "live_url"
    ]
   },
   "TimetableLiveLink": {
    "type": "object",
    "properties": {
     "section_id": {
      "type": "string",
      "format": "uuid"
     },
     "shift_id": {
      "type": "string",
      "format": "uuid"
     },
     "weekday": {
      "type": "integer",
      "minimum": 1,
      "maximum": 7
     },
     "period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24
     },
     "section_subject_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The subject and teacher the link was set for; when the published lesson at that period changes subject, the link stops showing."
     },
     "live_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "maxLength": 300,
      "description": "Null after a clear."
     },
     "set_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/PersonRef"
       },
       {
        "type": "null"
       }
      ]
     },
     "set_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "section_id",
     "shift_id",
     "weekday",
     "period_no",
     "section_subject_id",
     "live_url",
     "set_by",
     "set_at"
    ],
    "description": "One lesson's live-class link (HM38), kept apart from drafts and versions."
   },
   "SelfAttendanceWrite": {
    "type": "object",
    "properties": {
     "on_date": {
      "type": "string",
      "description": "Today, in the school's time.",
      "format": "date",
      "example": "2026-09-01"
     },
     "period_no": {
      "type": "integer",
      "minimum": 1,
      "maximum": 24,
      "description": "The lesson's period (NowLesson.period_no or TimetableLesson.period_no)."
     }
    },
    "required": [
     "on_date",
     "period_no"
    ]
   },
   "SelfAttendance": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "on_date": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "period_no": {
      "type": "integer"
     },
     "status": {
      "$ref": "#/components/schemas/AttendanceStatus"
     },
     "marked_by_self": {
      "type": "boolean",
      "description": "True when the mark is the student's own I'm here; false when a teacher's mark stands (it is never replaced)."
     },
     "marked_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "student_id",
     "on_date",
     "period_no",
     "status",
     "marked_by_self",
     "marked_at"
    ],
    "description": "The attendance for that lesson's period after I'm here (HM40)."
   },
   "FirstRun": {
    "type": "object",
    "properties": {
     "show": {
      "type": "boolean"
     },
     "seen_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     }
    },
    "required": [
     "show",
     "seen_at"
    ],
    "description": "The scholar's three welcome screens (HM27)."
   },
   "OutsideReportWrite": {
    "type": "object",
    "properties": {
     "body": {
      "type": "string",
      "description": "What happened.",
      "minLength": 3,
      "maxLength": 2000
     },
     "severity": {
      "$ref": "#/components/schemas/IssueSeverity"
     },
     "contact_email": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "format": "email",
      "description": "Where to answer, if the person wants an answer. Optional."
     },
     "host": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 100,
      "pattern": "^[a-z0-9.-]{1,100}$",
      "description": "The site the person was on, such as nexus.samas.school."
     },
     "signed_in": {
      "type": "boolean",
      "description": "True when they were signed in but their account would not load."
     },
     "screen": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200
     },
     "app_version": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     },
     "browser": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120
     },
     "incident": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssueIncident"
       },
       {
        "type": "null"
       }
      ]
     },
     "requests": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IssueRequestSeen"
      },
      "maxItems": 30
     },
     "steps": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 120,
       "pattern": "^(screen|tap):[^\\s]{1,113}$"
      },
      "maxItems": 30
     },
     "device": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssueDevice"
       },
       {
        "type": "null"
       }
      ]
     },
     "console": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IssueConsoleLine"
      },
      "maxItems": 100
     }
    },
    "required": [
     "body",
     "severity"
    ],
    "description": "A problem report from someone who cannot use the app: not signed in, or signed in but their account would not load.",
    "additionalProperties": false
   },
   "OutsideReport": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "ref": {
      "type": "string",
      "example": "OUT-2026-0001"
     },
     "kind": {
      "type": "string",
      "enum": [
       "problem",
       "idea"
      ],
      "description": "idea: a wish sent from samas.school/features (POST /school/public/site/ideas)."
     },
     "title": {
      "type": [
       "string",
       "null"
      ],
      "description": "A wish's title."
     },
     "requester_kind": {
      "type": [
       "string",
       "null"
      ],
      "description": "Who sent a wish: admin, teacher, finance, parent, student or prospect."
     },
     "roadmap_item": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "id": {
          "type": "string",
          "format": "uuid"
         },
         "title": {
          "type": "string"
         },
         "stage": {
          "$ref": "#/components/schemas/RoadmapStage"
         }
        },
        "required": [
         "id",
         "title",
         "stage"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "The roadmap item a wish joined."
     },
     "body": {
      "type": "string"
     },
     "severity": {
      "$ref": "#/components/schemas/IssueSeverity"
     },
     "contact_email": {
      "type": [
       "string",
       "null"
      ]
     },
     "host": {
      "type": [
       "string",
       "null"
      ]
     },
     "signed_in": {
      "type": "boolean"
     },
     "context": {
      "$ref": "#/components/schemas/IssueContext"
     },
     "status": {
      "type": "string",
      "enum": [
       "new",
       "closed"
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "closed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "ref",
     "body",
     "severity",
     "contact_email",
     "host",
     "signed_in",
     "context",
     "status",
     "note",
     "closed_at",
     "created_at"
    ]
   },
   "ClientErrorSeen": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "enum": [
       "error",
       "crash"
      ]
     },
     "text": {
      "type": "string",
      "description": "Scrubbed by the app: no email addresses, phone or identity numbers.",
      "minLength": 1,
      "maxLength": 300
     },
     "top_frame": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 300,
      "description": "The first frame of our own code, as <function> /assets/<file>.js:<line>:<column>."
     },
     "route": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 200,
      "description": "The screen, as a route pattern."
     },
     "version": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 40
     }
    },
    "required": [
     "kind",
     "text"
    ],
    "description": "One distinct browser error, sent once per session."
   },
   "ClientErrorGroup": {
    "type": "object",
    "properties": {
     "fingerprint": {
      "type": "string"
     },
     "kind": {
      "type": "string",
      "enum": [
       "error",
       "crash"
      ]
     },
     "pattern": {
      "type": "string",
      "description": "The text the group is matched by: numbers became # and ids {id}."
     },
     "sample": {
      "type": "string",
      "description": "The latest text, scrubbed."
     },
     "top_frame": {
      "type": [
       "string",
       "null"
      ]
     },
     "route": {
      "type": [
       "string",
       "null"
      ]
     },
     "hits": {
      "type": "integer"
     },
     "people": {
      "type": "integer"
     },
     "first_at": {
      "type": "string",
      "format": "date-time"
     },
     "last_at": {
      "type": "string",
      "format": "date-time"
     },
     "first_version": {
      "type": [
       "string",
       "null"
      ]
     },
     "last_version": {
      "type": [
       "string",
       "null"
      ]
     }
    },
    "required": [
     "fingerprint",
     "kind",
     "pattern",
     "sample",
     "top_frame",
     "route",
     "hits",
     "people",
     "first_at",
     "last_at",
     "first_version",
     "last_version"
    ]
   },
   "IssueUpload": {
    "type": "object",
    "properties": {
     "content_type": {
      "type": "string",
      "enum": [
       "image/png",
       "image/jpeg",
       "image/webp"
      ]
     },
     "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 5242880
     }
    },
    "required": [
     "content_type",
     "size_bytes"
    ]
   },
   "IssueNote": {
    "type": "object",
    "properties": {
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000,
      "description": "A message the reporter sees, left out for none."
     }
    }
   },
   "IssueReason": {
    "type": "object",
    "properties": {
     "reason": {
      "type": "string",
      "description": "Why the screenshot is being removed.",
      "minLength": 3,
      "maxLength": 200
     }
    },
    "required": [
     "reason"
    ]
   },
   "ScreenshotRemoval": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "issue_id": {
      "type": "string",
      "format": "uuid"
     },
     "ref": {
      "type": "string"
     },
     "school": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "name"
      ]
     },
     "removed_by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/IssuePerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "removed_as": {
      "type": "string",
      "enum": [
       "reporter",
       "school",
       "samas"
      ]
     },
     "reason": {
      "type": "string"
     },
     "file_path": {
      "type": [
       "string",
       "null"
      ]
     },
     "at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "issue_id",
     "ref",
     "school",
     "removed_by",
     "removed_as",
     "reason",
     "file_path",
     "at"
    ]
   },
   "Badges": {
    "type": "object",
    "properties": {
     "messages_unread": {
      "type": "integer",
      "description": "Conversations with unread messages."
     },
     "slips_waiting": {
      "type": "integer",
      "description": "Permission slips waiting for the caller's answer."
     },
     "reports_open": {
      "type": "integer",
      "description": "Reported messages waiting for the office."
     }
    },
    "required": [
     "messages_unread",
     "slips_waiting",
     "reports_open"
    ]
   },
   "SlipQuestion": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "example": "q1"
     },
     "label": {
      "type": "string",
      "minLength": 3,
      "maxLength": 200
     },
     "kind": {
      "type": "string",
      "enum": [
       "yes_no",
       "text"
      ]
     },
     "required": {
      "type": "boolean"
     }
    },
    "required": [
     "label",
     "kind"
    ]
   },
   "SlipAudience": {
    "type": "object",
    "properties": {
     "scope": {
      "type": "string",
      "enum": [
       "school",
       "classes",
       "sections"
      ]
     },
     "ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "label": {
      "type": "string",
      "description": "Read only."
     }
    },
    "required": [
     "scope"
    ]
   },
   "Slip": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "details": {
      "type": [
       "string",
       "null"
      ]
     },
     "event_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "sign_by": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "questions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SlipQuestion"
      }
     },
     "audience": {
      "$ref": "#/components/schemas/SlipAudience"
     },
     "status": {
      "type": "string",
      "enum": [
       "open",
       "closed",
       "cancelled"
      ],
      "description": "closed also once the last date has passed."
     },
     "cancel_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_by": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "closed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "reminded_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "counts": {
      "type": "object",
      "properties": {
       "students": {
        "type": "integer"
       },
       "agreed": {
        "type": "integer"
       },
       "declined": {
        "type": "integer"
       },
       "waiting": {
        "type": "integer"
       }
      },
      "required": [
       "students",
       "agreed",
       "declined",
       "waiting"
      ]
     },
     "can_manage": {
      "type": "boolean",
      "description": "The caller may change, remind, close or cancel it: the office, or the teacher who sent it. False for a class teacher reading a slip the office sent to her section."
     }
    },
    "required": [
     "id",
     "title",
     "sign_by",
     "questions",
     "audience",
     "status",
     "created_at",
     "counts"
    ]
   },
   "SlipAnswerRow": {
    "type": "object",
    "properties": {
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string"
       },
       "admission_no": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "full_name"
      ]
     },
     "class_label": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "enum": [
       "agree",
       "decline",
       "waiting"
      ]
     },
     "by": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/MessagePerson"
       },
       {
        "type": "null"
       }
      ]
     },
     "signed_name": {
      "type": [
       "string",
       "null"
      ]
     },
     "signed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "signed_ip": {
      "type": [
       "string",
       "null"
      ]
     },
     "answers": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      }
     },
     "changes": {
      "type": "integer",
      "description": "How many times the answer was changed."
     }
    },
    "required": [
     "student",
     "class_label",
     "status",
     "answers",
     "changes"
    ]
   },
   "SlipWrite": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "details": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 4000
     },
     "event_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "sign_by": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     },
     "questions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SlipQuestion"
      },
      "maxItems": 5
     },
     "audience": {
      "$ref": "#/components/schemas/SlipAudience"
     }
    }
   },
   "SlipAudienceOptions": {
    "type": "object",
    "properties": {
     "whole_school": {
      "type": "boolean",
      "description": "The caller may send to the whole school or to classes (the office)."
     },
     "can_send": {
      "type": "boolean"
     },
     "classes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "name": {
         "type": "string"
        },
        "sections": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "name": {
            "type": "string"
           }
          },
          "required": [
           "id",
           "name"
          ]
         }
        }
       },
       "required": [
        "id",
        "name",
        "sections"
       ]
      }
     }
    },
    "required": [
     "whole_school",
     "can_send",
     "classes"
    ]
   },
   "MySlip": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "details": {
      "type": [
       "string",
       "null"
      ]
     },
     "event_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "sign_by": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "questions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SlipQuestion"
      }
     },
     "status": {
      "type": "string",
      "enum": [
       "open",
       "closed",
       "cancelled"
      ]
     },
     "cancel_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_by": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "student": {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "full_name": {
        "type": "string"
       }
      },
      "required": [
       "id",
       "full_name"
      ]
     },
     "can_answer": {
      "type": "boolean"
     },
     "answer": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "decision": {
          "type": "string",
          "enum": [
           "agree",
           "decline"
          ]
         },
         "answers": {
          "type": "object",
          "additionalProperties": {
           "type": "string"
          }
         },
         "signed_name": {
          "type": "string"
         },
         "signed_at": {
          "type": "string",
          "format": "date-time"
         },
         "by": {
          "$ref": "#/components/schemas/MessagePerson"
         },
         "mine": {
          "type": "boolean"
         }
        },
        "required": [
         "decision",
         "signed_name",
         "signed_at",
         "by",
         "mine"
        ]
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "id",
     "title",
     "sign_by",
     "questions",
     "status",
     "student",
     "can_answer"
    ]
   },
   "SlipAnswerWrite": {
    "type": "object",
    "properties": {
     "student_id": {
      "type": "string",
      "format": "uuid"
     },
     "decision": {
      "type": "string",
      "enum": [
       "agree",
       "decline"
      ]
     },
     "answers": {
      "type": "object",
      "additionalProperties": {
       "type": "string",
       "maxLength": 300
      },
      "description": "Question id to answer; yes_no questions take yes or no."
     },
     "signed_name": {
      "type": "string",
      "minLength": 3,
      "maxLength": 100,
      "description": "The parent's full name, typed as their signature."
     },
     "confirm": {
      "type": "boolean",
      "const": true,
      "description": "The \"I agree\" tick."
     }
    },
    "required": [
     "student_id",
     "decision",
     "signed_name",
     "confirm"
    ]
   },
   "SlipDetail": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "title": {
      "type": "string"
     },
     "details": {
      "type": [
       "string",
       "null"
      ]
     },
     "event_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "sign_by": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFile"
      }
     },
     "questions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SlipQuestion"
      }
     },
     "audience": {
      "$ref": "#/components/schemas/SlipAudience"
     },
     "status": {
      "type": "string",
      "enum": [
       "open",
       "closed",
       "cancelled"
      ],
      "description": "closed also once the last date has passed."
     },
     "cancel_note": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_by": {
      "$ref": "#/components/schemas/MessagePerson"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "closed_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "reminded_at": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "counts": {
      "type": "object",
      "properties": {
       "students": {
        "type": "integer"
       },
       "agreed": {
        "type": "integer"
       },
       "declined": {
        "type": "integer"
       },
       "waiting": {
        "type": "integer"
       }
      },
      "required": [
       "students",
       "agreed",
       "declined",
       "waiting"
      ]
     },
     "can_manage": {
      "type": "boolean",
      "description": "The caller may change, remind, close or cancel it: the office, or the teacher who sent it. False for a class teacher reading a slip the office sent to her section."
     },
     "can_send": {
      "type": "boolean"
     },
     "answers": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SlipAnswerRow"
      }
     }
    },
    "required": [
     "id",
     "title",
     "sign_by",
     "questions",
     "audience",
     "status",
     "created_at",
     "counts",
     "answers"
    ]
   },
   "SlipCreate": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
     },
     "details": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 4000
     },
     "event_on": {
      "type": [
       "string",
       "null"
      ],
      "format": "date"
     },
     "sign_by": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     },
     "questions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SlipQuestion"
      },
      "maxItems": 5
     },
     "audience": {
      "$ref": "#/components/schemas/SlipAudience"
     }
    },
    "required": [
     "title",
     "sign_by",
     "audience"
    ]
   },
   "LiveResultParts": {
    "type": "object",
    "properties": {
     "sabaq": {
      "type": "string",
      "enum": [
       "pass",
       "repeat",
       "needs_work"
      ]
     },
     "sabqi": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "pass",
       "repeat",
       "needs_work",
       null
      ]
     },
     "manzil": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "pass",
       "repeat",
       "needs_work",
       null
      ]
     }
    }
   },
   "LiveSession": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "kind": {
      "type": "string",
      "enum": [
       "sabaq"
      ]
     },
     "status": {
      "type": "string",
      "enum": [
       "scheduled",
       "live",
       "ended",
       "missed",
       "cancelled"
      ]
     },
     "starts_at": {
      "type": "string",
      "format": "date-time"
     },
     "minutes": {
      "type": "integer"
     },
     "ends_by": {
      "type": "string",
      "description": "Started: when the time is up. Scheduled: the planned end.",
      "format": "date-time"
     },
     "mode": {
      "type": "string",
      "enum": [
       "audio"
      ]
     },
     "recorded": {
      "type": "boolean",
      "description": "Always true: every one-to-one session is recorded."
     },
     "teacher": {
      "type": "object",
      "properties": {
       "name": {
        "type": "string",
        "maxLength": 120
       }
      },
      "required": [
       "name"
      ]
     },
     "range": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "surah": {
          "type": "integer"
         },
         "ayah_from": {
          "type": "integer"
         },
         "ayah_to": {
          "type": "integer"
         }
        },
        "required": [
         "surah",
         "ayah_from",
         "ayah_to"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "can_join": {
      "type": "boolean"
     },
     "join_blocked": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "LIVE_NO_CONSENT",
       "LIVE_NOT_STARTED",
       "LIVE_ENDED",
       "LIVE_DISABLED",
       "LIVE_CAP_REACHED",
       null
      ]
     },
     "result": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LiveResultParts"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "id",
     "kind",
     "status",
     "starts_at",
     "minutes",
     "ends_by",
     "mode",
     "recorded",
     "teacher",
     "range",
     "can_join",
     "join_blocked",
     "result"
    ]
   },
   "LiveMark": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "surah": {
      "type": "integer"
     },
     "ayah": {
      "type": "integer"
     },
     "word": {
      "type": "integer",
      "description": "0-based word index, the same as the Quran partner contract's word_index."
     },
     "kind": {
      "type": "string",
      "enum": [
       "tajweed",
       "wrong_word",
       "skipped",
       "added",
       "hesitation",
       "makhraj"
      ]
     },
     "rule": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "ghunnah",
       "madd",
       "qalqalah",
       "ikhfa",
       "idgham",
       "iqlab",
       "izhar",
       "other",
       null
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "at": {
      "type": "string",
      "description": "Server time; replay works out the offset from the recording's start.",
      "format": "date-time"
     },
     "removed": {
      "type": "boolean"
     }
    },
    "required": [
     "id",
     "surah",
     "ayah",
     "word",
     "kind",
     "rule",
     "note",
     "at",
     "removed"
    ]
   },
   "LiveJoin": {
    "type": "object",
    "properties": {
     "url": {
      "type": "string",
      "description": "The audio service address the app connects to. Never shown to anyone."
     },
     "token": {
      "type": "string",
      "description": "The join pass, valid 10 minutes; refresh it with the token route."
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "role": {
      "type": "string",
      "enum": [
       "teacher",
       "student",
       "listener"
      ]
     },
     "mode": {
      "type": "string",
      "enum": [
       "audio"
      ]
     },
     "recording": {
      "type": "boolean"
     },
     "ends_by": {
      "type": "string",
      "format": "date-time"
     },
     "page": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "surah": {
          "type": "integer"
         },
         "ayah": {
          "type": "integer"
         }
        },
        "required": [
         "surah",
         "ayah"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "marks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LiveMark"
      }
     },
     "identity": {
      "type": "string",
      "description": "The caller's id in the room."
     },
     "name": {
      "type": "string",
      "description": "The name shown to everyone in the room."
     }
    },
    "required": [
     "url",
     "token",
     "expires_at",
     "role",
     "mode",
     "recording",
     "ends_by",
     "page",
     "marks"
    ]
   },
   "LiveToken": {
    "type": "object",
    "properties": {
     "token": {
      "type": "string"
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "token",
     "expires_at"
    ]
   },
   "LiveReview": {
    "type": "object",
    "properties": {
     "session": {
      "$ref": "#/components/schemas/LiveSession"
     },
     "attendance": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "joined_at": {
          "type": "string",
          "format": "date-time"
         },
         "left_at": {
          "type": [
           "string",
           "null"
          ],
          "format": "date-time"
         },
         "seconds": {
          "type": "integer"
         }
        },
        "required": [
         "joined_at",
         "seconds"
        ]
       },
       {
        "type": "null"
       }
      ],
      "description": "When the student was in the room (this session only)."
     },
     "marks": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LiveMark"
      }
     },
     "result": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/LiveResultParts"
       },
       {
        "type": "null"
       }
      ]
     },
     "next_assignment": {
      "type": [
       "string",
       "null"
      ]
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "recording": {
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "available": {
          "const": true
         },
         "started_at": {
          "type": "string",
          "format": "date-time"
         },
         "duration_seconds": {
          "type": "integer"
         }
        },
        "required": [
         "available",
         "started_at",
         "duration_seconds"
        ]
       },
       {
        "type": "object",
        "properties": {
         "available": {
          "const": false
         }
        },
        "required": [
         "available"
        ]
       }
      ]
     }
    },
    "required": [
     "session",
     "attendance",
     "marks",
     "result",
     "next_assignment",
     "note",
     "recording"
    ]
   },
   "LivePlayLink": {
    "type": "object",
    "properties": {
     "url": {
      "type": "string",
      "description": "A 10-minute link on the API host that streams the recording. Each play is logged; recordings are never downloadable."
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "content_type": {
      "type": "string",
      "maxLength": 40
     },
     "duration_seconds": {
      "type": "integer"
     }
    },
    "required": [
     "url",
     "expires_at",
     "content_type",
     "duration_seconds"
    ]
   },
   "LiveReportOpened": {
    "type": "object",
    "properties": {
     "report_id": {
      "type": "string",
      "format": "uuid"
     },
     "status": {
      "type": "string",
      "enum": [
       "open"
      ]
     }
    },
    "required": [
     "report_id",
     "status"
    ]
   },
   "LiveAny": {
    "type": "object",
    "description": "See the Live Sabaq TDD (docs/tdd.md, section 6I) for the full shape."
   }
  }
 }
}