{
 "openapi": "3.1.0",
 "info": {
  "title": "saMas School Student API",
  "version": "1.0.0-draft",
  "description": "The Student API serves the signed-in person: a student, or a parent acting for their children. It is what the saMas School mobile apps use and what a family-facing partner app may use with the family's consent.\n\nAuthentication: a session token (Bearer). Everything is scoped to the caller: a parent sees the children linked to them and their family account; a student sees only themself. There is no way to name another family.\n\nWrites are limited to what a family does at the school counter: upload a payment proof, raise a dispute on an invoice, mark notifications read.",
  "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": [
  {
   "clerkToken": []
  }
 ],
 "tags": [
  {
   "name": "Profile",
   "description": "The signed-in person, their contexts and devices."
  },
  {
   "name": "Attendance",
   "description": "Attendance for the caller's children."
  },
  {
   "name": "Timetable",
   "description": "Published timetables."
  },
  {
   "name": "Grades",
   "description": "Published marks."
  },
  {
   "name": "Homework",
   "description": "Homework for the children, and handing it in."
  },
  {
   "name": "Flex",
   "description": "Choosing a Flex session."
  },
  {
   "name": "Work cycle",
   "description": "Montessori work plans, the journal and small-group lessons."
  },
  {
   "name": "Weekly plan",
   "description": "The week ahead from the class teacher, and ticking goals."
  },
  {
   "name": "Exams",
   "description": "Date sheets and published results."
  },
  {
   "name": "Notices",
   "description": "Announcements, calendar and notifications."
  },
  {
   "name": "Fees",
   "description": "Invoices, receipts and disputes for the family."
  },
  {
   "name": "Payment proofs",
   "description": "Upload a bank or wallet slip for the office to match."
  },
  {
   "name": "AI assistants",
   "description": "Connections to the school MCP server (staff)."
  },
  {
   "name": "Admissions",
   "description": "Apply for a place at a school and follow the application."
  },
  {
   "name": "Messages",
   "description": "Conversations with teachers, parents and the office."
  },
  {
   "name": "Forms",
   "description": "Permission slips to answer."
  },
  {
   "name": "Help",
   "description": "Ask the help buddy (Mithoo or the owl), send a question on, read the team's replies; and tell the saMas help team how help is doing: panels opened, guides voted on, steps that could not be found, searches with no results."
  },
  {
   "name": "Getting started",
   "description": "The signed-in staff member's getting started checklist: items ticked by the app or by the person, skipping with a reason, and hiding the page."
  },
  {
   "name": "Issues",
   "description": "Report a problem, an idea or a school matter, and follow it: the conversation, the timeline, is it fixed for you."
  },
  {
   "name": "Quran",
   "description": "A child's Quran lessons, juz map and tajweed notes when the school runs the Quran Madrasa add-on."
  },
  {
   "name": "Learn",
   "description": "A scholar's courses, days of notes with pictures, lecture week and History, when the school runs the Learn add-on."
  },
  {
   "name": "Live Sabaq",
   "description": "Live one-to-one audio sabaq: scheduling, joining, marks, results, recordings, reports, limits and the audit log (the Live Sabaq add-on)."
  }
 ],
 "paths": {
  "/me/session": {
   "get": {
    "tags": [
     "Profile"
    ],
    "summary": "My session",
    "description": "Everything an app needs to render for the signed-in person: who they are, who the app is acting as, their roles at each school, any View-as session, and whether writes are allowed. A first-time sign-in is linked to the school that invited it; someone nobody invited gets 403 NOT_INVITED.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one Session.\n\n| Field | Type | Notes |\n|---|---|---|\n| `actor` | object | Always present; The person who signed in. |\n| `effective` | object | Always present; The person the app behaves as: the actor, or the person being viewed during View-as. |\n| `role` | SessionRole | May be omitted. |\n| `school_id` | string | May be omitted; may be null; UUID; The active school. |\n| `is_super_admin` | boolean | Always present; False while viewing as someone, even for a super admin. |\n| `is_staff` | boolean | May be omitted; saMas staff or a super admin (not while viewing as someone): Labs is shown. |\n| `can_write` | boolean | Always present; False during a read-only View-as session; writes are refused. |\n| `memberships` | array of Membership | Always present. |\n| `view_as` | ViewAsSession | May be omitted. |\n| `view_as_open_targets` | boolean | May be omitted; 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. |\n| `branding` | Branding | May be omitted. |\n| `add_ons` | array of string | May be omitted; The add-ons that are on (or in trial) for the active school; the app shows their pages only then. |\n| `color_mode` | object | May be omitted; The signed-in person's own choice of light or dark, kept on their account. |\n| `admin_site_url` | string | May be omitted; may be null; https URL; 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. |\n| `admin_site_required` | boolean | May be omitted; True when admin work is only accepted through the admin site (the regular site should send admins there). |\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/Session"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "actor": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "Hina Siddiqui",
           "email": "parent@example.pk",
           "phone": "+923001234567",
           "role": "school_admin",
           "organization_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "school_name": "saMas School Demo",
           "school_code": "DEMO",
           "is_super_admin": true,
           "is_active": true
          },
          "effective": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "Hina Siddiqui",
           "email": "parent@example.pk",
           "phone": "+923001234567",
           "role": "school_admin",
           "organization_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "school_name": "saMas School Demo",
           "school_code": "DEMO",
           "is_super_admin": true,
           "is_active": true
          },
          "role": "school_admin",
          "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "is_super_admin": true,
          "is_staff": true,
          "can_write": true,
          "memberships": [
           {
            "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "school_name": "string",
            "school_code": "string",
            "role": "school_admin",
            "is_default": true,
            "is_active": true,
            "web_url": "https://erp.example.pk/samas/hook"
           }
          ],
          "view_as": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "target_user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "target_name": "string",
           "target_email": "string",
           "target_role": "school_admin",
           "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "school_name": "string",
           "mode": "read_only",
           "reason": "string",
           "started_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "extended_at": "2026-09-22T09:30:00Z",
           "can_extend": true
          },
          "view_as_open_targets": true,
          "branding": {
           "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"
          },
          "add_ons": [
           "madrasa"
          ],
          "color_mode": {},
          "admin_site_url": "https://erp.example.pk/samas/hook",
          "admin_site_required": 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"
     }
    }
   }
  },
  "/me/search": {
   "get": {
    "tags": [
     "Profile"
    ],
    "summary": "Search my school",
    "description": "Everything the caller is allowed to see, ranked. A parent finds their own children, family and invoices; staff find what their role shows them.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `q` | query | string | Required; 2 to 80 characters; What to look for: a name (typos are fine), admission number, phone, invoice or receipt number. |\n| `kind` | query | string | Optional; one of `student`, `person`, `family`, `invoice`, `receipt`. |\n| `limit` | query | integer | Optional; from 1 to 50. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of SearchHit.\n\n| Field | Type | Notes |\n|---|---|---|\n| `kind` | string | Always present; one of `student`, `person`, `family`, `invoice`, `receipt`. |\n| `id` | string | Always present; UUID; Open it with the matching endpoint for its kind. |\n| `label` | string | Always present; Name, invoice number or receipt number. |\n| `subtitle` | string | May be omitted; may be null; Short context: admission number and class, role and phone, student and month, family and amount. |\n| `score` | number | Always present; Higher is a better match. Exact matches score 2, prefixes 1.5, fuzzy matches 0.3 to 1. |\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",
      "required": true,
      "schema": {
       "type": "string",
       "description": "What to look for: a name (typos are fine), admission number, phone, invoice or receipt number.",
       "minLength": 2,
       "maxLength": 80
      },
      "description": "At least 2 characters. Invoice and receipt numbers need at least 3."
     },
     {
      "name": "kind",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "student",
        "person",
        "family",
        "invoice",
        "receipt"
       ]
      },
      "description": "Only this kind. Leave out for all kinds."
     },
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 50,
       "default": 20
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/SearchHit"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "kind": "student",
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "label": "Ali Khan",
           "subtitle": "ADM-0042 . Grade 5 A",
           "score": 1.5
          }
         ],
         "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"
     }
    }
   }
  },
  "/me": {
   "get": {
    "tags": [
     "Profile"
    ],
    "summary": "Who am I",
    "description": "The caller, their active role and school, and the children they can act for.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `person` | Person | Always present. |\n| `school` | School | May be omitted. |\n| `children` | array of object | 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": {
           "type": "object",
           "properties": {
            "person": {
             "$ref": "#/components/schemas/Person"
            },
            "school": {
             "oneOf": [
              {
               "$ref": "#/components/schemas/School"
              },
              {
               "type": "null"
              }
             ],
             "description": "Null for a super admin in the platform view (no school picked)."
            },
            "children": {
             "type": "array",
             "items": {
              "type": "object",
              "properties": {
               "id": {
                "type": "string",
                "format": "uuid"
               },
               "full_name": {
                "type": "string"
               },
               "admission_no": {
                "type": "string"
               },
               "section_id": {
                "type": [
                 "string",
                 "null"
                ],
                "format": "uuid"
               },
               "class_name": {
                "type": [
                 "string",
                 "null"
                ]
               },
               "section_name": {
                "type": [
                 "string",
                 "null"
                ]
               },
               "relation": {
                "type": [
                 "string",
                 "null"
                ]
               }
              },
              "required": [
               "id",
               "full_name",
               "admission_no"
              ]
             }
            }
           },
           "required": [
            "person",
            "children"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "person": {
           "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
          },
          "school": {
           "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"
           }
          },
          "children": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "admission_no": "string",
            "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "class_name": "string",
            "section_name": "string",
            "relation": "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"
     }
    }
   }
  },
  "/me/contexts": {
   "get": {
    "tags": [
     "Profile"
    ],
    "summary": "Roles and schools I hold",
    "description": "One person may be a parent at one school and a teacher at another. Switching changes what every other endpoint returns.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `school` | School | May be omitted. |\n| `role` | Role | May be omitted; one of `school_admin`, `school_principal`, `school_finance`, `teacher`, `class_head`, `parent`, `student`, `school_prefect`. |\n| `is_active` | 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"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "school": {
              "$ref": "#/components/schemas/School"
             },
             "role": {
              "$ref": "#/components/schemas/Role"
             },
             "is_active": {
              "type": "boolean"
             }
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "school": {
            "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"
            }
           },
           "role": "school_admin",
           "is_active": 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"
     }
    }
   }
  },
  "/me/contexts/switch": {
   "post": {
    "tags": [
     "Profile"
    ],
    "summary": "Switch active role or school",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "school_id": {
          "type": "string",
          "format": "uuid",
          "maxLength": 40
         },
         "role": {
          "$ref": "#/components/schemas/Role"
         }
        },
        "required": [
         "school_id",
         "role"
        ],
        "additionalProperties": false
       },
       "example": {
        "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "role": "school_admin"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Person"
          }
         },
         "required": [
          "data"
         ]
        },
        "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
         },
         "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": "Switch active role or school.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `school_id` | string | Required; UUID; at most 40 characters. |\n| `role` | Role | Required; one of `school_admin`, `school_principal`, `school_finance`, `teacher`, `class_head`, `parent`, `student`, `school_prefect`. |\n\n**Response 200:** `{ data, meta }` where `data` is one 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- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/me/schools": {
   "get": {
    "tags": [
     "Profile"
    ],
    "summary": "Schools I can switch to",
    "description": "For the school picker: each school with its name, main color, logo, web address and the caller's roles there. A super admin gets every school (their own roles first). Searches name, code and web address.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `q` | query | string | Optional; at most 200 characters; Part of a school's name, its code or its web address. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of SchoolChoice.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `code` | string | Always present. |\n| `name` | string | Always present. |\n| `display_name` | string | Always present. |\n| `primary_color` | string | Always present; pattern `^#[0-9a-f]{6}$`. |\n| `logo_url` | string | May be omitted; may be null; https URL. |\n| `web_url` | string | May be omitted; may be null; https URL; The school's own web address, when it has one. |\n| `roles` | array of Role | Always present; The caller's roles at this school, the default first. Empty for a super admin with no role there. |\n| `is_current` | boolean | Always present; The school the caller works in now. |\n| `is_active` | boolean | Always present; False for a suspended school. |\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",
       "description": "Part of a school's name, its code or its web address.",
       "maxLength": 200
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/SchoolChoice"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "code": "DEMO",
           "name": "saMas School Demo",
           "display_name": "saMas School Demo",
           "primary_color": "#1e3a8a",
           "logo_url": "https://erp.example.pk/samas/hook",
           "web_url": "https://erp.example.pk/samas/hook",
           "roles": [
            "school_admin"
           ],
           "is_current": true,
           "is_active": 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"
     }
    }
   }
  },
  "/me/working-school": {
   "put": {
    "tags": [
     "Profile"
    ],
    "summary": "Choose the school I work in",
    "description": "Moves the caller into a school. Someone with roles there works in their default role unless they name one; a super admin may pick any school, or null to leave, and keeps their super admin powers. Refused during View-as. Returns the new session.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `school_id` | string | Required; may be null; UUID; at most 40 characters. |\n| `role` | Role | Optional; one of `school_admin`, `school_principal`, `school_finance`, `teacher`, `class_head`, `parent`, `student`, `school_prefect`. |\n\n**Response 200:** `{ data, meta }` where `data` is one Session.\n\n| Field | Type | Notes |\n|---|---|---|\n| `actor` | object | Always present; The person who signed in. |\n| `effective` | object | Always present; The person the app behaves as: the actor, or the person being viewed during View-as. |\n| `role` | SessionRole | May be omitted. |\n| `school_id` | string | May be omitted; may be null; UUID; The active school. |\n| `is_super_admin` | boolean | Always present; False while viewing as someone, even for a super admin. |\n| `is_staff` | boolean | May be omitted; saMas staff or a super admin (not while viewing as someone): Labs is shown. |\n| `can_write` | boolean | Always present; False during a read-only View-as session; writes are refused. |\n| `memberships` | array of Membership | Always present. |\n| `view_as` | ViewAsSession | May be omitted. |\n| `view_as_open_targets` | boolean | May be omitted; 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. |\n| `branding` | Branding | May be omitted. |\n| `add_ons` | array of string | May be omitted; The add-ons that are on (or in trial) for the active school; the app shows their pages only then. |\n| `color_mode` | object | May be omitted; The signed-in person's own choice of light or dark, kept on their account. |\n| `admin_site_url` | string | May be omitted; may be null; https URL; 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. |\n| `admin_site_required` | boolean | May be omitted; True when admin work is only accepted through the admin site (the regular site should send admins there). |\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": {
         "school_id": {
          "type": [
           "string",
           "null"
          ],
          "format": "uuid",
          "maxLength": 40
         },
         "role": {
          "$ref": "#/components/schemas/Role"
         }
        },
        "required": [
         "school_id"
        ],
        "additionalProperties": false
       },
       "example": {
        "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "role": "school_admin"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Session"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "actor": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "Hina Siddiqui",
           "email": "parent@example.pk",
           "phone": "+923001234567",
           "role": "school_admin",
           "organization_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "school_name": "saMas School Demo",
           "school_code": "DEMO",
           "is_super_admin": true,
           "is_active": true
          },
          "effective": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "Hina Siddiqui",
           "email": "parent@example.pk",
           "phone": "+923001234567",
           "role": "school_admin",
           "organization_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "school_name": "saMas School Demo",
           "school_code": "DEMO",
           "is_super_admin": true,
           "is_active": true
          },
          "role": "school_admin",
          "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "is_super_admin": true,
          "is_staff": true,
          "can_write": true,
          "memberships": [
           {
            "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "school_name": "string",
            "school_code": "string",
            "role": "school_admin",
            "is_default": true,
            "is_active": true,
            "web_url": "https://erp.example.pk/samas/hook"
           }
          ],
          "view_as": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "target_user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "target_name": "string",
           "target_email": "string",
           "target_role": "school_admin",
           "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "school_name": "string",
           "mode": "read_only",
           "reason": "string",
           "started_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "extended_at": "2026-09-22T09:30:00Z",
           "can_extend": true
          },
          "view_as_open_targets": true,
          "branding": {
           "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"
          },
          "add_ons": [
           "madrasa"
          ],
          "color_mode": {},
          "admin_site_url": "https://erp.example.pk/samas/hook",
          "admin_site_required": 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"
     }
    }
   }
  },
  "/me/preferences": {
   "put": {
    "tags": [
     "Profile"
    ],
    "summary": "Save my preferences",
    "description": "Settings that follow the person to every device. Only the fields sent change.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `color_mode` | ColorMode | Optional; one of `light`, `dark`, `system`; Light, dark, or follow the device. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `color_mode` | ColorMode | Always present; one of `light`, `dark`, `system`; Light, dark, or follow the device. |\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": {
         "color_mode": {
          "$ref": "#/components/schemas/ColorMode"
         }
        },
        "additionalProperties": false
       },
       "example": {
        "color_mode": "light"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "color_mode": {
             "$ref": "#/components/schemas/ColorMode"
            }
           },
           "required": [
            "color_mode"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "color_mode": "light"
         },
         "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"
     }
    }
   }
  },
  "/me/students/{student_id}/attendance": {
   "get": {
    "tags": [
     "Attendance"
    ],
    "summary": "Attendance for a child",
    "description": "Whole-day records in a date range with the summary. Defaults to the current month.\n\n**Who can call:** signed-in person (Bearer session token).\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\n**Response 200:** `{ data, meta }` where `data` is one AttendanceHistory.\n\n| Field | Type | Notes |\n|---|---|---|\n| `records` | array of AttendanceRecord | Always present. |\n| `summary` | AttendanceSummary | 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- 429 RATE_LIMITED (see Retry-After)",
    "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
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/AttendanceHistory"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "records": [
           {
            "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"
           }
          ],
          "summary": {
           "present": 1,
           "absent": 1,
           "late": 1,
           "excused": 1,
           "total": 1,
           "pct": 1.5
          }
         },
         "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"
     }
    }
   }
  },
  "/me/students/{student_id}/timetable": {
   "get": {
    "tags": [
     "Timetable"
    ],
    "summary": "Weekly timetable for a child",
    "description": "Published slots only, with the school's period times.\n\n**Who can call:** signed-in person (Bearer session token).\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 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/student_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"
     }
    }
   }
  },
  "/me/students/{student_id}/shift": {
   "get": {
    "tags": [
     "Timetable"
    ],
    "summary": "A child's shift",
    "description": "The child's shift, today's bell times and when they next change. A student passes their own id.\n\n**Who can call:** signed-in person (Bearer session token).\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 ChildShift.\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/student_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/ChildShift"
            },
            {
             "type": "null"
            }
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "name": "string",
          "colour": "string",
          "weekdays": [
           1
          ],
          "day_rules": {},
          "today": {
           "on_date": "2026-09-01",
           "blocks": [
            {
             "period_no": 1,
             "label": "string",
             "block_type": "string",
             "starts_at": "string",
             "ends_at": "string"
            }
           ]
          },
          "timing_today": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "closures": [
           {
            "on_date": "2026-09-01",
            "reason": "string",
            "follows": 1
           }
          ],
          "next_change": {
           "effective_from": "2026-09-01"
          }
         },
         "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"
     }
    }
   }
  },
  "/me/students/{student_id}/flex": {
   "get": {
    "tags": [
     "Flex"
    ],
    "summary": "A child's Flex choices",
    "description": "The child's Flex blocks of a day (today unless on is given): each session with seats left, the child's place (chosen, requested by a teacher, or the study hall) and the sessions they wait for.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `on` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one StudentFlex.\n\n| Field | Type | Notes |\n|---|---|---|\n| `on` | string | Always present; date YYYY-MM-DD. |\n| `closed` | boolean | Always present. |\n| `cutoff` | string | Always present; at most 5 characters. |\n| `blocks` | array of object | 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/student_id"
     },
     {
      "name": "on",
      "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": {
           "$ref": "#/components/schemas/StudentFlex"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "on": "2026-09-01",
          "closed": true,
          "cutoff": "string",
          "blocks": [
           {
            "shift_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "period_no": 1,
            "label": "string",
            "starts_at": "string",
            "ends_at": "string",
            "place": {
             "offering_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "kind": "chosen"
            },
            "waiting": [
             "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
            ],
            "offerings": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "title": "string",
              "is_default": true,
              "room": "string",
              "teacher": "string",
              "seats_left": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/flex/{on}/{period_no}": {
   "put": {
    "tags": [
     "Flex"
    ],
    "summary": "Choose a Flex session",
    "description": "The student chooses or switches in one step until the cut-off (FLEX_CLOSED). A full session is FLEX_FULL unless waitlist is true, which joins its waiting list. A teacher's request stands (FLEX_REQUESTED).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `on` | path | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `period_no` | path | integer | Required; from 1 to 24. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `offering_id` | string | Required; UUID; at most 40 characters. |\n| `waitlist` | boolean | Optional. |\n\n**Response 200:** `{ data, meta }` where `data` is one StudentFlex.\n\n| Field | Type | Notes |\n|---|---|---|\n| `on` | string | Always present; date YYYY-MM-DD. |\n| `closed` | boolean | Always present. |\n| `cutoff` | string | Always present; at most 5 characters. |\n| `blocks` | array of object | 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"
     },
     {
      "name": "on",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date",
       "maxLength": 40
      }
     },
     {
      "name": "period_no",
      "in": "path",
      "required": true,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 24
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "offering_id": {
          "type": "string",
          "format": "uuid",
          "maxLength": 40
         },
         "waitlist": {
          "type": "boolean"
         }
        },
        "required": [
         "offering_id"
        ],
        "additionalProperties": false
       },
       "example": {
        "offering_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "waitlist": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/StudentFlex"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "on": "2026-09-01",
          "closed": true,
          "cutoff": "string",
          "blocks": [
           {
            "shift_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "period_no": 1,
            "label": "string",
            "starts_at": "string",
            "ends_at": "string",
            "place": {
             "offering_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "kind": "chosen"
            },
            "waiting": [
             "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
            ],
            "offerings": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "title": "string",
              "is_default": true,
              "room": "string",
              "teacher": "string",
              "seats_left": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/work-plans/{week}": {
   "get": {
    "tags": [
     "Work cycle"
    ],
    "summary": "A child's work plan",
    "description": "Montessori classes: the week's work plan (must do with a count, may do, and how many times it was done), the journal of the week and the child's small-group lessons. journal_open says whether the child can add to the journal now.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `week` | path | string | Required; date YYYY-MM-DD; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one WorkPlan.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student` | object | Always present. |\n| `section` | object | May be omitted. |\n| `week` | string | Always present; date YYYY-MM-DD; The Monday. |\n| `montessori` | boolean | Always present; The child's class has a work cycle. |\n| `work_cycles_today` | array of CycleTime | Always present. |\n| `journal_open` | boolean | Always present; The caller is the child and a work cycle is on now. |\n| `items` | array of WorkPlanItem | Always present. |\n| `journal` | array of JournalEntry | Always present. |\n| `small_groups` | array of SmallGroupLesson | Always present. |\n| `subjects` | array of object | Always present. |\n| `can_write` | boolean | May be omitted; Staff: the caller writes the plan and the journal. |\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/student_id"
     },
     {
      "name": "week",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date",
       "maxLength": 40
      },
      "description": "Any date in the week; the plan is for its Monday."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WorkPlan"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "label": "string"
          },
          "week": "2026-09-01",
          "montessori": true,
          "work_cycles_today": [
           {
            "starts_at": "string",
            "ends_at": "string"
           }
          ],
          "journal_open": true,
          "items": [
           {
            "subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "subject": "string",
            "kind": "must",
            "target": 1,
            "done": 1
           }
          ],
          "journal": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "on": "2026-09-01",
            "started_at": "string",
            "minutes": 1,
            "subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "subject": "string",
            "note": "string",
            "by_student": true,
            "added_by": "string"
           }
          ],
          "small_groups": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "section_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "on": "2026-09-01",
            "starts_at": "string",
            "title": "string",
            "material": "string",
            "place": "string",
            "teacher": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "students": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "full_name": "string"
             }
            ]
           }
          ],
          "subjects": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string"
           }
          ],
          "can_write": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/journal": {
   "post": {
    "tags": [
     "Work cycle"
    ],
    "summary": "Add to my journal",
    "description": "The child adds the work they chose and for how long, during a work cycle of their class today (JOURNAL_CLOSED otherwise).\n\n**Who can call:** signed-in person (Bearer session token).\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| `on` | string | Optional; date YYYY-MM-DD; at most 40 characters; Staff only: the day (today or before). |\n| `started_at` | string | Optional; pattern `^([01]\\d\\|2[0-3]):[0-5]\\d$`; at most 5 characters. |\n| `minutes` | integer | Required; from 1 to 240. |\n| `subject_id` | string | Optional; may be null; UUID; at most 40 characters. |\n| `note` | string | Required; 1 to 120 characters; The work the child chose. |\n\n**Response 201:** `{ data, meta }` where `data` is one JournalEntry.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `on` | string | Always present; date YYYY-MM-DD. |\n| `started_at` | string | May be omitted; may be null; at most 5 characters. |\n| `minutes` | integer | Always present. |\n| `subject_id` | string | May be omitted; may be null; UUID. |\n| `subject` | string | May be omitted; may be null. |\n| `note` | string | Always present; at most 120 characters; The work. |\n| `by_student` | boolean | Always present. |\n| `added_by` | 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- 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/JournalWrite"
       },
       "example": {
        "on": "2026-09-01",
        "started_at": "string",
        "minutes": 1,
        "subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "note": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/JournalEntry"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "on": "2026-09-01",
          "started_at": "string",
          "minutes": 1,
          "subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "subject": "string",
          "note": "string",
          "by_student": true,
          "added_by": "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"
     }
    }
   }
  },
  "/me/students/{student_id}/weekly-plans/{week}": {
   "get": {
    "tags": [
     "Weekly plan"
    ],
    "summary": "A child's weekly plan",
    "description": "Published plans only (otherwise published is false and nothing else is shown); the child's own lessons and options, and whether the child ticked each goal.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `week` | path | string | Required; date YYYY-MM-DD; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one WeeklyPlan.\n\n| Field | Type | Notes |\n|---|---|---|\n| `section` | object | Always present. |\n| `week` | string | Always present; date YYYY-MM-DD; The Monday of the week. |\n| `published` | boolean | Always present. |\n| `published_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `changed` | boolean | May be omitted; Edited after it was published. |\n| `auto_published` | boolean | May be omitted. |\n| `class_teacher` | PersonRef | May be omitted. |\n| `note` | string | May be omitted; may be null; at most 1000 characters. |\n| `bring` | array of object | May be omitted. |\n| `day_notes` | array of object | May be omitted; The class teacher's line for each day: what the class will do. |\n| `goals` | array of WeeklyPlanGoal | May be omitted. |\n| `days` | array of WeeklyPlanDay | May be omitted. |\n| `can_lead` | boolean | May be omitted; Staff: the caller writes the note and publishes. |\n| `students` | integer | May be omitted. |\n| `publish_at` | PlanPublish | May be omitted. |\n| `sections_written` | integer | May be omitted. |\n| `copied` | 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/student_id"
     },
     {
      "name": "week",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date",
       "maxLength": 40
      },
      "description": "Any date in the week; the plan is for its Monday."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WeeklyPlan"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "label": "string"
          },
          "week": "2026-09-01",
          "published": true,
          "published_at": "2026-09-22T09:30:00Z",
          "changed": true,
          "auto_published": true,
          "class_teacher": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "note": "string",
          "bring": [
           {
            "weekday": 1,
            "text": "string"
           }
          ],
          "day_notes": [
           {
            "weekday": 1,
            "text": "string"
           }
          ],
          "goals": [
           {
            "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "subject": "string",
            "teacher": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "goal_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "goal": "string",
            "ticked": true,
            "can_write": true,
            "ticks": 1,
            "ticked_by": [
             "string"
            ],
            "also_sections": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "label": "string"
             }
            ]
           }
          ],
          "days": [
           {
            "weekday": 1,
            "on": "2026-09-01",
            "lessons": [
             {
              "period_no": 1,
              "starts_at": "string",
              "ends_at": "string",
              "subject": "string",
              "teacher": "string",
              "room": "string",
              "option": true,
              "section": "string"
             }
            ],
            "homework_due": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "title": "string",
              "subject": "string"
             }
            ],
            "tests": [
             {
              "title": "string",
              "subject": "string",
              "kind": "string"
             }
            ],
            "bring": [
             "string"
            ],
            "note": "string",
            "duties": [
             {
              "name": "string",
              "students": []
             }
            ]
           }
          ],
          "can_lead": true,
          "students": 1,
          "publish_at": {
           "weekday": 1,
           "time": "14:00"
          },
          "sections_written": 1,
          "copied": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/weekly-plans/{week}/goals/{goal_id}/tick": {
   "post": {
    "tags": [
     "Weekly plan"
    ],
    "summary": "Tick a goal",
    "description": "The student ticks a goal they reached, until the Monday after the week (TICK_CLOSED).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `week` | path | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `goal_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one WeeklyPlan.\n\n| Field | Type | Notes |\n|---|---|---|\n| `section` | object | Always present. |\n| `week` | string | Always present; date YYYY-MM-DD; The Monday of the week. |\n| `published` | boolean | Always present. |\n| `published_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `changed` | boolean | May be omitted; Edited after it was published. |\n| `auto_published` | boolean | May be omitted. |\n| `class_teacher` | PersonRef | May be omitted. |\n| `note` | string | May be omitted; may be null; at most 1000 characters. |\n| `bring` | array of object | May be omitted. |\n| `day_notes` | array of object | May be omitted; The class teacher's line for each day: what the class will do. |\n| `goals` | array of WeeklyPlanGoal | May be omitted. |\n| `days` | array of WeeklyPlanDay | May be omitted. |\n| `can_lead` | boolean | May be omitted; Staff: the caller writes the note and publishes. |\n| `students` | integer | May be omitted. |\n| `publish_at` | PlanPublish | May be omitted. |\n| `sections_written` | integer | May be omitted. |\n| `copied` | integer | May be omitted. |\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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/student_id"
     },
     {
      "name": "week",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date",
       "maxLength": 40
      },
      "description": "Any date in the week; the plan is for its Monday."
     },
     {
      "name": "goal_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WeeklyPlan"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "label": "string"
          },
          "week": "2026-09-01",
          "published": true,
          "published_at": "2026-09-22T09:30:00Z",
          "changed": true,
          "auto_published": true,
          "class_teacher": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "note": "string",
          "bring": [
           {
            "weekday": 1,
            "text": "string"
           }
          ],
          "day_notes": [
           {
            "weekday": 1,
            "text": "string"
           }
          ],
          "goals": [
           {
            "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "subject": "string",
            "teacher": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "goal_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "goal": "string",
            "ticked": true,
            "can_write": true,
            "ticks": 1,
            "ticked_by": [
             "string"
            ],
            "also_sections": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "label": "string"
             }
            ]
           }
          ],
          "days": [
           {
            "weekday": 1,
            "on": "2026-09-01",
            "lessons": [
             {
              "period_no": 1,
              "starts_at": "string",
              "ends_at": "string",
              "subject": "string",
              "teacher": "string",
              "room": "string",
              "option": true,
              "section": "string"
             }
            ],
            "homework_due": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "title": "string",
              "subject": "string"
             }
            ],
            "tests": [
             {
              "title": "string",
              "subject": "string",
              "kind": "string"
             }
            ],
            "bring": [
             "string"
            ],
            "note": "string",
            "duties": [
             {
              "name": "string",
              "students": []
             }
            ]
           }
          ],
          "can_lead": true,
          "students": 1,
          "publish_at": {
           "weekday": 1,
           "time": "14:00"
          },
          "sections_written": 1,
          "copied": 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"
     }
    }
   },
   "delete": {
    "tags": [
     "Weekly plan"
    ],
    "summary": "Untick a goal",
    "description": "The student takes a tick back, until the Monday after the week (TICK_CLOSED).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `week` | path | string | Required; date YYYY-MM-DD; at most 40 characters. |\n| `goal_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one WeeklyPlan.\n\n| Field | Type | Notes |\n|---|---|---|\n| `section` | object | Always present. |\n| `week` | string | Always present; date YYYY-MM-DD; The Monday of the week. |\n| `published` | boolean | Always present. |\n| `published_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `changed` | boolean | May be omitted; Edited after it was published. |\n| `auto_published` | boolean | May be omitted. |\n| `class_teacher` | PersonRef | May be omitted. |\n| `note` | string | May be omitted; may be null; at most 1000 characters. |\n| `bring` | array of object | May be omitted. |\n| `day_notes` | array of object | May be omitted; The class teacher's line for each day: what the class will do. |\n| `goals` | array of WeeklyPlanGoal | May be omitted. |\n| `days` | array of WeeklyPlanDay | May be omitted. |\n| `can_lead` | boolean | May be omitted; Staff: the caller writes the note and publishes. |\n| `students` | integer | May be omitted. |\n| `publish_at` | PlanPublish | May be omitted. |\n| `sections_written` | integer | May be omitted. |\n| `copied` | 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/student_id"
     },
     {
      "name": "week",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date",
       "maxLength": 40
      },
      "description": "Any date in the week; the plan is for its Monday."
     },
     {
      "name": "goal_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WeeklyPlan"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "section": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "label": "string"
          },
          "week": "2026-09-01",
          "published": true,
          "published_at": "2026-09-22T09:30:00Z",
          "changed": true,
          "auto_published": true,
          "class_teacher": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "note": "string",
          "bring": [
           {
            "weekday": 1,
            "text": "string"
           }
          ],
          "day_notes": [
           {
            "weekday": 1,
            "text": "string"
           }
          ],
          "goals": [
           {
            "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "subject": "string",
            "teacher": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "goal_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "goal": "string",
            "ticked": true,
            "can_write": true,
            "ticks": 1,
            "ticked_by": [
             "string"
            ],
            "also_sections": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "label": "string"
             }
            ]
           }
          ],
          "days": [
           {
            "weekday": 1,
            "on": "2026-09-01",
            "lessons": [
             {
              "period_no": 1,
              "starts_at": "string",
              "ends_at": "string",
              "subject": "string",
              "teacher": "string",
              "room": "string",
              "option": true,
              "section": "string"
             }
            ],
            "homework_due": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "title": "string",
              "subject": "string"
             }
            ],
            "tests": [
             {
              "title": "string",
              "subject": "string",
              "kind": "string"
             }
            ],
            "bring": [
             "string"
            ],
            "note": "string",
            "duties": [
             {
              "name": "string",
              "students": []
             }
            ]
           }
          ],
          "can_lead": true,
          "students": 1,
          "publish_at": {
           "weekday": 1,
           "time": "14:00"
          },
          "sections_written": 1,
          "copied": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/timetable/week": {
   "get": {
    "tags": [
     "Timetable"
    ],
    "summary": "A child's week",
    "description": "The child's class week with the same filters as the office's view (no free periods). A student passes their own id.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `days` | query | string | Optional; pattern `^[1-7](,[1-7]){0,6}$`; at most 13 characters; Weekdays to show, 1 is Monday. |\n| `subject_ids` | query | string | Optional; pattern `^[0-9a-fA-F-]{36}(,[0-9a-fA-F-]{36}){0,19}$`; at most 740 characters; Subjects to keep bright; others fade. |\n| `room` | query | string | Optional; at most 20 characters; A room to keep bright. |\n\n**Response 200:** `{ data, meta }` where `data` is one TimetableWeek.\n\n| Field | Type | Notes |\n|---|---|---|\n| `target` | TimetableTarget | Always present. |\n| `days` | array of integer | Always present. |\n| `periods` | array of Period | Always present. |\n| `lessons` | array of TimetableLesson | Always present. |\n| `shifts` | array of object | May be omitted. |\n| `free` | array of object | May be omitted. |\n| `summary` | object | May be omitted. |\n| `showing` | object | Always present. |\n| `valid_from` | string | May be omitted; may be null; date YYYY-MM-DD; The last time a timetable in this week was published. |\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/student_id"
     },
     {
      "name": "days",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "Weekdays to show, 1 is Monday.",
       "pattern": "^[1-7](,[1-7]){0,6}$",
       "maxLength": 13
      }
     },
     {
      "name": "subject_ids",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "Subjects to keep bright; others fade.",
       "pattern": "^[0-9a-fA-F-]{36}(,[0-9a-fA-F-]{36}){0,19}$",
       "maxLength": 740
      }
     },
     {
      "name": "room",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "A room to keep bright.",
       "maxLength": 20
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TimetableWeek"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "target": {
           "kind": "section",
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "label": "string",
           "sub": "string"
          },
          "days": [
           1
          ],
          "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"
           }
          ],
          "lessons": [
           {
            "weekday": 1,
            "period_no": 1,
            "starts_at": "string",
            "ends_at": "string",
            "subject": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "string"
            },
            "section": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "label": "string"
            },
            "teacher": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "room": "string",
            "shift": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "string",
             "colour": "string"
            },
            "span": 1,
            "with": [
             "string"
            ],
            "option": true,
            "on_date": "2026-09-01",
            "cover": {
             "kind": "cover",
             "on_date": "2026-09-01",
             "teacher": {
              "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "full_name": "string"
             },
             "for": "string",
             "with": "string",
             "note": "string"
            },
            "matches": true,
            "live_url": "https://erp.example.pk/samas/hook"
           }
          ],
          "shifts": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "colour": "string",
            "is_main": true,
            "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"
             }
            ],
            "days": [
             {
              "weekday": 1,
              "on_date": "2026-09-01",
              "kind": "full",
              "ends_after": 1,
              "closed": "string",
              "follows": 1,
              "timing": "string",
              "periods": []
             }
            ]
           }
          ],
          "free": [
           {
            "weekday": 1,
            "period_no": 1,
            "shift_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "starts_at": "string",
            "ends_at": "string"
           }
          ],
          "summary": {
           "lessons": 1,
           "free": 1,
           "days_working": 1,
           "latest_finish": "string",
           "classes": 1,
           "shifts": 1
          },
          "showing": {
           "matching": 1,
           "total": 1
          },
          "valid_from": "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"
     }
    }
   }
  },
  "/me/students/{student_id}/timetable/now": {
   "get": {
    "tags": [
     "Timetable"
    ],
    "summary": "A child's now and next",
    "description": "Now, next and later today for the child's class. A student passes their own id. A lesson with a live-class link carries live_url (HM38).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `at` | query | string | Optional; timestamp with offset or Z (RFC 3339); at most 40 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"
     },
     {
      "$ref": "#/components/parameters/student_id"
     },
     {
      "name": "at",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date-time",
       "maxLength": 40
      }
     }
    ],
    "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"
     }
    }
   }
  },
  "/me/students/{student_id}/timetable/here": {
   "post": {
    "tags": [
     "Timetable"
    ],
    "summary": "I'm here",
    "description": "The student marks herself present for a lesson of her own class that has a live-class link, while it is live: from 10 minutes before it starts to its end, in the school's time (NOT_LIVE_NOW otherwise). Only when the school has turned on self_attendance (SELF_ATTENDANCE_OFF), and only the student herself, never a parent (NOT_ALLOWED). Writes the period's attendance as present, marked by her; a mark a teacher made is never replaced (marked_by_self false), and the teacher's next save replaces hers. A History line (HM40).\n\n**Who can call:** signed-in person (Bearer session token).\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| `on_date` | string | Required; date YYYY-MM-DD; at most 40 characters; Today, in the school's time. |\n| `period_no` | integer | Required; from 1 to 24; The lesson's period (NowLesson.period_no or TimetableLesson.period_no). |\n\n**Response 200:** `{ data, meta }` where `data` is one SelfAttendance.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student_id` | string | Always present; UUID. |\n| `on_date` | string | Always present; date YYYY-MM-DD. |\n| `period_no` | integer | Always present. |\n| `status` | AttendanceStatus | Always present; one of `present`, `absent`, `late`, `excused`. |\n| `marked_by_self` | boolean | Always present; True when the mark is the student's own I'm here; false when a teacher's mark stands (it is never replaced). |\n| `marked_at` | string | Always present; 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)",
    "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/SelfAttendanceWrite"
       },
       "example": {
        "on_date": "2026-09-01",
        "period_no": 1
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/SelfAttendance"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "on_date": "2026-09-01",
          "period_no": 1,
          "status": "present",
          "marked_by_self": true,
          "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"
     }
    }
   }
  },
  "/me/students/{student_id}/grades": {
   "get": {
    "tags": [
     "Grades"
    ],
    "summary": "Published marks for a child",
    "description": "Only assessments the teacher has published. Absent is reported, not scored.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `subject_id` | query | string | Optional; 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| `percent` | number | 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- 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/student_id"
     },
     {
      "name": "subject_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "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"
             },
             "percent": {
              "type": [
               "number",
               "null"
              ]
             }
            }
           }
          }
         },
         "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"
           },
           "percent": 1.5
          }
         ],
         "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"
     }
    }
   }
  },
  "/me/announcements": {
   "get": {
    "tags": [
     "Notices"
    ],
    "summary": "Announcements for me",
    "description": "Notices addressed to everyone, to my role, or to my children's sections. Pinned first.\n\n**Who can call:** signed-in person (Bearer session token).\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"
     }
    }
   }
  },
  "/me/calendar": {
   "get": {
    "tags": [
     "Notices"
    ],
    "summary": "School calendar",
    "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": "School calendar.\n\n**Who can call:** signed-in person (Bearer session token).\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)"
   }
  },
  "/me/notifications": {
   "get": {
    "tags": [
     "Notices"
    ],
    "summary": "My notifications",
    "description": "Fee reminders, View-as notices and other messages the school sent to me.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `unread` | query | boolean | Optional. |\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 Notification.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `kind` | string | Always present. |\n| `title` | string | Always present. |\n| `body` | string | Always present. |\n| `data` | object | May be omitted. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `read_at` | 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"
     },
     {
      "name": "unread",
      "in": "query",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "$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/Notification"
           }
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         },
         "required": [
          "data",
          "page"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "kind": "fee_reminder",
           "title": "string",
           "body": "string",
           "data": {},
           "created_at": "2026-09-22T09:30:00Z",
           "read_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"
     }
    }
   }
  },
  "/me/notifications/{id}/read": {
   "post": {
    "tags": [
     "Notices"
    ],
    "summary": "Mark a notification read",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "204": {
      "description": "Marked."
     },
     "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": "Mark a notification read.\n\n**Who can call:** signed-in person (Bearer session token).\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- 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)"
   }
  },
  "/me/fees": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "Fee summary per child",
    "description": "This month's invoice status, outstanding and overdue amounts, and the next due date for each child.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student_id` | string | May be omitted; UUID. |\n| `student_name` | string | May be omitted. |\n| `family_account_id` | string | May be omitted; may be null; UUID. |\n| `outstanding` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `overdue` | string | May be omitted; pattern `^-?\\d+\\.\\d{2}$`; Amount with two decimals, as a string. |\n| `next_due_date` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `next_due_amount` | string | May be omitted; may be null. |\n| `current_invoice_id` | string | May be omitted; may be null; UUID. |\n| `current_status` | InvoiceStatus | 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": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "student_id": {
              "type": "string",
              "format": "uuid"
             },
             "student_name": {
              "type": "string"
             },
             "family_account_id": {
              "type": [
               "string",
               "null"
              ],
              "format": "uuid"
             },
             "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"
             },
             "next_due_date": {
              "type": [
               "string",
               "null"
              ],
              "format": "date"
             },
             "next_due_amount": {
              "type": [
               "string",
               "null"
              ]
             },
             "current_invoice_id": {
              "type": [
               "string",
               "null"
              ],
              "format": "uuid"
             },
             "current_status": {
              "oneOf": [
               {
                "$ref": "#/components/schemas/InvoiceStatus"
               },
               {
                "type": "null"
               }
              ]
             }
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "student_name": "string",
           "family_account_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "outstanding": "12500.00",
           "overdue": "12500.00",
           "next_due_date": "2026-09-22",
           "next_due_amount": "12500.00",
           "current_invoice_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "current_status": "issued"
          }
         ],
         "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"
     }
    }
   }
  },
  "/me/statement": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "My family's statement",
    "description": "Every charge, payment and refund on your family account between two dates, with the balance carried forward. Up to 366 days. Without dates: the last 30 days up to the school's today. With children on more than one family account, pass family_account_id.\n\n**Who can call:** signed-in person (Bearer session token).\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| `family_account_id` | query | string | Optional; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one FamilyStatement.\n\n| Field | Type | Notes |\n|---|---|---|\n| `family` | object | Always present. |\n| `school_name` | string | May be omitted; may be null. |\n| `currency` | string | Always present. |\n| `from` | string | Always present; date YYYY-MM-DD. |\n| `to` | string | Always present; date YYYY-MM-DD. |\n| `opening_balance` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; What the family owed on the day before 'from'. Negative means the family was in credit. |\n| `closing_balance` | string | Always present; pattern `^-?\\d+\\.\\d{2}$`; What the family owed at the end of 'to'. |\n| `totals` | object | Always present. |\n| `lines` | array of object | Always present. |\n| `generated_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- 429 RATE_LIMITED (see Retry-After)",
    "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": "family_account_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/FamilyStatement"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "family": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "phone": "string",
           "students": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "admission_no": "string",
             "class_name": "string",
             "section_name": "string"
            }
           ]
          },
          "school_name": "string",
          "currency": "PKR",
          "from": "2026-09-01",
          "to": "2026-09-01",
          "opening_balance": "string",
          "closing_balance": "string",
          "totals": {
           "charged": "12500.00",
           "cancelled": "12500.00",
           "paid": "12500.00",
           "reversed": "12500.00",
           "refunded": "12500.00"
          },
          "lines": [
           {
            "date": "2026-09-01",
            "kind": "invoice",
            "reference": "string",
            "description": "string",
            "student": "string",
            "debit": "12500.00",
            "credit": "12500.00",
            "balance": "12500.00",
            "invoice_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "payment_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
           }
          ],
          "generated_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"
     }
    }
   }
  },
  "/me/invoices": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "My family's invoices",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "status",
      "in": "query",
      "schema": {
       "$ref": "#/components/schemas/InvoiceStatus"
      }
     },
     {
      "name": "student_id",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "overdue",
      "in": "query",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "$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"
     }
    },
    "description": "My family's invoices.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `status` | query | InvoiceStatus | Optional. |\n| `student_id` | query | string | Optional; UUID; at most 40 characters. |\n| `overdue` | query | boolean | Optional. |\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)"
   }
  },
  "/me/invoices/{id}": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "One invoice",
    "description": "Items, totals, payments applied and any disputes I raised.\n\n**Who can call:** signed-in person (Bearer session token).\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| `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| `disputes` | array of Flag | 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/Invoice"
            },
            {
             "type": "object",
             "properties": {
              "disputes": {
               "type": "array",
               "items": {
                "$ref": "#/components/schemas/Flag"
               }
              }
             }
            }
           ]
          }
         },
         "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"
           }
          ],
          "disputes": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "entity_type": "invoice",
            "entity_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "category": "dispute",
            "priority": "low",
            "reason": "string",
            "raised_by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "is_resolved": true,
            "resolved_by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "resolved_at": "2026-09-22T09:30:00Z",
            "resolution": "string",
            "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"
     }
    }
   }
  },
  "/me/invoices/{id}/paid": {
   "post": {
    "tags": [
     "Fees"
    ],
    "summary": "I have paid",
    "description": "The parent's answer to a fee reminder: the day, the method, the amount and a reference, with a slip optional. It becomes a claim in the office's proof queue; every finance in-charge is told at once (in the app and by email), reminders for this invoice pause until the office matches or returns it, and the receipt follows the match. One waiting claim per invoice (CLAIM_OPEN); a transfer or a cheque needs a reference or a slip (REFERENCE_REQUIRED); the invoice must be open (INVALID_STATE); 10 a day (TOO_MANY). Idempotency-Key is required on this operation.\n\n**Who can call:** signed-in person (Bearer session token).\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; The day the parent paid; not in the future. |\n| `amount` | string | Required; pattern `^\\d+\\.\\d{2}$`; at most 80 characters; Amount with two decimals, as a string; never negative. |\n| `method` | string | Required; one of `bank_transfer`, `wallet`, `cheque`, `cash`. |\n| `reference` | string | Optional; at most 80 characters; The transaction or cheque number; required for a transfer or a cheque unless a slip is attached. |\n| `upload_id` | string | Optional; at most 200 characters; Optional: a slip uploaded through POST /me/proofs/upload-url. |\n\n**Response 201:** `{ data, meta }` where `data` is one 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- 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/ClaimWrite"
       },
       "example": {
        "paid_on": "2026-09-01",
        "amount": "12500.00",
        "method": "bank_transfer",
        "reference": "string",
        "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Proof"
          }
         },
         "required": [
          "data"
         ]
        },
        "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"
         },
         "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-idempotency-required": true
   }
  },
  "/me/invoices/{id}/disputes": {
   "post": {
    "tags": [
     "Fees"
    ],
    "summary": "Raise a dispute on an invoice",
    "description": "The office sees it on the invoice and replies. The invoice does not change until they resolve it.\n\n**Who can call:** signed-in person (Bearer session token).\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| `reason` | string | Required; 5 to 500 characters. |\n\n**Response 201:** `{ data, meta }` where `data` is one Flag.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `entity_type` | string | Always present; one of `invoice`, `payment`, `proof`. |\n| `entity_id` | string | Always present; UUID. |\n| `category` | string | Always present; one of `dispute`, `amount_discrepancy`, `missing_document`, `duplicate`, `other`. |\n| `priority` | string | May be omitted; one of `low`, `normal`, `high`. |\n| `reason` | string | Always present. |\n| `raised_by` | PersonRef | May be omitted. |\n| `is_resolved` | boolean | Always present. |\n| `resolved_by` | PersonRef | May be omitted. |\n| `resolved_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `resolution` | string | May be omitted; may be null. |\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- 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": {
         "reason": {
          "type": "string",
          "minLength": 5,
          "maxLength": 500
         }
        },
        "required": [
         "reason"
        ],
        "additionalProperties": false
       },
       "example": {
        "reason": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Flag"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "entity_type": "invoice",
          "entity_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "category": "dispute",
          "priority": "low",
          "reason": "string",
          "raised_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "is_resolved": true,
          "resolved_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "resolved_at": "2026-09-22T09:30:00Z",
          "resolution": "string",
          "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"
     }
    }
   }
  },
  "/me/receipts": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "My family's receipts",
    "description": "Confirmed and reversed payments with the invoices they covered.\n\n**Who can call:** signed-in person (Bearer session token).\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| `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)",
    "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
      }
     },
     {
      "$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"
     }
    }
   }
  },
  "/me/ai-connections": {
   "get": {
    "tags": [
     "AI assistants"
    ],
    "summary": "My AI assistant connections",
    "description": "Assistants (Claude, ChatGPT, Grok, Gemini, Claude Code) you connected to the school MCP server.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of AiConnection.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `assistant` | string | Always present. |\n| `person` | string | May be omitted; may be null. |\n| `role` | Role | May be omitted. |\n| `school_name` | string | May be omitted; may be null. |\n| `connected_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `last_used_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `disconnected_at` | 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/AiConnection"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "assistant": "Claude",
           "person": "string",
           "role": "school_admin",
           "school_name": "string",
           "connected_at": "2026-09-22T09:30:00Z",
           "last_used_at": "2026-09-22T09:30:00Z",
           "disconnected_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"
     }
    }
   }
  },
  "/me/ai-connections/{id}": {
   "delete": {
    "tags": [
     "AI assistants"
    ],
    "summary": "Disconnect an assistant",
    "description": "It can no longer read anything; its tokens stop working within a minute.\n\n**Who can call:** signed-in person (Bearer session token).\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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "204": {
      "description": "Disconnected."
     },
     "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"
     }
    }
   }
  },
  "/me/ai-connections/requests/{request}": {
   "get": {
    "tags": [
     "AI assistants"
    ],
    "summary": "A pending connection request",
    "description": "Shown on the approval page after an assistant sends you to sign in.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `request` | path | string | Required; pattern `^[A-Za-z0-9_-]{16,64}$`; at most 80 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `assistant` | string | Always present. |\n| `return_host` | string | Always present; Where you go back to after approving. |\n| `school_name` | string | May be omitted; may be null. |\n| `role` | Role | May be omitted. |\n| `ai_enabled` | boolean | Always present. |\n| `hide_names` | boolean | Always present. |\n| `can_connect` | boolean | Always present. |\n| `reason` | string | May be omitted; may be null; Why you cannot connect, when can_connect is false. |\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": "request",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[A-Za-z0-9_-]{16,64}$",
       "maxLength": 80
      },
      "description": "From the approval page address."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "assistant": {
             "type": "string"
            },
            "return_host": {
             "type": "string",
             "description": "Where you go back to after approving."
            },
            "school_name": {
             "type": [
              "string",
              "null"
             ]
            },
            "role": {
             "oneOf": [
              {
               "$ref": "#/components/schemas/Role"
              },
              {
               "type": "null"
              }
             ]
            },
            "ai_enabled": {
             "type": "boolean"
            },
            "hide_names": {
             "type": "boolean"
            },
            "can_connect": {
             "type": "boolean"
            },
            "reason": {
             "type": [
              "string",
              "null"
             ],
             "description": "Why you cannot connect, when can_connect is false."
            }
           },
           "required": [
            "assistant",
            "return_host",
            "ai_enabled",
            "hide_names",
            "can_connect"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "assistant": "string",
          "return_host": "string",
          "school_name": "string",
          "role": "school_admin",
          "ai_enabled": true,
          "hide_names": true,
          "can_connect": true,
          "reason": "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"
     }
    }
   }
  },
  "/me/ai-connections/requests/{request}/approve": {
   "post": {
    "tags": [
     "AI assistants"
    ],
    "summary": "Approve the connection",
    "description": "Returns the address to send the browser back to, with a one-time code for the assistant.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `request` | path | string | Required; pattern `^[A-Za-z0-9_-]{16,64}$`; at most 80 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `redirect_url` | string | Always present; https URL. |\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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "request",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[A-Za-z0-9_-]{16,64}$",
       "maxLength": 80
      },
      "description": "From the approval page address."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "redirect_url": {
             "type": "string",
             "format": "uri"
            }
           },
           "required": [
            "redirect_url"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "redirect_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"
     }
    }
   }
  },
  "/me/ai-connections/requests/{request}/deny": {
   "post": {
    "tags": [
     "AI assistants"
    ],
    "summary": "Decline the connection",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "request",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[A-Za-z0-9_-]{16,64}$",
       "maxLength": 80
      },
      "description": "From the approval page address."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "redirect_url": {
             "type": "string",
             "format": "uri"
            }
           },
           "required": [
            "redirect_url"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "redirect_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"
     }
    },
    "description": "Decline the connection.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `request` | path | string | Required; pattern `^[A-Za-z0-9_-]{16,64}$`; at most 80 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `redirect_url` | string | Always present; https URL. |\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)"
   }
  },
  "/me/receipts/{id}": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "One receipt",
    "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"
     }
    },
    "description": "One receipt.\n\n**Who can call:** signed-in person (Bearer session token).\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)"
   }
  },
  "/me/receipts/{id}/pdf": {
   "get": {
    "tags": [
     "Fees"
    ],
    "summary": "Receipt as PDF",
    "description": "Short-lived signed URL to the printable receipt.\n\n**Who can call:** signed-in person (Bearer session token).\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)",
    "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"
     }
    }
   }
  },
  "/me/proofs": {
   "get": {
    "tags": [
     "Payment proofs"
    ],
    "summary": "Proofs I uploaded",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "status",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "waiting",
        "matched",
        "returned",
        "rejected"
       ]
      }
     },
     {
      "$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"
     }
    },
    "description": "Proofs I uploaded.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `status` | query | string | Optional; one of `waiting`, `matched`, `returned`, `rejected`. |\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)"
   },
   "post": {
    "tags": [
     "Payment proofs"
    ],
    "summary": "Submit a payment proof",
    "description": "Step 2 of 2: after uploading the file, submit the details. Duplicates (same file) are accepted and flagged; the office matches once. Limit 10 per family per day (TOO_MANY). Idempotency-Key is required on this operation.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `student_ids` | array of string | Required; at most 8 items. |\n| `fee_month` | string | Required; date YYYY-MM-DD; at most 40 characters; Any day; normalised to the first of the month. |\n| `amount` | string | Required; pattern `^\\d+\\.\\d{2}$`; at most 80 characters; Amount with two decimals, as a string; never negative. |\n| `method` | string | Required; one of `bank_transfer`, `wallet`, `cheque`, `cash`. |\n| `reference` | string | Optional; at most 80 characters. |\n| `upload_id` | string | Optional; at most 200 characters; From POST /me/proofs/upload-url. Optional since 2026-10-14: without it the proof is a claim with no file. |\n\n**Response 201:** `{ data, meta }` where `data` is one object.\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| `duplicate` | 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ProofWrite"
       },
       "example": {
        "student_ids": [
         "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
        ],
        "fee_month": "2026-09-01",
        "amount": "12500.00",
        "method": "bank_transfer",
        "reference": "string",
        "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Received.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/Proof"
            },
            {
             "type": "object",
             "properties": {
              "duplicate": {
               "type": "boolean"
              }
             }
            }
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "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",
          "duplicate": 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-idempotency-required": true
   }
  },
  "/me/proofs/upload-url": {
   "post": {
    "tags": [
     "Payment proofs"
    ],
    "summary": "Get an upload URL for a proof file",
    "description": "Step 1 of 2: returns a one-time signed PUT URL for the file (JPEG, PNG, WebP or PDF, 5 MB max). Upload the bytes there, then POST /me/proofs with the upload_id.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `content_type` | string | Required; one of `image/jpeg`, `image/png`, `image/webp`, `application/pdf`. |\n| `size_bytes` | integer | Required; from any to 5242880. |\n| `sha256` | string | Required; pattern `^[a-f0-9]{64}$`; at most 80 characters. |\n| `student_id` | string | Optional; UUID; at most 40 characters; The child the proof is for. Sets up the family account if the office has not yet, and picks the right one when you have children on more than one. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `upload_id` | string | May be omitted. |\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- 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": {
         "content_type": {
          "type": "string",
          "enum": [
           "image/jpeg",
           "image/png",
           "image/webp",
           "application/pdf"
          ]
         },
         "size_bytes": {
          "type": "integer",
          "maximum": 5242880
         },
         "sha256": {
          "type": "string",
          "pattern": "^[a-f0-9]{64}$",
          "maxLength": 80
         },
         "student_id": {
          "type": "string",
          "description": "The child the proof is for. Sets up the family account if the office has not yet, and picks the right one when you have children on more than one.",
          "format": "uuid",
          "maxLength": 40
         }
        },
        "required": [
         "content_type",
         "size_bytes",
         "sha256"
        ],
        "additionalProperties": false
       },
       "example": {
        "content_type": "image/jpeg",
        "size_bytes": 1,
        "sha256": "string",
        "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "upload_id": {
             "type": "string"
            },
            "url": {
             "type": "string",
             "format": "uri"
            },
            "expires_at": {
             "type": "string",
             "format": "date-time"
            }
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "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"
     }
    }
   }
  },
  "/me/devices": {
   "post": {
    "tags": [
     "Profile"
    ],
    "summary": "Register a push token",
    "description": "For the mobile apps. One row per device; re-registering the same token refreshes it.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `platform` | string | Required; one of `ios`, `android`, `web`. |\n| `token` | string | Required; at most 200 characters. |\n| `app_version` | string | Optional; at most 200 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- 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": {
         "platform": {
          "type": "string",
          "enum": [
           "ios",
           "android",
           "web"
          ]
         },
         "token": {
          "type": "string",
          "maxLength": 200
         },
         "app_version": {
          "type": "string",
          "maxLength": 200
         }
        },
        "required": [
         "platform",
         "token"
        ],
        "additionalProperties": false
       },
       "example": {
        "platform": "ios",
        "token": "string",
        "app_version": "string"
       }
      }
     }
    },
    "responses": {
     "204": {
      "description": "Registered."
     },
     "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"
     }
    }
   },
   "delete": {
    "tags": [
     "Profile"
    ],
    "summary": "Remove a push token",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "token": {
          "type": "string",
          "maxLength": 200
         }
        },
        "required": [
         "token"
        ],
        "additionalProperties": false
       },
       "example": {
        "token": "string"
       }
      }
     }
    },
    "responses": {
     "204": {
      "description": "Removed."
     },
     "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": "Remove a push token.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `token` | string | Required; at most 200 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- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)"
   }
  },
  "/me/students/{student_id}/homework": {
   "get": {
    "tags": [
     "Homework"
    ],
    "summary": "A child's homework",
    "description": "Everything set for the child's class, newest due first, with their own work. Marks and feedback show once the teacher returns them.\n\n**Who can call:** signed-in person (Bearer session token).\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 ChildHomework.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `title` | string | Always present. |\n| `instructions` | string | May be omitted; may be null. |\n| `due_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `max_marks` | integer | Always present. |\n| `allow_late` | boolean | May be omitted. |\n| `attachments` | array of HomeworkFile | Always present. |\n| `subject` | object | Always present. |\n| `teacher` | PersonRef | May be omitted. |\n| `status` | string | Always present; one of `due`, `overdue`, `handed_in`, `late`, `marked`, `missing`; 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. |\n| `submission` | HomeworkSubmission | 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/student_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/ChildHomework"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "instructions": "string",
           "due_at": "2026-09-22T09:30:00Z",
           "max_marks": 1,
           "allow_late": true,
           "attachments": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "worksheet.pdf",
             "content_type": "string",
             "size_bytes": 1,
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "subject": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string"
           },
           "teacher": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "status": "due",
           "submission": {
            "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "body": "string",
            "files": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "worksheet.pdf",
              "content_type": "string",
              "size_bytes": 1,
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "submitted_at": "2026-09-22T09:30:00Z",
            "is_late": true,
            "marks": 1,
            "feedback": "string",
            "graded_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"
     }
    }
   }
  },
  "/me/homework/upload-url": {
   "post": {
    "tags": [
     "Homework"
    ],
    "summary": "Get an upload URL for work to hand in",
    "description": "Photos, PDF or Office files, 20 MB at most. PUT the bytes, then send {upload_id} in the hand-in's files.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `file_name` | string | Required; 1 to 200 characters. |\n| `content_type` | string | Required; one of `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`. |\n| `size_bytes` | integer | Required; from 1 to 20971520. |\n\n**Response 200:** `{ data, meta }` where `data` is one UploadUrl.\n\n| Field | Type | Notes |\n|---|---|---|\n| `upload_id` | string | Always present. |\n| `url` | string | Always present; https URL; PUT the file bytes here within 15 minutes. |\n| `expires_at` | string | Always present; 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- 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/HomeworkUpload"
       },
       "example": {
        "file_name": "string",
        "content_type": "image/jpeg",
        "size_bytes": 1
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/UploadUrl"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "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"
     }
    }
   }
  },
  "/me/assignments/{id}/submission": {
   "put": {
    "tags": [
     "Homework"
    ],
    "summary": "Hand in (or replace) work",
    "description": "The student, or a parent for their child. Text and/or up to 5 files. Late work is marked late, or refused when the teacher does not take late work. Replacing is allowed until the teacher marks it.\n\n**Who can call:** signed-in person (Bearer session token).\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| `student_id` | string | Required; UUID; at most 40 characters. |\n| `body` | string | Optional; may be null; at most 8000 characters. |\n| `files` | array of HomeworkFileRef | Optional; at most 5 items. |\n\n**Response 200:** `{ data, meta }` where `data` is one HomeworkSubmission.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student_id` | string | Always present; UUID. |\n| `body` | string | May be omitted; may be null. |\n| `files` | array of HomeworkFile | Always present. |\n| `submitted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339); Null for work marked without an online hand-in. |\n| `is_late` | boolean | Always present. |\n| `marks` | integer | May be omitted; may be null. |\n| `feedback` | string | May be omitted; may be null. |\n| `graded_at` | 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "student_id": {
          "type": "string",
          "format": "uuid",
          "maxLength": 40
         },
         "body": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 8000
         },
         "files": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/HomeworkFileRef"
          },
          "maxItems": 5
         }
        },
        "required": [
         "student_id"
        ],
        "additionalProperties": false
       },
       "example": {
        "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "body": "string",
        "files": [
         {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/HomeworkSubmission"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "body": "string",
          "files": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "worksheet.pdf",
            "content_type": "string",
            "size_bytes": 1,
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "submitted_at": "2026-09-22T09:30:00Z",
          "is_late": true,
          "marks": 1,
          "feedback": "string",
          "graded_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"
     }
    }
   }
  },
  "/me/admissions/open": {
   "get": {
    "tags": [
     "Admissions"
    ],
    "summary": "What a school takes applications for",
    "description": "The school's name and the classes open for online applications (with a seat limit set), per academic year. Any signed-in person.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `school` | query | string | Optional; pattern `^[A-Za-z0-9-]{2,40}$`; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one AdmissionOpen.\n\n| Field | Type | Notes |\n|---|---|---|\n| `school_name` | string | Always present. |\n| `school_code` | string | Always present. |\n| `options` | array of object | 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"
     },
     {
      "name": "school",
      "in": "query",
      "schema": {
       "type": "string",
       "maxLength": 40,
       "pattern": "^[A-Za-z0-9-]{2,40}$"
      },
      "description": "The school's address name (primary) or code."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/AdmissionOpen"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "school_name": "string",
          "school_code": "string",
          "options": [
           {
            "academic_year_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "academic_year": "string",
            "class_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "class_name": "string",
            "min_age": 1,
            "max_age": 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-signed-in-only": true
   }
  },
  "/me/admissions": {
   "get": {
    "tags": [
     "Admissions"
    ],
    "summary": "My applications",
    "description": "Applications made with this sign-in, at any school, newest first, with where each stands.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of MyApplication.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `application_no` | string | Always present. |\n| `school_name` | string | Always present. |\n| `child_name` | string | Always present. |\n| `class_name` | string | Always present. |\n| `academic_year` | string | Always present. |\n| `status` | AdmissionStatus | Always present; one of `applied`, `under_review`, `test_booked`, `offered`, `accepted`, `enrolled`, `waitlisted`, `rejected`, `withdrawn`; applied, under review, test or interview booked, offered, accepted, enrolled; side exits waitlisted, rejected, withdrawn. |\n| `test_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `decision_note` | string | May be omitted; may be null. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_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- 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/MyApplication"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "application_no": "string",
           "school_name": "string",
           "child_name": "string",
           "class_name": "string",
           "academic_year": "string",
           "status": "applied",
           "test_at": "2026-09-22T09:30:00Z",
           "decision_note": "string",
           "created_at": "2026-09-22T09:30:00Z",
           "updated_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-signed-in-only": true
   },
   "post": {
    "tags": [
     "Admissions"
    ],
    "summary": "Apply for a place",
    "description": "Any signed-in person (the sign-in proves their email, which is where the school writes back). At most 5 a day. The office sees it under Admissions; the family is told at every step.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `school` | string | Required; at most 40 characters; The school's address name (primary) or code. |\n| `academic_year_id` | string | Required; UUID; at most 40 characters. |\n| `class_id` | string | Required; UUID; at most 40 characters. |\n| `child_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 `male`, `female`, `other`, `None`. |\n| `previous_school` | string | Optional; may be null; at most 120 characters. |\n| `guardians` | array of AdmissionGuardian | Required; at most 3 items. |\n| `contact_phone` | string | Optional; may be null; pattern `^\\+[1-9][0-9]{6,14}$`; at most 80 characters. |\n| `notes` | string | Optional; may be null; at most 2000 characters. |\n\n**Response 201:** `{ data, meta }` where `data` is one MyApplication.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `application_no` | string | Always present. |\n| `school_name` | string | Always present. |\n| `child_name` | string | Always present. |\n| `class_name` | string | Always present. |\n| `academic_year` | string | Always present. |\n| `status` | AdmissionStatus | Always present; one of `applied`, `under_review`, `test_booked`, `offered`, `accepted`, `enrolled`, `waitlisted`, `rejected`, `withdrawn`; applied, under review, test or interview booked, offered, accepted, enrolled; side exits waitlisted, rejected, withdrawn. |\n| `test_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `decision_note` | string | May be omitted; may be null. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_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- 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/ApplyWrite"
       },
       "example": {
        "school": "string",
        "academic_year_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "class_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "child_name": "string",
        "date_of_birth": "2026-09-01",
        "gender": "male",
        "previous_school": "string",
        "guardians": [
         {
          "name": "Ahmed Khan",
          "relation": "father",
          "phone": "+923001234567",
          "email": "parent@example.pk"
         }
        ],
        "contact_phone": "+923001234567",
        "notes": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/MyApplication"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "application_no": "string",
          "school_name": "string",
          "child_name": "string",
          "class_name": "string",
          "academic_year": "string",
          "status": "applied",
          "test_at": "2026-09-22T09:30:00Z",
          "decision_note": "string",
          "created_at": "2026-09-22T09:30:00Z",
          "updated_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-signed-in-only": true
   }
  },
  "/me/getting-started": {
   "get": {
    "tags": [
     "Getting started"
    ],
    "summary": "My getting started checklist",
    "description": "The checklist for the caller's staff roles at their school (merged when they hold two), with the state of every item. The app ticks items it can see done in the school's data, and notices when one stops being true. Parents and students get an empty list. During View as it shows the other person's list and records nothing.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one GettingStarted.\n\n| Field | Type | Notes |\n|---|---|---|\n| `roles` | array of string | Always present. |\n| `role` | string | Always present; may be null; The role whose road is shown (the most senior one). Null for people with no checklist, such as parents. |\n| `preparing_for` | array of string | May be omitted; 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. |\n| `info` | object | Always present. |\n| `items` | array of GettingStartedItem | Always present. |\n| `progress` | GettingStartedProgress | Always present. |\n| `seen_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `hidden_at` | string | Always present; 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": {
           "$ref": "#/components/schemas/GettingStarted"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "roles": [
           "string"
          ],
          "role": "string",
          "preparing_for": [
           "string"
          ],
          "info": {
           "title": "string",
           "lead": "string",
           "goal": "string",
           "art": "string",
           "stops": [
            "string"
           ]
          },
          "items": [
           {
            "key": "timetable",
            "title": "string",
            "minutes": 1,
            "scope": "you",
            "role": "string",
            "priority": "H",
            "stop": 1,
            "check": "app",
            "skippable": true,
            "state": "todo",
            "at": "2026-09-22T09:30:00Z",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "string",
             "is_me": true
            },
            "reason": "string",
            "help": {
             "issue_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "ref": "PRB-2026-0042",
             "at": "2026-09-22T09:30:00Z"
            },
            "do_route": "string",
            "do_anchor": "string",
            "guide": "string"
           }
          ],
          "progress": {
           "done": 1,
           "total": 1,
           "high_done": 1,
           "high_total": 1
          },
          "seen_at": "2026-09-22T09:30:00Z",
          "hidden_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"
     }
    }
   }
  },
  "/me/getting-started/items/{key}": {
   "post": {
    "tags": [
     "Getting started"
    ],
    "summary": "Tick, untick, skip or bring back an item",
    "description": "tick and untick only for items the person ticks (the app ticks the rest). skip needs a skippable item (ITEM_NOT_SKIPPABLE) and a reason of 3 to 200 characters (REASON_REQUIRED); unskip by the person who skipped it or the school office. Never during View as. Returns the whole checklist.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `key` | path | string | Required; pattern `^[a-z][a-z0-9_]{1,40}$`; at most 80 characters. |\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `action` | string | Required; one of `tick`, `untick`, `skip`, `unskip`. |\n| `reason` | string | Optional; at most 200 characters; Why the school does not need it (skip). |\n\n**Response 200:** `{ data, meta }` where `data` is one GettingStarted.\n\n| Field | Type | Notes |\n|---|---|---|\n| `roles` | array of string | Always present. |\n| `role` | string | Always present; may be null; The role whose road is shown (the most senior one). Null for people with no checklist, such as parents. |\n| `preparing_for` | array of string | May be omitted; 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. |\n| `info` | object | Always present. |\n| `items` | array of GettingStartedItem | Always present. |\n| `progress` | GettingStartedProgress | Always present. |\n| `seen_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `hidden_at` | string | Always present; 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "key",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[a-z][a-z0-9_]{1,40}$",
       "maxLength": 80
      },
      "description": "The item key."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "action": {
          "type": "string",
          "enum": [
           "tick",
           "untick",
           "skip",
           "unskip"
          ]
         },
         "reason": {
          "type": "string",
          "description": "Why the school does not need it (skip).",
          "maxLength": 200
         }
        },
        "required": [
         "action"
        ],
        "additionalProperties": false
       },
       "example": {
        "action": "tick",
        "reason": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/GettingStarted"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "roles": [
           "string"
          ],
          "role": "string",
          "preparing_for": [
           "string"
          ],
          "info": {
           "title": "string",
           "lead": "string",
           "goal": "string",
           "art": "string",
           "stops": [
            "string"
           ]
          },
          "items": [
           {
            "key": "timetable",
            "title": "string",
            "minutes": 1,
            "scope": "you",
            "role": "string",
            "priority": "H",
            "stop": 1,
            "check": "app",
            "skippable": true,
            "state": "todo",
            "at": "2026-09-22T09:30:00Z",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "string",
             "is_me": true
            },
            "reason": "string",
            "help": {
             "issue_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "ref": "PRB-2026-0042",
             "at": "2026-09-22T09:30:00Z"
            },
            "do_route": "string",
            "do_anchor": "string",
            "guide": "string"
           }
          ],
          "progress": {
           "done": 1,
           "total": 1,
           "high_done": 1,
           "high_total": 1
          },
          "seen_at": "2026-09-22T09:30:00Z",
          "hidden_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"
     }
    }
   }
  },
  "/me/getting-started/view": {
   "post": {
    "tags": [
     "Getting started"
    ],
    "summary": "The page was opened, hidden or shown again",
    "description": "Records the first opening (the page opens by itself on the first sign-in). hidden true hides it from the Help menu, false shows it again, left out changes nothing.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `hidden` | boolean | Optional; may be null. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `seen_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `hidden_at` | string | Always present; 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- 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": {
         "hidden": {
          "type": [
           "boolean",
           "null"
          ]
         }
        },
        "additionalProperties": false
       },
       "example": {
        "hidden": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "seen_at": {
             "type": [
              "string",
              "null"
             ],
             "format": "date-time"
            },
            "hidden_at": {
             "type": [
              "string",
              "null"
             ],
             "format": "date-time"
            }
           },
           "required": [
            "seen_at",
            "hidden_at"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "seen_at": "2026-09-22T09:30:00Z",
          "hidden_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"
     }
    }
   }
  },
  "/me/help/events": {
   "post": {
    "tags": [
     "Help"
    ],
    "summary": "Record a help event",
    "description": "One event about the caller's own use of help. Nothing is recorded while viewing as someone else (recorded is false). At most 300 a day per person (RATE_LIMITED).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `kind` | string | Required; one of `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`; 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. |\n| `guide` | string | Optional; pattern `^[a-z0-9-]{2,60}$`; at most 80 characters; The guide's slug. Needed for guide_opened, helpful, not_helpful and tour_abandoned. |\n| `screen` | string | Optional; pattern `^[a-z0-9-]{2,60}$`; at most 80 characters; The screen key. Needed for panel_opened, save, easy and not_easy. |\n| `anchor` | string | Optional; pattern `^[a-z0-9]+(-[a-z0-9]+)*(\\.[a-z0-9]+(-[a-z0-9]+)*)+$`; at most 80 characters; The anchor a guide or tour could not find on the screen. Needed for broken. |\n| `query` | string | Optional; 1 to 80 characters; The words of a search that found nothing. Needed for search_empty. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `recorded` | 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- 409 rule conflicts (for example SECTION_FULL, ON_HOLD, RECEIPT_IMMUTABLE)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/HelpEvent"
       },
       "example": {
        "kind": "panel_opened",
        "guide": "post-an-announcement",
        "screen": "admin-timetable",
        "anchor": "admin-timetable.publish",
        "query": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "recorded": {
             "type": "boolean"
            }
           },
           "required": [
            "recorded"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "recorded": 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"
     }
    }
   }
  },
  "/me/help/ask": {
   "post": {
    "tags": [
     "Help"
    ],
    "summary": "Ask the help buddy",
    "description": "Mithoo or the owl answers from this build's help guides and the person's own data, read through the school's read-only tools as the person. context carries ids only. Rules before anything comes back: every id in path exists in the help graph, every quoted button is a label on a cited step, every table row and chart point comes from a lookup that ran; otherwise the plain answer (fallback true). 20 questions a day per person (RATE_LIMITED with the reset time in the hint; a school admin can raise it to 50), 10 a minute. Never during View as (HELP_VIEW_AS). NOT_ALLOWED when the buddy is off for this audience or everywhere. The answer comes back whole in this phase, not streamed.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `question` | string | Required; 1 to 2000 characters; What the person asked, in their own words. |\n| `context` | object | Optional; Where the person is, as ids only. Anything else is refused (UNKNOWN_FIELD). |\n\n**Response 200:** `{ data, meta }` where `data` is one HelpAnswer.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID; The question's id, for the vote and the hand-off. |\n| `answer` | string | Always present; The one-line answer. |\n| `blocks` | array of HelpBlock | Always present. |\n| `path` | array of string | Always present. |\n| `doit` | array of string | Always present. |\n| `lookups` | array of string | Always present. |\n| `done_task_ids` | array of string | Always present. |\n| `unblocked_check` | boolean | Always present; Always true: the client asks Did this unblock you?. |\n| `confidence` | number | Always present; from 0 to 1. |\n| `fallback` | boolean | Always present; 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. |\n| `remaining_today` | integer | Always present; may be null; Questions left today, null for super admins. |\n| `buddy` | string | Always present; one of `mithoo`, `hoot`; Which character answers: Mithoo for students up to grade 7, Smarty the owl (id hoot) for everyone else. |\n| `tone_plain` | boolean | Always present; Less jokes is on for this person. |\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/HelpAsk"
       },
       "example": {
        "question": "I can't find the Mark button",
        "context": {
         "screen": "string",
         "task": "string",
         "last_code": "string",
         "code_count_today": 1,
         "progress": [
          "training.teacher.mark_homework"
         ]
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/HelpAnswer"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "answer": "string",
          "blocks": [
           {
            "type": "text",
            "text": "string"
           }
          ],
          "path": [
           "string"
          ],
          "doit": [
           "string"
          ],
          "lookups": [
           "string"
          ],
          "done_task_ids": [
           "string"
          ],
          "unblocked_check": true,
          "confidence": 1.5,
          "fallback": true,
          "remaining_today": 1,
          "buddy": "mithoo",
          "tone_plain": 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"
     }
    }
   }
  },
  "/me/help/ask/{id}/vote": {
   "post": {
    "tags": [
     "Help"
    ],
    "summary": "Did this unblock you?",
    "description": "yes closes the question; not_yet is counted for the help team and, when the hard-case council is on for Not yet and inside its budget, sits the council once with the same question: a passing answer comes back as next_answer and is shown in place before the one-more-try and the hand-off. Only the person who asked.\n\n**Who can call:** signed-in person (Bearer session token).\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| `vote` | string | Required; one of `yes`, `not_yet`. |\n\n**Response 200:** `{ data, meta }` where `data` is one HelpVoteResult.\n\n| Field | Type | Notes |\n|---|---|---|\n| `next_answer` | HelpAnswer | 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/HelpVote"
       },
       "example": {
        "vote": "yes"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/HelpVoteResult"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "next_answer": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "answer": "string",
           "blocks": [
            {
             "type": "text",
             "text": "string"
            }
           ],
           "path": [
            "string"
           ],
           "doit": [
            "string"
           ],
           "lookups": [
            "string"
           ],
           "done_task_ids": [
            "string"
           ],
           "unblocked_check": true,
           "confidence": 1.5,
           "fallback": true,
           "remaining_today": 1,
           "buddy": "mithoo",
           "tone_plain": 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"
     }
    }
   }
  },
  "/me/help/ask/{id}/report": {
   "post": {
    "tags": [
     "Help"
    ],
    "summary": "Send it on (the hand-off)",
    "description": "Files a problem report built from the question's recorded context and the graph: the screen, the refusal code and how often it came back today, the suggested fix, the severity and the person's note. Never names or field values. It goes to the school office when the fix is a school permission, otherwise to saMas triage, and appears under My reports like any report. Once per question (INVALID_STATE).\n\n**Who can call:** signed-in person (Bearer session token).\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| `note` | string | Optional; may be null; at most 1000 characters; The person's own words, left out for none. |\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\n**Response 201:** `{ data, meta }` where `data` is one HelpReportResult.\n\n| Field | Type | Notes |\n|---|---|---|\n| `issue_id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `owner` | string | Always present; one of `samas`, `school`; Who got it: the school office (a school permission) or saMas triage. |\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/HelpReport"
       },
       "example": {
        "note": "string",
        "severity": "blocked"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/HelpReportResult"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "issue_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "ref": "PRB-2026-0007",
          "owner": "samas"
         },
         "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"
     }
    }
   }
  },
  "/me/help/buddy": {
   "get": {
    "tags": [
     "Help"
    ],
    "summary": "The help buddy for me",
    "description": "Whether the button shows for this person (the platform switch, the school's switch for their audience, and View as), which character they get, the questions left today, whether Less jokes is on, and how many replies from the help team are waiting. The client reads it on load and every few minutes.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one HelpBuddyStatus.\n\n| Field | Type | Notes |\n|---|---|---|\n| `enabled` | boolean | Always present; 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. |\n| `buddy` | string | Always present; one of `mithoo`, `hoot`; Which character this person gets: Mithoo for students up to grade 7, Smarty the owl (id hoot) for everyone else. |\n| `remaining_today` | integer | Always present; may be null; Questions left today, null for super admins. |\n| `tone_plain` | boolean | Always present; Less jokes is on for this person. |\n| `replies_unread` | integer | Always present; Replies from the saMas help team waiting to be read: the badge on the button. |\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/HelpBuddyStatus"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "enabled": true,
          "buddy": "mithoo",
          "remaining_today": 1,
          "tone_plain": true,
          "replies_unread": 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"
     }
    }
   }
  },
  "/me/help/whats-new": {
   "get": {
    "tags": [
     "Help"
    ],
    "summary": "What's new for me",
    "description": "What's new on the Help page: short lines about what changed, newest first, only the ones for the caller's role (super admins also get the ones for super admins) and only those whose add-on (madrasa, learn) the school has on. A parent never sees office-only lines. At most 10 (limit 1 to 10, default 10). The same for every school; the lines ship with each release.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `limit` | query | integer | Optional; from 1 to 10. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of WhatsNewItem.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; at most 64 characters; Stable id of the item. |\n| `date` | string | Always present; date YYYY-MM-DD; The day it shipped. |\n| `title` | string | Always present; at most 60 characters. |\n| `text` | string | Always present; at most 200 characters; One plain sentence. |\n| `add_on` | string | Always present; may be null; one of `madrasa`, `learn`, `charity`, `live_sabaq`, `toefl`, `flashcards`, `None`; The add-on the item is about; the item is only returned when the school has it on. Null when it needs none. |\n| `try_it` | object | 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"
     },
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 10,
       "default": 10
      },
      "description": "How many, newest first (1 to 10)."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/WhatsNewItem"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "date": "2026-09-01",
           "title": "string",
           "text": "string",
           "add_on": "madrasa",
           "try_it": {
            "route": "string",
            "anchor": "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"
     }
    }
   }
  },
  "/me/help/tone": {
   "put": {
    "tags": [
     "Help"
    ],
    "summary": "Less jokes",
    "description": "Plain answers from the buddy for 30 days (plain true), or back to its usual voice (plain false). Never during View as (HELP_VIEW_AS).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `plain` | boolean | Required; True turns Less jokes on for 30 days; false turns it off. |\n\n**Response 200:** `{ data, meta }` where `data` is one HelpToneResult.\n\n| Field | Type | Notes |\n|---|---|---|\n| `plain` | boolean | Always present. |\n| `until` | string | Always present; may be null; date YYYY-MM-DD; The last day plain answers apply, null when off. |\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/HelpTone"
       },
       "example": {
        "plain": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/HelpToneResult"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "plain": true,
          "until": "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"
     }
    }
   }
  },
  "/me/help/replies": {
   "get": {
    "tags": [
     "Help"
    ],
    "summary": "Replies waiting for me",
    "description": "Unread replies from the saMas help team to questions the buddy had no guide for. The client shows a badge while any are waiting.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of HelpReply.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `question_id` | string | Always present; UUID. |\n| `question` | string | Always present; may be null; The question as asked, while it is still kept (30 days). |\n| `body` | string | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `read_at` | string | Always present; 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/HelpReply"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "question_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "question": "string",
           "body": "string",
           "created_at": "2026-09-22T09:30:00Z",
           "read_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"
     }
    }
   }
  },
  "/me/help/replies/{id}/read": {
   "post": {
    "tags": [
     "Help"
    ],
    "summary": "Mark a reply read",
    "description": "Clears it from the badge.\n\n**Who can call:** signed-in person (Bearer session token).\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- 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"
     }
    ],
    "responses": {
     "204": {
      "description": "Cleared."
     },
     "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"
     }
    }
   }
  },
  "/me/emails": {
   "get": {
    "tags": [
     "Profile"
    ],
    "summary": "Emails the school sent me",
    "description": "The last 30 days, newest first (at most 50), at any school: what each was about, whether it went, and in plain words why not. Shows the address on your profile. During View as: the other person's emails, read only.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one MyEmails.\n\n| Field | Type | Notes |\n|---|---|---|\n| `email` | string | Always present; may be null; The address on your profile, where every email goes. Null when there is none. |\n| `items` | array of MyEmail | 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/MyEmails"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "email": "string",
          "items": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "kind": "cover_assigned",
            "what": "string",
            "school_name": "string",
            "created_at": "2026-09-22T09:30:00Z",
            "sent_at": "2026-09-22T09:30:00Z",
            "status": "sent",
            "reason": "string",
            "can_resend": 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-signed-in-only": true
   }
  },
  "/me/emails/{id}/resend": {
   "post": {
    "tags": [
     "Profile"
    ],
    "summary": "Send one of my emails again",
    "description": "Only an email that was not sent or failed, to an address that can get mail. At most 3 a day (RATE_LIMITED); INVALID_STATE for one that already went or cannot be sent. The next minute's run sends it.\n\n**Who can call:** signed-in person (Bearer session token).\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| `status` | string | Always present; one of `on_the_way`. |\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)",
    "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": {
           "type": "object",
           "properties": {
            "status": {
             "type": "string",
             "enum": [
              "on_the_way"
             ]
            }
           },
           "required": [
            "status"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "status": "on_the_way"
         },
         "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-signed-in-only": true
   }
  },
  "/me/issues": {
   "get": {
    "tags": [
     "Issues"
    ],
    "summary": "My reports",
    "description": "Everything the caller reported, newest first, at any school. During View as: the other person's reports, read only.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of Issue.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `kind` | IssueKind | Always present; one of `app`, `school`, `idea`; 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). |\n| `severity` | IssueSeverity | Always present; 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| `status` | IssueStatus | Always present; one of `new`, `looking`, `fixed`, `closed`; new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done. |\n| `duplicate_of` | string | May be omitted; may be null; UUID; The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one. |\n| `owner` | string | Always present; one of `samas`, `school`; Who is looking after it now: the saMas team or the school office. |\n| `priority` | string | Always present; may be null; one of `P1`, `P2`, `P3`, `None`; Set by the saMas team. |\n| `assignee` | IssuePerson | Always present. |\n| `reporter` | object | Always present. |\n| `school` | object | Always present. |\n| `body` | string | Always present; What happened, in the reporter's words. |\n| `context` | IssueContext | Always present; What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers. |\n| `journey_item_key` | string | Always present; may be null; The getting started item it was sent from, if any. |\n| `screenshot` | IssueScreenshot | Always present. |\n| `screenshot_removed` | boolean | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `first_reply_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `fixed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `closed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `reopened_count` | integer | Always present. |\n| `messages_count` | integer | Always present. |\n| `last_message_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `my_side` | string | Always present; one of `reporter`, `school`, `samas`; Which side the caller stands on for this report. |\n| `private_to_samas` | boolean | Always present; 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. |\n| `closed_reason` | string | Always present; may be null; one of `stale`, `office`, `confirmed`, `released`, `None`; 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. |\n| `roadmap_item` | object | May be omitted. |\n| `priority_word` | string | May be omitted; may be null; one of `being looked at`, `planned`, `later`, `None`; 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). |\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/Issue"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "ref": "PRB-2026-0042",
           "kind": "app",
           "severity": "blocked",
           "status": "new",
           "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "owner": "samas",
           "priority": "P1",
           "assignee": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "reporter": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "role": "string",
            "is_me": true
           },
           "school": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string"
           },
           "body": "string",
           "context": {
            "screen": "string",
            "role": "string",
            "school_name": "string",
            "error_codes": [
             "string"
            ],
            "console_errors": [
             "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": []
             }
            ]
           },
           "journey_item_key": "string",
           "screenshot": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "string",
            "size_bytes": 1,
            "url": "https://erp.example.pk/samas/hook",
            "removed_at": "2026-09-22T09:30:00Z"
           },
           "screenshot_removed": true,
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z",
           "first_reply_at": "2026-09-22T09:30:00Z",
           "fixed_at": "2026-09-22T09:30:00Z",
           "closed_at": "2026-09-22T09:30:00Z",
           "reopened_count": 1,
           "messages_count": 1,
           "last_message_at": "2026-09-22T09:30:00Z",
           "my_side": "reporter",
           "private_to_samas": true,
           "closed_reason": "stale",
           "roadmap_item": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "title": "string",
            "stage": "proposed",
            "target": "string"
           },
           "priority_word": "being looked at"
          }
         ],
         "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": [
     "Issues"
    ],
    "summary": "Report a problem",
    "description": "An app problem or an idea goes to the saMas team; a school matter goes to the school office, and the side that looks after it is emailed. The app attaches the screen, role, school, the last error codes, the version and the browser; with what the person leaves ticked, the error they hit (incident), the last 30 calls to the API (route patterns, status and time, never what was sent), the last 30 steps (screens and help anchors, never what was typed) and the device (screen size, zoom, connection, time zone), and nothing else. The text passes the school's blocked words check (BLOCKED_WORDS). Ten reports a day per person (ISSUE_LIMIT). Never during View as. Returns the report with its reference (PRB-YYYY-NNNN).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `kind` | IssueKind | Required; one of `app`, `school`, `idea`; 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). |\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| `screen` | string | Optional; may be null; at most 200 characters; The route the person was on. |\n| `error_codes` | array of string | Optional; at most 3 items; The last API error codes the app saw, codes only. |\n| `console_errors` | array of string | Optional; at most 10 items; 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. |\n| `app_version` | string | Optional; may be null; at most 40 characters. |\n| `browser` | string | Optional; may be null; at most 120 characters. |\n| `item_key` | string | Optional; may be null; pattern `^[a-z][a-z0-9_]{1,40}$`; at most 80 characters; The getting started item this was sent from (Having trouble?). That item shows needs help until the report is sorted. |\n| `screenshot_upload_id` | string | Optional; may be null; at most 64 characters; From POST /me/issues/upload-url; left out or null for no screenshot. |\n| `incident` | IssueIncident | Optional. |\n| `requests` | array of IssueRequestSeen | Optional; at most 30 items; The app's last calls to the API, newest last. |\n| `steps` | array of string | Optional; at most 30 items; The last steps, newest last: screen:<route pattern> or tap:<help anchor>. |\n| `device` | IssueDevice | Optional. |\n| `console` | array of IssueConsoleLine | Optional; at most 100 items; 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. |\n| `private_to_samas` | boolean | Optional; 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. |\n\n**Response 201:** `{ data, meta }` where `data` is one IssueDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `kind` | IssueKind | Always present; one of `app`, `school`, `idea`; 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). |\n| `severity` | IssueSeverity | Always present; 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| `status` | IssueStatus | Always present; one of `new`, `looking`, `fixed`, `closed`; new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done. |\n| `duplicate_of` | string | May be omitted; may be null; UUID; The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one. |\n| `owner` | string | Always present; one of `samas`, `school`; Who is looking after it now: the saMas team or the school office. |\n| `priority` | string | Always present; may be null; one of `P1`, `P2`, `P3`, `None`; Set by the saMas team. |\n| `assignee` | IssuePerson | Always present. |\n| `reporter` | object | Always present. |\n| `school` | object | Always present. |\n| `body` | string | Always present; What happened, in the reporter's words. |\n| `context` | IssueContext | Always present; What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers. |\n| `journey_item_key` | string | Always present; may be null; The getting started item it was sent from, if any. |\n| `screenshot` | IssueScreenshot | Always present. |\n| `screenshot_removed` | boolean | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `first_reply_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `fixed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `closed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `reopened_count` | integer | Always present. |\n| `messages_count` | integer | Always present. |\n| `last_message_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `my_side` | string | Always present; one of `reporter`, `school`, `samas`; Which side the caller stands on for this report. |\n| `private_to_samas` | boolean | Always present; 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. |\n| `closed_reason` | string | Always present; may be null; one of `stale`, `office`, `confirmed`, `released`, `None`; 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. |\n| `roadmap_item` | object | May be omitted. |\n| `priority_word` | string | May be omitted; may be null; one of `being looked at`, `planned`, `later`, `None`; 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). |\n| `messages` | array of IssueMessage | Always present. |\n| `events` | array of IssueEvent | 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/IssueWrite"
       },
       "example": {
        "kind": "app",
        "body": "string",
        "severity": "blocked",
        "screen": "string",
        "error_codes": [
         "string"
        ],
        "console_errors": [
         "string"
        ],
        "app_version": "string",
        "browser": "string",
        "item_key": "string",
        "screenshot_upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "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"
          ]
         }
        ],
        "private_to_samas": true
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/IssueDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "ref": "PRB-2026-0042",
          "kind": "app",
          "severity": "blocked",
          "status": "new",
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "owner": "samas",
          "priority": "P1",
          "assignee": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "reporter": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "role": "string",
           "is_me": true
          },
          "school": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "body": "string",
          "context": {
           "screen": "string",
           "role": "string",
           "school_name": "string",
           "error_codes": [
            "string"
           ],
           "console_errors": [
            "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"
             ]
            }
           ]
          },
          "journey_item_key": "string",
          "screenshot": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "content_type": "string",
           "size_bytes": 1,
           "url": "https://erp.example.pk/samas/hook",
           "removed_at": "2026-09-22T09:30:00Z"
          },
          "screenshot_removed": true,
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "first_reply_at": "2026-09-22T09:30:00Z",
          "fixed_at": "2026-09-22T09:30:00Z",
          "closed_at": "2026-09-22T09:30:00Z",
          "reopened_count": 1,
          "messages_count": 1,
          "last_message_at": "2026-09-22T09:30:00Z",
          "my_side": "reporter",
          "private_to_samas": true,
          "closed_reason": "stale",
          "roadmap_item": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "stage": "proposed",
           "target": "string"
          },
          "priority_word": "being looked at",
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "side": "reporter",
            "author": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "body": "string",
            "pictures": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "string",
              "content_type": "string",
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "created_at": "2026-09-22T09:30:00Z"
           }
          ],
          "events": [
           {
            "kind": "created",
            "side": "string",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "note": "string",
            "detail": {},
            "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"
     }
    }
   }
  },
  "/me/client-errors": {
   "post": {
    "tags": [
     "Issues"
    ],
    "summary": "Tell saMas about browser errors",
    "description": "The web app sends each distinct error once per session, at most 20 a session, so the saMas team sees errors nobody reported. Up to 10 a call and 50 a day per person; more are dropped without an error. accepted is false while the saMas team has this turned off, and the app then stops sending for the session.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `errors` | array of ClientErrorSeen | Required; at most 10 items. |\n\n**Response 200:** `{ data, meta }` where `data` is one object.\n\n| Field | Type | Notes |\n|---|---|---|\n| `accepted` | boolean | Always present. |\n| `kept` | 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- 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": {
         "errors": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/ClientErrorSeen"
          },
          "minItems": 1,
          "maxItems": 10
         }
        },
        "required": [
         "errors"
        ],
        "additionalProperties": false
       },
       "example": {
        "errors": [
         {
          "kind": "error",
          "text": "string",
          "top_frame": "string",
          "route": "string",
          "version": "string"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "accepted": {
             "type": "boolean"
            },
            "kept": {
             "type": "integer"
            }
           },
           "required": [
            "accepted",
            "kept"
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "accepted": true,
          "kept": 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"
     }
    }
   }
  },
  "/me/issues/upload-url": {
   "post": {
    "tags": [
     "Issues"
    ],
    "summary": "Get an upload URL for a screenshot",
    "description": "PNG, JPEG or WebP, 5 MB at most. PUT the bytes, then send screenshot_upload_id with the report, or picture_upload_id with a message.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `content_type` | string | Required; one of `image/png`, `image/jpeg`, `image/webp`. |\n| `size_bytes` | integer | Required; from 1 to 5242880. |\n\n**Response 200:** `{ data, meta }` where `data` is one UploadUrl.\n\n| Field | Type | Notes |\n|---|---|---|\n| `upload_id` | string | Always present. |\n| `url` | string | Always present; https URL; PUT the file bytes here within 15 minutes. |\n| `expires_at` | string | Always present; 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- 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/IssueUpload"
       },
       "example": {
        "content_type": "image/png",
        "size_bytes": 1
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/UploadUrl"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "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"
     }
    }
   }
  },
  "/me/issues/{id}": {
   "get": {
    "tags": [
     "Issues"
    ],
    "summary": "One of my reports",
    "description": "With the conversation and the timeline. The office and the saMas team read the reports they look after here too.\n\n**Who can call:** signed-in person (Bearer session token).\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 IssueDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `kind` | IssueKind | Always present; one of `app`, `school`, `idea`; 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). |\n| `severity` | IssueSeverity | Always present; 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| `status` | IssueStatus | Always present; one of `new`, `looking`, `fixed`, `closed`; new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done. |\n| `duplicate_of` | string | May be omitted; may be null; UUID; The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one. |\n| `owner` | string | Always present; one of `samas`, `school`; Who is looking after it now: the saMas team or the school office. |\n| `priority` | string | Always present; may be null; one of `P1`, `P2`, `P3`, `None`; Set by the saMas team. |\n| `assignee` | IssuePerson | Always present. |\n| `reporter` | object | Always present. |\n| `school` | object | Always present. |\n| `body` | string | Always present; What happened, in the reporter's words. |\n| `context` | IssueContext | Always present; What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers. |\n| `journey_item_key` | string | Always present; may be null; The getting started item it was sent from, if any. |\n| `screenshot` | IssueScreenshot | Always present. |\n| `screenshot_removed` | boolean | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `first_reply_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `fixed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `closed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `reopened_count` | integer | Always present. |\n| `messages_count` | integer | Always present. |\n| `last_message_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `my_side` | string | Always present; one of `reporter`, `school`, `samas`; Which side the caller stands on for this report. |\n| `private_to_samas` | boolean | Always present; 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. |\n| `closed_reason` | string | Always present; may be null; one of `stale`, `office`, `confirmed`, `released`, `None`; 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. |\n| `roadmap_item` | object | May be omitted. |\n| `priority_word` | string | May be omitted; may be null; one of `being looked at`, `planned`, `later`, `None`; 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). |\n| `messages` | array of IssueMessage | Always present. |\n| `events` | array of IssueEvent | 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- 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": {
           "$ref": "#/components/schemas/IssueDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "ref": "PRB-2026-0042",
          "kind": "app",
          "severity": "blocked",
          "status": "new",
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "owner": "samas",
          "priority": "P1",
          "assignee": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "reporter": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "role": "string",
           "is_me": true
          },
          "school": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "body": "string",
          "context": {
           "screen": "string",
           "role": "string",
           "school_name": "string",
           "error_codes": [
            "string"
           ],
           "console_errors": [
            "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"
             ]
            }
           ]
          },
          "journey_item_key": "string",
          "screenshot": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "content_type": "string",
           "size_bytes": 1,
           "url": "https://erp.example.pk/samas/hook",
           "removed_at": "2026-09-22T09:30:00Z"
          },
          "screenshot_removed": true,
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "first_reply_at": "2026-09-22T09:30:00Z",
          "fixed_at": "2026-09-22T09:30:00Z",
          "closed_at": "2026-09-22T09:30:00Z",
          "reopened_count": 1,
          "messages_count": 1,
          "last_message_at": "2026-09-22T09:30:00Z",
          "my_side": "reporter",
          "private_to_samas": true,
          "closed_reason": "stale",
          "roadmap_item": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "stage": "proposed",
           "target": "string"
          },
          "priority_word": "being looked at",
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "side": "reporter",
            "author": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "body": "string",
            "pictures": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "string",
              "content_type": "string",
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "created_at": "2026-09-22T09:30:00Z"
           }
          ],
          "events": [
           {
            "kind": "created",
            "side": "string",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "note": "string",
            "detail": {},
            "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"
     }
    }
   }
  },
  "/me/issues/{id}/messages": {
   "post": {
    "tags": [
     "Issues"
    ],
    "summary": "Add to the conversation",
    "description": "Up to 2,000 characters; blocked words are refused. A message may carry one picture (PNG, JPEG or WebP, 5 MB), kept on the message. A reply from the office or the saMas team emails the reporter, with the picture attached.\n\n**Who can call:** signed-in person (Bearer session token).\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| `body` | string | Required; 1 to 2000 characters. |\n| `picture_upload_id` | string | Optional; may be null; at most 64 characters; 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. |\n\n**Response 200:** `{ data, meta }` where `data` is one IssueDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `kind` | IssueKind | Always present; one of `app`, `school`, `idea`; 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). |\n| `severity` | IssueSeverity | Always present; 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| `status` | IssueStatus | Always present; one of `new`, `looking`, `fixed`, `closed`; new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done. |\n| `duplicate_of` | string | May be omitted; may be null; UUID; The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one. |\n| `owner` | string | Always present; one of `samas`, `school`; Who is looking after it now: the saMas team or the school office. |\n| `priority` | string | Always present; may be null; one of `P1`, `P2`, `P3`, `None`; Set by the saMas team. |\n| `assignee` | IssuePerson | Always present. |\n| `reporter` | object | Always present. |\n| `school` | object | Always present. |\n| `body` | string | Always present; What happened, in the reporter's words. |\n| `context` | IssueContext | Always present; What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers. |\n| `journey_item_key` | string | Always present; may be null; The getting started item it was sent from, if any. |\n| `screenshot` | IssueScreenshot | Always present. |\n| `screenshot_removed` | boolean | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `first_reply_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `fixed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `closed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `reopened_count` | integer | Always present. |\n| `messages_count` | integer | Always present. |\n| `last_message_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `my_side` | string | Always present; one of `reporter`, `school`, `samas`; Which side the caller stands on for this report. |\n| `private_to_samas` | boolean | Always present; 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. |\n| `closed_reason` | string | Always present; may be null; one of `stale`, `office`, `confirmed`, `released`, `None`; 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. |\n| `roadmap_item` | object | May be omitted. |\n| `priority_word` | string | May be omitted; may be null; one of `being looked at`, `planned`, `later`, `None`; 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). |\n| `messages` | array of IssueMessage | Always present. |\n| `events` | array of IssueEvent | 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/IssueMessageWrite"
       },
       "example": {
        "body": "string",
        "picture_upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/IssueDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "ref": "PRB-2026-0042",
          "kind": "app",
          "severity": "blocked",
          "status": "new",
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "owner": "samas",
          "priority": "P1",
          "assignee": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "reporter": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "role": "string",
           "is_me": true
          },
          "school": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "body": "string",
          "context": {
           "screen": "string",
           "role": "string",
           "school_name": "string",
           "error_codes": [
            "string"
           ],
           "console_errors": [
            "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"
             ]
            }
           ]
          },
          "journey_item_key": "string",
          "screenshot": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "content_type": "string",
           "size_bytes": 1,
           "url": "https://erp.example.pk/samas/hook",
           "removed_at": "2026-09-22T09:30:00Z"
          },
          "screenshot_removed": true,
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "first_reply_at": "2026-09-22T09:30:00Z",
          "fixed_at": "2026-09-22T09:30:00Z",
          "closed_at": "2026-09-22T09:30:00Z",
          "reopened_count": 1,
          "messages_count": 1,
          "last_message_at": "2026-09-22T09:30:00Z",
          "my_side": "reporter",
          "private_to_samas": true,
          "closed_reason": "stale",
          "roadmap_item": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "stage": "proposed",
           "target": "string"
          },
          "priority_word": "being looked at",
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "side": "reporter",
            "author": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "body": "string",
            "pictures": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "string",
              "content_type": "string",
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "created_at": "2026-09-22T09:30:00Z"
           }
          ],
          "events": [
           {
            "kind": "created",
            "side": "string",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "note": "string",
            "detail": {},
            "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"
     }
    }
   }
  },
  "/me/issues/{id}/reopen": {
   "post": {
    "tags": [
     "Issues"
    ],
    "summary": "It is not fixed for me",
    "description": "The reporter reopens a report marked fixed or closed (INVALID_STATE otherwise). The note, if any, becomes a message.\n\n**Who can call:** signed-in person (Bearer session token).\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| `note` | string | Optional; may be null; at most 2000 characters; A message the reporter sees, left out for none. |\n\n**Response 200:** `{ data, meta }` where `data` is one IssueDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `kind` | IssueKind | Always present; one of `app`, `school`, `idea`; 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). |\n| `severity` | IssueSeverity | Always present; 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| `status` | IssueStatus | Always present; one of `new`, `looking`, `fixed`, `closed`; new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done. |\n| `duplicate_of` | string | May be omitted; may be null; UUID; The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one. |\n| `owner` | string | Always present; one of `samas`, `school`; Who is looking after it now: the saMas team or the school office. |\n| `priority` | string | Always present; may be null; one of `P1`, `P2`, `P3`, `None`; Set by the saMas team. |\n| `assignee` | IssuePerson | Always present. |\n| `reporter` | object | Always present. |\n| `school` | object | Always present. |\n| `body` | string | Always present; What happened, in the reporter's words. |\n| `context` | IssueContext | Always present; What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers. |\n| `journey_item_key` | string | Always present; may be null; The getting started item it was sent from, if any. |\n| `screenshot` | IssueScreenshot | Always present. |\n| `screenshot_removed` | boolean | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `first_reply_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `fixed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `closed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `reopened_count` | integer | Always present. |\n| `messages_count` | integer | Always present. |\n| `last_message_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `my_side` | string | Always present; one of `reporter`, `school`, `samas`; Which side the caller stands on for this report. |\n| `private_to_samas` | boolean | Always present; 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. |\n| `closed_reason` | string | Always present; may be null; one of `stale`, `office`, `confirmed`, `released`, `None`; 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. |\n| `roadmap_item` | object | May be omitted. |\n| `priority_word` | string | May be omitted; may be null; one of `being looked at`, `planned`, `later`, `None`; 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). |\n| `messages` | array of IssueMessage | Always present. |\n| `events` | array of IssueEvent | 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/IssueNote"
       },
       "example": {
        "note": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/IssueDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "ref": "PRB-2026-0042",
          "kind": "app",
          "severity": "blocked",
          "status": "new",
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "owner": "samas",
          "priority": "P1",
          "assignee": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "reporter": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "role": "string",
           "is_me": true
          },
          "school": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "body": "string",
          "context": {
           "screen": "string",
           "role": "string",
           "school_name": "string",
           "error_codes": [
            "string"
           ],
           "console_errors": [
            "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"
             ]
            }
           ]
          },
          "journey_item_key": "string",
          "screenshot": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "content_type": "string",
           "size_bytes": 1,
           "url": "https://erp.example.pk/samas/hook",
           "removed_at": "2026-09-22T09:30:00Z"
          },
          "screenshot_removed": true,
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "first_reply_at": "2026-09-22T09:30:00Z",
          "fixed_at": "2026-09-22T09:30:00Z",
          "closed_at": "2026-09-22T09:30:00Z",
          "reopened_count": 1,
          "messages_count": 1,
          "last_message_at": "2026-09-22T09:30:00Z",
          "my_side": "reporter",
          "private_to_samas": true,
          "closed_reason": "stale",
          "roadmap_item": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "stage": "proposed",
           "target": "string"
          },
          "priority_word": "being looked at",
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "side": "reporter",
            "author": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "body": "string",
            "pictures": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "string",
              "content_type": "string",
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "created_at": "2026-09-22T09:30:00Z"
           }
          ],
          "events": [
           {
            "kind": "created",
            "side": "string",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "note": "string",
            "detail": {},
            "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"
     }
    }
   }
  },
  "/me/issues/{id}/confirm-fixed": {
   "post": {
    "tags": [
     "Issues"
    ],
    "summary": "It is fixed for me",
    "description": "The reporter confirms a report marked fixed; it closes.\n\n**Who can call:** signed-in person (Bearer session token).\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 IssueDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `kind` | IssueKind | Always present; one of `app`, `school`, `idea`; 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). |\n| `severity` | IssueSeverity | Always present; 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| `status` | IssueStatus | Always present; one of `new`, `looking`, `fixed`, `closed`; new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done. |\n| `duplicate_of` | string | May be omitted; may be null; UUID; The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one. |\n| `owner` | string | Always present; one of `samas`, `school`; Who is looking after it now: the saMas team or the school office. |\n| `priority` | string | Always present; may be null; one of `P1`, `P2`, `P3`, `None`; Set by the saMas team. |\n| `assignee` | IssuePerson | Always present. |\n| `reporter` | object | Always present. |\n| `school` | object | Always present. |\n| `body` | string | Always present; What happened, in the reporter's words. |\n| `context` | IssueContext | Always present; What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers. |\n| `journey_item_key` | string | Always present; may be null; The getting started item it was sent from, if any. |\n| `screenshot` | IssueScreenshot | Always present. |\n| `screenshot_removed` | boolean | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `first_reply_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `fixed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `closed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `reopened_count` | integer | Always present. |\n| `messages_count` | integer | Always present. |\n| `last_message_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `my_side` | string | Always present; one of `reporter`, `school`, `samas`; Which side the caller stands on for this report. |\n| `private_to_samas` | boolean | Always present; 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. |\n| `closed_reason` | string | Always present; may be null; one of `stale`, `office`, `confirmed`, `released`, `None`; 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. |\n| `roadmap_item` | object | May be omitted. |\n| `priority_word` | string | May be omitted; may be null; one of `being looked at`, `planned`, `later`, `None`; 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). |\n| `messages` | array of IssueMessage | Always present. |\n| `events` | array of IssueEvent | Always present. |\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)",
    "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/IssueDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "ref": "PRB-2026-0042",
          "kind": "app",
          "severity": "blocked",
          "status": "new",
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "owner": "samas",
          "priority": "P1",
          "assignee": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "reporter": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "role": "string",
           "is_me": true
          },
          "school": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "body": "string",
          "context": {
           "screen": "string",
           "role": "string",
           "school_name": "string",
           "error_codes": [
            "string"
           ],
           "console_errors": [
            "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"
             ]
            }
           ]
          },
          "journey_item_key": "string",
          "screenshot": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "content_type": "string",
           "size_bytes": 1,
           "url": "https://erp.example.pk/samas/hook",
           "removed_at": "2026-09-22T09:30:00Z"
          },
          "screenshot_removed": true,
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "first_reply_at": "2026-09-22T09:30:00Z",
          "fixed_at": "2026-09-22T09:30:00Z",
          "closed_at": "2026-09-22T09:30:00Z",
          "reopened_count": 1,
          "messages_count": 1,
          "last_message_at": "2026-09-22T09:30:00Z",
          "my_side": "reporter",
          "private_to_samas": true,
          "closed_reason": "stale",
          "roadmap_item": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "stage": "proposed",
           "target": "string"
          },
          "priority_word": "being looked at",
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "side": "reporter",
            "author": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "body": "string",
            "pictures": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "string",
              "content_type": "string",
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "created_at": "2026-09-22T09:30:00Z"
           }
          ],
          "events": [
           {
            "kind": "created",
            "side": "string",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "note": "string",
            "detail": {},
            "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"
     }
    }
   }
  },
  "/me/issues/{id}/screenshot": {
   "delete": {
    "tags": [
     "Issues"
    ],
    "summary": "Remove the screenshot",
    "description": "The reporter, the school office of that school or the saMas team, with a reason of 3 to 200 characters (REASON_REQUIRED). It disappears for everyone but the saMas team, and the removal is logged.\n\n**Who can call:** signed-in person (Bearer session token).\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| `reason` | string | Required; 3 to 200 characters; Why the screenshot is being removed. |\n\n**Response 200:** `{ data, meta }` where `data` is one IssueDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `ref` | string | Always present. |\n| `kind` | IssueKind | Always present; one of `app`, `school`, `idea`; 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). |\n| `severity` | IssueSeverity | Always present; 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| `status` | IssueStatus | Always present; one of `new`, `looking`, `fixed`, `closed`; new: nobody has replied yet. looking: someone is on it. fixed: marked fixed, waiting for the reporter to check. closed: done. |\n| `duplicate_of` | string | May be omitted; may be null; UUID; The first report of the same thing, when the saMas team linked this one to it; it is fixed and closed with that one. |\n| `owner` | string | Always present; one of `samas`, `school`; Who is looking after it now: the saMas team or the school office. |\n| `priority` | string | Always present; may be null; one of `P1`, `P2`, `P3`, `None`; Set by the saMas team. |\n| `assignee` | IssuePerson | Always present. |\n| `reporter` | object | Always present. |\n| `school` | object | Always present. |\n| `body` | string | Always present; What happened, in the reporter's words. |\n| `context` | IssueContext | Always present; What the app attached by itself, shown to the person before sending. Never passwords, codes, message text, fee amounts or identity numbers. |\n| `journey_item_key` | string | Always present; may be null; The getting started item it was sent from, if any. |\n| `screenshot` | IssueScreenshot | Always present. |\n| `screenshot_removed` | boolean | Always present. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `first_reply_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `fixed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `closed_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `reopened_count` | integer | Always present. |\n| `messages_count` | integer | Always present. |\n| `last_message_at` | string | Always present; may be null; timestamp with offset or Z (RFC 3339). |\n| `my_side` | string | Always present; one of `reporter`, `school`, `samas`; Which side the caller stands on for this report. |\n| `private_to_samas` | boolean | Always present; 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. |\n| `closed_reason` | string | Always present; may be null; one of `stale`, `office`, `confirmed`, `released`, `None`; 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. |\n| `roadmap_item` | object | May be omitted. |\n| `priority_word` | string | May be omitted; may be null; one of `being looked at`, `planned`, `later`, `None`; 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). |\n| `messages` | array of IssueMessage | Always present. |\n| `events` | array of IssueEvent | 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/IssueReason"
       },
       "example": {
        "reason": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/IssueDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "ref": "PRB-2026-0042",
          "kind": "app",
          "severity": "blocked",
          "status": "new",
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "owner": "samas",
          "priority": "P1",
          "assignee": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "reporter": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "role": "string",
           "is_me": true
          },
          "school": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "body": "string",
          "context": {
           "screen": "string",
           "role": "string",
           "school_name": "string",
           "error_codes": [
            "string"
           ],
           "console_errors": [
            "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"
             ]
            }
           ]
          },
          "journey_item_key": "string",
          "screenshot": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "content_type": "string",
           "size_bytes": 1,
           "url": "https://erp.example.pk/samas/hook",
           "removed_at": "2026-09-22T09:30:00Z"
          },
          "screenshot_removed": true,
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "first_reply_at": "2026-09-22T09:30:00Z",
          "fixed_at": "2026-09-22T09:30:00Z",
          "closed_at": "2026-09-22T09:30:00Z",
          "reopened_count": 1,
          "messages_count": 1,
          "last_message_at": "2026-09-22T09:30:00Z",
          "my_side": "reporter",
          "private_to_samas": true,
          "closed_reason": "stale",
          "roadmap_item": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "stage": "proposed",
           "target": "string"
          },
          "priority_word": "being looked at",
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "side": "reporter",
            "author": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "body": "string",
            "pictures": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "string",
              "content_type": "string",
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "created_at": "2026-09-22T09:30:00Z"
           }
          ],
          "events": [
           {
            "kind": "created",
            "side": "string",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "note": "string",
            "detail": {},
            "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"
     }
    }
   }
  },
  "/me/badges": {
   "get": {
    "tags": [
     "Messages"
    ],
    "summary": "Counts for the menu",
    "description": "Conversations with unread messages, permission slips waiting for an answer and (for the office) reported messages.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one Badges.\n\n| Field | Type | Notes |\n|---|---|---|\n| `messages_unread` | integer | Always present; Conversations with unread messages. |\n| `slips_waiting` | integer | Always present; Permission slips waiting for the caller's answer. |\n| `reports_open` | integer | Always present; Reported messages waiting for the office. |\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/Badges"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "messages_unread": 1,
          "slips_waiting": 1,
          "reports_open": 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"
     }
    }
   }
  },
  "/me/messaging/contacts": {
   "get": {
    "tags": [
     "Messages"
    ],
    "summary": "Who I can write to",
    "description": "Per child for families; teachers also get their students' families and the school's staff; the office searches any child with q.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `q` | query | string | Optional; at most 80 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one MessageContacts.\n\n| Field | Type | Notes |\n|---|---|---|\n| `children` | array of object | Always present. |\n| `staff` | array of MessagePerson | 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"
     },
     {
      "name": "q",
      "in": "query",
      "schema": {
       "type": "string",
       "maxLength": 80
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/MessageContacts"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "children": [
           {
            "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "class_label": "Grade 5 A",
            "student_can_write": true,
            "people": [
             {
              "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "full_name": "string",
              "role": "teacher",
              "detail": "string",
              "in_thread_as": "staff"
             }
            ]
           }
          ],
          "staff": [
           {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "role": "teacher",
            "detail": "string",
            "in_thread_as": "staff"
           }
          ]
         },
         "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"
     }
    }
   }
  },
  "/me/threads": {
   "get": {
    "tags": [
     "Messages"
    ],
    "summary": "My conversations",
    "description": "Newest first. Parents see every conversation about their children, including ones their child started.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | query | string | Optional; UUID; at most 40 characters; Only conversations about this child. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of ThreadSummary.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `subject` | string | Always present. |\n| `includes_student` | boolean | May be omitted. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `last_message_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `student` | object | May be omitted. |\n| `people` | array of MessagePerson | Always present. |\n| `last_message` | object | May be omitted. |\n| `unread` | integer | Always present. |\n| `open_reports` | integer | May be omitted; Reports not yet dealt with (the office acts on these). |\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": "student_id",
      "in": "query",
      "schema": {
       "type": "string",
       "description": "Only conversations about this child.",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/ThreadSummary"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "subject": "string",
           "includes_student": true,
           "created_at": "2026-09-22T09:30:00Z",
           "last_message_at": "2026-09-22T09:30:00Z",
           "student": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "people": [
            {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "role": "teacher",
             "detail": "string",
             "in_thread_as": "staff"
            }
           ],
           "last_message": {
            "sender": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "role": "teacher",
             "detail": "string",
             "in_thread_as": "staff"
            },
            "created_at": "2026-09-22T09:30:00Z",
            "preview": "string",
            "removed": true,
            "files": 1
           },
           "unread": 1,
           "open_reports": 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": [
     "Messages"
    ],
    "summary": "Start a conversation",
    "description": "About one child (the child's parents are always in it) or, for staff, between staff. Refused with MESSAGING_NOT_ALLOWED outside the school's rules, MESSAGING_PAUSED when the school paused the caller, BLOCKED_WORDS for words the school does not allow.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `student_id` | string | Optional; may be null; UUID; at most 40 characters; The child it is about. Leave out for a staff conversation. |\n| `to` | array of string | Required; at most 10 items; From the contacts list. The child's parents are always added to conversations about the child. |\n| `subject` | string | Required; 2 to 120 characters. |\n| `body` | string | Optional; may be null; at most 2000 characters. |\n| `files` | array of HomeworkFileRef | Optional; at most 5 items. |\n\n**Response 201:** `{ data, meta }` where `data` is one ThreadDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `subject` | string | Always present. |\n| `includes_student` | boolean | May be omitted. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `last_message_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `student` | object | May be omitted. |\n| `people` | array of MessagePerson | Always present. |\n| `last_message` | object | May be omitted. |\n| `unread` | integer | May be omitted. |\n| `open_reports` | integer | May be omitted. |\n| `access` | string | Always present; one of `member`, `office`; office: the principal or school admin reading a conversation they are not in (logged). |\n| `can_reply` | boolean | Always present. |\n| `messages` | array of Message | 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/ThreadStart"
       },
       "example": {
        "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "to": [
         "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
        ],
        "subject": "string",
        "body": "string",
        "files": [
         {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ThreadDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "subject": "string",
          "includes_student": true,
          "created_at": "2026-09-22T09:30:00Z",
          "last_message_at": "2026-09-22T09:30:00Z",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "people": [
           {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "role": "teacher",
            "detail": "string",
            "in_thread_as": "staff"
           }
          ],
          "last_message": {},
          "unread": 1,
          "open_reports": 1,
          "access": "member",
          "can_reply": true,
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "sender": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "role": "teacher",
             "detail": "string",
             "in_thread_as": "staff"
            },
            "created_at": "2026-09-22T09:30:00Z",
            "mine": true,
            "body": "string",
            "files": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "worksheet.pdf",
              "content_type": "string",
              "size_bytes": 1,
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "deleted": true,
            "hidden": true,
            "hidden_reason": "string",
            "can_delete": true,
            "reports": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "reporter": {},
              "reason": "string",
              "created_at": "2026-09-22T09:30:00Z",
              "resolved": 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"
     }
    }
   }
  },
  "/me/threads/{id}": {
   "get": {
    "tags": [
     "Messages"
    ],
    "summary": "A conversation",
    "description": "With every message. Deleted and removed messages show without their text.\n\n**Who can call:** signed-in person (Bearer session token).\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 ThreadDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `subject` | string | Always present. |\n| `includes_student` | boolean | May be omitted. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `last_message_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `student` | object | May be omitted. |\n| `people` | array of MessagePerson | Always present. |\n| `last_message` | object | May be omitted. |\n| `unread` | integer | May be omitted. |\n| `open_reports` | integer | May be omitted. |\n| `access` | string | Always present; one of `member`, `office`; office: the principal or school admin reading a conversation they are not in (logged). |\n| `can_reply` | boolean | Always present. |\n| `messages` | array of Message | 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- 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": {
           "$ref": "#/components/schemas/ThreadDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "subject": "string",
          "includes_student": true,
          "created_at": "2026-09-22T09:30:00Z",
          "last_message_at": "2026-09-22T09:30:00Z",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "people": [
           {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "role": "teacher",
            "detail": "string",
            "in_thread_as": "staff"
           }
          ],
          "last_message": {},
          "unread": 1,
          "open_reports": 1,
          "access": "member",
          "can_reply": true,
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "sender": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "role": "teacher",
             "detail": "string",
             "in_thread_as": "staff"
            },
            "created_at": "2026-09-22T09:30:00Z",
            "mine": true,
            "body": "string",
            "files": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "worksheet.pdf",
              "content_type": "string",
              "size_bytes": 1,
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "deleted": true,
            "hidden": true,
            "hidden_reason": "string",
            "can_delete": true,
            "reports": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "reporter": {},
              "reason": "string",
              "created_at": "2026-09-22T09:30:00Z",
              "resolved": 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"
     }
    }
   }
  },
  "/me/threads/{id}/messages": {
   "post": {
    "tags": [
     "Messages"
    ],
    "summary": "Send a message",
    "description": "Text up to 2,000 characters and/or up to 5 files.\n\n**Who can call:** signed-in person (Bearer session token).\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| `body` | string | Optional; may be null; at most 2000 characters. |\n| `files` | array of HomeworkFileRef | Optional; at most 5 items. |\n\n**Response 201:** `{ data, meta }` where `data` is one ThreadDetail.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `subject` | string | Always present. |\n| `includes_student` | boolean | May be omitted. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `last_message_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `student` | object | May be omitted. |\n| `people` | array of MessagePerson | Always present. |\n| `last_message` | object | May be omitted. |\n| `unread` | integer | May be omitted. |\n| `open_reports` | integer | May be omitted. |\n| `access` | string | Always present; one of `member`, `office`; office: the principal or school admin reading a conversation they are not in (logged). |\n| `can_reply` | boolean | Always present. |\n| `messages` | array of Message | 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/MessageWrite"
       },
       "example": {
        "body": "string",
        "files": [
         {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ThreadDetail"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "subject": "string",
          "includes_student": true,
          "created_at": "2026-09-22T09:30:00Z",
          "last_message_at": "2026-09-22T09:30:00Z",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "people": [
           {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "role": "teacher",
            "detail": "string",
            "in_thread_as": "staff"
           }
          ],
          "last_message": {},
          "unread": 1,
          "open_reports": 1,
          "access": "member",
          "can_reply": true,
          "messages": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "sender": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "role": "teacher",
             "detail": "string",
             "in_thread_as": "staff"
            },
            "created_at": "2026-09-22T09:30:00Z",
            "mine": true,
            "body": "string",
            "files": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "name": "worksheet.pdf",
              "content_type": "string",
              "size_bytes": 1,
              "url": "https://erp.example.pk/samas/hook"
             }
            ],
            "deleted": true,
            "hidden": true,
            "hidden_reason": "string",
            "can_delete": true,
            "reports": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "reporter": {},
              "reason": "string",
              "created_at": "2026-09-22T09:30:00Z",
              "resolved": 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"
     }
    }
   }
  },
  "/me/threads/{id}/read": {
   "post": {
    "tags": [
     "Messages"
    ],
    "summary": "Mark a conversation read",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "responses": {
     "204": {
      "description": "Marked."
     },
     "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": "Mark a conversation read.\n\n**Who can call:** signed-in person (Bearer session token).\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- 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)"
   }
  },
  "/me/messages/{id}": {
   "delete": {
    "tags": [
     "Messages"
    ],
    "summary": "Delete my message",
    "description": "Only in the first 5 minutes (TOO_OLD after that). The school keeps it.\n\n**Who can call:** signed-in person (Bearer session token).\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)",
    "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"
     }
    }
   }
  },
  "/me/messages/{id}/report": {
   "post": {
    "tags": [
     "Messages"
    ],
    "summary": "Report a message",
    "description": "The office is told and can remove it.\n\n**Who can call:** signed-in person (Bearer session token).\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| `reason` | string | Required; 3 to 300 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "$ref": "#/components/parameters/id"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "reason": {
          "type": "string",
          "minLength": 3,
          "maxLength": 300
         }
        },
        "required": [
         "reason"
        ],
        "additionalProperties": false
       },
       "example": {
        "reason": "string"
       }
      }
     }
    },
    "responses": {
     "204": {
      "description": "Reported."
     },
     "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"
     }
    }
   }
  },
  "/me/messages/upload-url": {
   "post": {
    "tags": [
     "Messages"
    ],
    "summary": "Get an upload URL for a file to send",
    "description": "Photos, PDF or Office files, 20 MB at most. PUT the bytes, then send {upload_id} in the message's files.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `file_name` | string | Required; 1 to 200 characters. |\n| `content_type` | string | Required; one of `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`. |\n| `size_bytes` | integer | Required; from 1 to 20971520. |\n\n**Response 200:** `{ data, meta }` where `data` is one UploadUrl.\n\n| Field | Type | Notes |\n|---|---|---|\n| `upload_id` | string | Always present. |\n| `url` | string | Always present; https URL; PUT the file bytes here within 15 minutes. |\n| `expires_at` | string | Always present; 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- 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/HomeworkUpload"
       },
       "example": {
        "file_name": "string",
        "content_type": "image/jpeg",
        "size_bytes": 1
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/UploadUrl"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "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"
     }
    }
   }
  },
  "/me/slips": {
   "get": {
    "tags": [
     "Forms"
    ],
    "summary": "My permission slips",
    "description": "One per child, waiting ones first.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of MySlip.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `title` | string | Always present. |\n| `details` | string | May be omitted; may be null. |\n| `event_on` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `sign_by` | string | Always present; date YYYY-MM-DD. |\n| `files` | array of HomeworkFile | May be omitted. |\n| `questions` | array of SlipQuestion | Always present. |\n| `status` | string | Always present; one of `open`, `closed`, `cancelled`. |\n| `cancel_note` | string | May be omitted; may be null. |\n| `created_by` | MessagePerson | May be omitted. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `student` | object | Always present. |\n| `can_answer` | boolean | Always present. |\n| `answer` | 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": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/MySlip"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "details": "string",
           "event_on": "2026-09-22",
           "sign_by": "2026-09-01",
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "worksheet.pdf",
             "content_type": "string",
             "size_bytes": 1,
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "questions": [
            {
             "id": "q1",
             "label": "string",
             "kind": "yes_no",
             "required": true
            }
           ],
           "status": "open",
           "cancel_note": "string",
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "role": "teacher",
            "detail": "string",
            "in_thread_as": "staff"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "student": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "can_answer": true,
           "answer": {
            "decision": "agree",
            "answers": {},
            "signed_name": "string",
            "signed_at": "2026-09-22T09:30:00Z",
            "by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "role": "teacher",
             "detail": "string",
             "in_thread_as": "staff"
            },
            "mine": 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"
     }
    }
   }
  },
  "/me/slips/{id}/answer": {
   "post": {
    "tags": [
     "Forms"
    ],
    "summary": "Answer a permission slip",
    "description": "For one child. The typed name, the account, the time, the IP address and the browser are kept as the signature. Can be changed until the last date (SLIP_CLOSED after); every change is kept.\n\n**Who can call:** signed-in person (Bearer session token).\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| `student_id` | string | Required; UUID; at most 40 characters. |\n| `decision` | string | Required; one of `agree`, `decline`. |\n| `answers` | object | Optional; Question id to answer; yes_no questions take yes or no. |\n| `signed_name` | string | Required; 3 to 100 characters; The parent's full name, typed as their signature. |\n| `confirm` | boolean | Required; The \"I agree\" tick. |\n\n**Response 200:** `{ data, meta }` where `data` is one MySlip.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `title` | string | Always present. |\n| `details` | string | May be omitted; may be null. |\n| `event_on` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `sign_by` | string | Always present; date YYYY-MM-DD. |\n| `files` | array of HomeworkFile | May be omitted. |\n| `questions` | array of SlipQuestion | Always present. |\n| `status` | string | Always present; one of `open`, `closed`, `cancelled`. |\n| `cancel_note` | string | May be omitted; may be null. |\n| `created_by` | MessagePerson | May be omitted. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `student` | object | Always present. |\n| `can_answer` | boolean | Always present. |\n| `answer` | 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": {
        "$ref": "#/components/schemas/SlipAnswerWrite"
       },
       "example": {
        "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "decision": "agree",
        "answers": {},
        "signed_name": "string",
        "confirm": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/MySlip"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "details": "string",
          "event_on": "2026-09-22",
          "sign_by": "2026-09-01",
          "files": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "worksheet.pdf",
            "content_type": "string",
            "size_bytes": 1,
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "questions": [
           {
            "id": "q1",
            "label": "string",
            "kind": "yes_no",
            "required": true
           }
          ],
          "status": "open",
          "cancel_note": "string",
          "created_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "role": "teacher",
           "detail": "string",
           "in_thread_as": "staff"
          },
          "created_at": "2026-09-22T09:30:00Z",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "can_answer": true,
          "answer": {
           "decision": "agree",
           "answers": {},
           "signed_name": "string",
           "signed_at": "2026-09-22T09:30:00Z",
           "by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "role": "teacher",
            "detail": "string",
            "in_thread_as": "staff"
           },
           "mine": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/exams": {
   "get": {
    "tags": [
     "Exams"
    ],
    "summary": "A child's exams",
    "description": "Date sheets that are out for the child's class, newest first, with marks once the class's results are published.\n\n**Who can call:** signed-in person (Bearer session token).\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 ChildExams.\n\n| Field | Type | Notes |\n|---|---|---|\n| `term` | object | Always present. |\n| `published_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339); Results for the child's class; marks are null until then. |\n| `papers` | array of object | 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- 429 RATE_LIMITED (see Retry-After)",
    "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": {
            "$ref": "#/components/schemas/ChildExams"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "term": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "starts_on": "2026-09-01",
            "ends_on": "2026-09-01",
            "instructions": "string"
           },
           "published_at": "2026-09-22T09:30:00Z",
           "papers": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "subject": "string",
             "on_date": "2026-09-01",
             "starts_at": "string",
             "duration_min": 1,
             "room": "string",
             "max_marks": 1,
             "marks": 1,
             "absent": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/quran": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "A child's Quran record",
    "description": "Quran Madrasa add-on: the same record the teacher sees (enrolment, today's lesson, the week, the juz map, the tajweed chart) with the teacher's remarks. A parent for their child; a student passes their own id. ADD_ON_OFF when the school has not turned the add-on on.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `on` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one QuranStudent.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student` | object | Always present. |\n| `enrollment` | QuranEnrollment | May be omitted. |\n| `today` | QuranDay | Always present. |\n| `week` | array of QuranDay | Always present. |\n| `juz_map` | array of JuzState | Always present. |\n| `tajweed` | object | Always present. |\n| `attention` | array of object | May be omitted. |\n| `recitations` | array of Recitation | May be omitted. |\n| `qiraat` | object | Always present. |\n| `practice` | object | 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/student_id"
     },
     {
      "name": "on",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date",
       "example": "2026-09-01",
       "maxLength": 40
      },
      "description": "Default today."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/QuranStudent"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "first_name": "string",
           "full_name": "string",
           "admission_no": "string",
           "halaqa": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "label": "string"
           }
          },
          "enrollment": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "program": "qaida",
           "riwayah": "hafs_asim",
           "teacher": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "started_on": "2026-09-01",
           "ended_on": "2026-09-22",
           "ended_reason": "string"
          },
          "today": {
           "on": "2026-09-01",
           "parts": [
            {
             "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "on": "2026-09-01",
             "part": "sabaq",
             "kind": "range",
             "surah": 1,
             "ayah_from": 1,
             "ayah_to": 1,
             "juz_from": 1,
             "juz_to": 1,
             "qaida_lesson": 1,
             "riwayah": "string",
             "result": "pending",
             "grade": "mumtaz",
             "remark": "string",
             "graded_at": "2026-09-22T09:30:00Z",
             "due_date": "2026-09-01",
             "assignment_status": "assigned"
            }
           ]
          },
          "week": [
           {
            "on": "2026-09-01",
            "parts": [
             {
              "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "on": "2026-09-01",
              "part": "sabaq",
              "kind": "range",
              "surah": 1,
              "ayah_from": 1,
              "ayah_to": 1,
              "juz_from": 1,
              "juz_to": 1,
              "qaida_lesson": 1,
              "riwayah": "string",
              "result": "pending",
              "grade": "mumtaz",
              "remark": "string",
              "graded_at": "2026-09-22T09:30:00Z",
              "due_date": "2026-09-01",
              "assignment_status": "assigned"
             }
            ]
           }
          ],
          "juz_map": [
           {
            "juz": 1,
            "state": "not_started",
            "riwayah": "string",
            "memorized_on": "2026-09-01",
            "tested_on": "2026-09-01",
            "test_grade": "mumtaz"
           }
          ],
          "tajweed": {
           "days_30": {},
           "days_90": {},
           "recent": [
            {
             "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "recitation_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "surah": 1,
             "ayah": 1,
             "word_index": 1,
             "word": "string",
             "category": "makharij",
             "text": "string",
             "written_at": "2026-09-22T09:30:00Z",
             "written_by": {
              "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "full_name": "string"
             },
             "at_seconds": 1
            }
           ]
          },
          "attention": [
           {
            "rule": "string",
            "since": "2026-09-01",
            "detail": {}
           }
          ],
          "recitations": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "student": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "first_name": "string",
             "full_name": "string",
             "halaqa": {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "label": "string"
             }
            },
            "kind": "student",
            "attempt": 1,
            "status": "uploading",
            "riwayah": "string",
            "surah": 1,
            "surah_name": "string",
            "ayah_from": 1,
            "ayah_to": 1,
            "juz": 1,
            "lesson": {
             "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "on": "2026-09-01",
             "part": "sabaq",
             "kind": "range",
             "surah": 1,
             "ayah_from": 1,
             "ayah_to": 1,
             "juz_from": 1,
             "juz_to": 1,
             "qaida_lesson": 1,
             "riwayah": "string",
             "result": "pending",
             "grade": "mumtaz",
             "remark": "string",
             "graded_at": "2026-09-22T09:30:00Z",
             "due_date": "2026-09-01",
             "assignment_status": "assigned"
            },
            "duration_seconds": 1,
            "content_type": "string",
            "ayah_marks": [
             {
              "ayah": 1,
              "at_seconds": 1.5
             }
            ],
            "submitted_at": "2026-09-22T09:30:00Z",
            "waited_hours": 1,
            "reviewed_at": "2026-09-22T09:30:00Z",
            "reviewed_by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "grade": "string",
            "remark": "string",
            "notes": [
             {
              "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "recitation_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "surah": 1,
              "ayah": 1,
              "word_index": 1,
              "word": "string",
              "category": "makharij",
              "text": "string",
              "written_at": "2026-09-22T09:30:00Z",
              "written_by": {},
              "at_seconds": 1
             }
            ],
            "model": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "duration_seconds": 1,
             "status": "string"
            },
            "pinned": true,
            "delete_after": "2026-09-22",
            "deleted_at": "2026-09-22T09:30:00Z",
            "playable": true,
            "plays": 1,
            "from_app": "string"
           }
          ],
          "qiraat": {
           "paths": [
            {}
           ]
          },
          "practice": {
           "linked": true,
           "state": "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"
     }
    }
   }
  },
  "/me/students/{student_id}/quran/lessons": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "A child's Quran lessons",
    "description": "Each logged day with its parts and results, newest first; from and to default to the last 30 days (at most 120).\n\n**Who can call:** signed-in person (Bearer session token).\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\n**Response 200:** `{ data, meta }` where `data` is a list of QuranDay.\n\n| Field | Type | Notes |\n|---|---|---|\n| `on` | string | Always present; date YYYY-MM-DD. |\n| `parts` | array of QuranLessonPart | 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- 429 RATE_LIMITED (see Retry-After)",
    "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
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/QuranDay"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "on": "2026-09-01",
           "parts": [
            {
             "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "on": "2026-09-01",
             "part": "sabaq",
             "kind": "range",
             "surah": 1,
             "ayah_from": 1,
             "ayah_to": 1,
             "juz_from": 1,
             "juz_to": 1,
             "qaida_lesson": 1,
             "riwayah": "string",
             "result": "pending",
             "grade": "mumtaz",
             "remark": "string",
             "graded_at": "2026-09-22T09:30:00Z",
             "due_date": "2026-09-01",
             "assignment_status": "assigned"
            }
           ]
          }
         ],
         "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"
     }
    }
   }
  },
  "/me/quran/text": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "Quran text",
    "description": "The Uthmani text of a range (Hafs, Kufan count) for the family site's lesson card. Any signed-in member.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `surah` | query | integer | Required; from 1 to 114. |\n| `ayah_from` | query | integer | Optional; from 1 to 286. |\n| `ayah_to` | query | integer | Optional; from 1 to 286. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of QuranAyah.\n\n| Field | Type | Notes |\n|---|---|---|\n| `ayah` | integer | Always present. |\n| `text` | string | Always present; The Uthmani text (Hafs, Kufan count). |\n| `words` | array of 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "surah",
      "in": "query",
      "required": true,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 114
      }
     },
     {
      "name": "ayah_from",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 286
      },
      "description": "Default 1."
     },
     {
      "name": "ayah_to",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 286
      },
      "description": "Default ayah_from; at most 50 ayahs at a time (OUT_OF_RANGE)."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/QuranAyah"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "ayah": 1,
           "text": "string",
           "words": [
            "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"
     }
    }
   }
  },
  "/me/quran/links": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "Quran app links for the family",
    "description": "Pending requests from a Quran reading app and active links across the caller's children (a student: their own). ADD_ON_OFF when the school has no madrasa.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of QuranLink.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student` | object | Always present. |\n| `partner` | object | Always present. |\n| `status` | string | Always present; one of `pending`, `active`, `revoked`, `expired`. |\n| `account_hint` | string | May be omitted; may be null; The first two characters of the app account, for support; the account itself is never stored. |\n| `display` | string | May be omitted; may be null. |\n| `requested_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `approved_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `linked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_by` | string | May be omitted; may be null. |\n| `revoke_note` | string | May be omitted; may be null. |\n| `student_requested_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `practice_deleted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `approved_by` | PersonRef | May be omitted. |\n| `shares` | array of 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- 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/QuranLink"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "student": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "first_name": "string",
            "full_name": "string",
            "halaqa": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "label": "string"
            }
           },
           "partner": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "slug": "string"
           },
           "status": "pending",
           "account_hint": "string",
           "display": "string",
           "requested_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "approved_at": "2026-09-22T09:30:00Z",
           "linked_at": "2026-09-22T09:30:00Z",
           "revoked_at": "2026-09-22T09:30:00Z",
           "revoked_by": "string",
           "revoke_note": "string",
           "student_requested_at": "2026-09-22T09:30:00Z",
           "practice_deleted_at": "2026-09-22T09:30:00Z",
           "approved_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "shares": [
            "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"
     }
    }
   }
  },
  "/me/quran/links/{link_id}/approve": {
   "post": {
    "tags": [
     "Quran"
    ],
    "summary": "Approve a link",
    "description": "A parent of the child, themselves (never the office, never while viewing as someone). From then on the app receives the child's homework and may send home practice and recitations. LINK_NOT_PENDING when already answered or expired.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `link_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one QuranLink.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student` | object | Always present. |\n| `partner` | object | Always present. |\n| `status` | string | Always present; one of `pending`, `active`, `revoked`, `expired`. |\n| `account_hint` | string | May be omitted; may be null; The first two characters of the app account, for support; the account itself is never stored. |\n| `display` | string | May be omitted; may be null. |\n| `requested_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `approved_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `linked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_by` | string | May be omitted; may be null. |\n| `revoke_note` | string | May be omitted; may be null. |\n| `student_requested_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `practice_deleted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `approved_by` | PersonRef | May be omitted. |\n| `shares` | array of string | Always present. |\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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "link_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/QuranLink"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "first_name": "string",
           "full_name": "string",
           "halaqa": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "label": "string"
           }
          },
          "partner": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "slug": "string"
          },
          "status": "pending",
          "account_hint": "string",
          "display": "string",
          "requested_at": "2026-09-22T09:30:00Z",
          "expires_at": "2026-09-22T09:30:00Z",
          "approved_at": "2026-09-22T09:30:00Z",
          "linked_at": "2026-09-22T09:30:00Z",
          "revoked_at": "2026-09-22T09:30:00Z",
          "revoked_by": "string",
          "revoke_note": "string",
          "student_requested_at": "2026-09-22T09:30:00Z",
          "practice_deleted_at": "2026-09-22T09:30:00Z",
          "approved_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "shares": [
           "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"
     }
    }
   }
  },
  "/me/quran/links/{link_id}/decline": {
   "post": {
    "tags": [
     "Quran"
    ],
    "summary": "Decline a link request",
    "description": "A parent of the child; the app is told.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `link_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| `note` | string | Optional; at most 200 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one QuranLink.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student` | object | Always present. |\n| `partner` | object | Always present. |\n| `status` | string | Always present; one of `pending`, `active`, `revoked`, `expired`. |\n| `account_hint` | string | May be omitted; may be null; The first two characters of the app account, for support; the account itself is never stored. |\n| `display` | string | May be omitted; may be null. |\n| `requested_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `approved_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `linked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_by` | string | May be omitted; may be null. |\n| `revoke_note` | string | May be omitted; may be null. |\n| `student_requested_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `practice_deleted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `approved_by` | PersonRef | May be omitted. |\n| `shares` | array of 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- 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"
     },
     {
      "name": "link_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "note": {
          "type": "string",
          "maxLength": 200
         }
        },
        "additionalProperties": false
       },
       "example": {
        "note": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/QuranLink"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "first_name": "string",
           "full_name": "string",
           "halaqa": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "label": "string"
           }
          },
          "partner": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "slug": "string"
          },
          "status": "pending",
          "account_hint": "string",
          "display": "string",
          "requested_at": "2026-09-22T09:30:00Z",
          "expires_at": "2026-09-22T09:30:00Z",
          "approved_at": "2026-09-22T09:30:00Z",
          "linked_at": "2026-09-22T09:30:00Z",
          "revoked_at": "2026-09-22T09:30:00Z",
          "revoked_by": "string",
          "revoke_note": "string",
          "student_requested_at": "2026-09-22T09:30:00Z",
          "practice_deleted_at": "2026-09-22T09:30:00Z",
          "approved_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "shares": [
           "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"
     }
    }
   }
  },
  "/me/quran/links/{link_id}": {
   "delete": {
    "tags": [
     "Quran"
    ],
    "summary": "End a link",
    "description": "A parent ends the child's link; delete_practice also removes every day of home practice the app sent. The app is told and stops receiving anything about the child.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `link_id` | path | string | Required; UUID; at most 40 characters. |\n| `delete_practice` | query | boolean | Optional. |\n| `note` | query | string | Optional; at most 200 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one QuranLink.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student` | object | Always present. |\n| `partner` | object | Always present. |\n| `status` | string | Always present; one of `pending`, `active`, `revoked`, `expired`. |\n| `account_hint` | string | May be omitted; may be null; The first two characters of the app account, for support; the account itself is never stored. |\n| `display` | string | May be omitted; may be null. |\n| `requested_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `approved_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `linked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_by` | string | May be omitted; may be null. |\n| `revoke_note` | string | May be omitted; may be null. |\n| `student_requested_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `practice_deleted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `approved_by` | PersonRef | May be omitted. |\n| `shares` | array of 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- 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"
     },
     {
      "name": "link_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "delete_practice",
      "in": "query",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "name": "note",
      "in": "query",
      "schema": {
       "type": "string",
       "maxLength": 200
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/QuranLink"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "first_name": "string",
           "full_name": "string",
           "halaqa": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "label": "string"
           }
          },
          "partner": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "slug": "string"
          },
          "status": "pending",
          "account_hint": "string",
          "display": "string",
          "requested_at": "2026-09-22T09:30:00Z",
          "expires_at": "2026-09-22T09:30:00Z",
          "approved_at": "2026-09-22T09:30:00Z",
          "linked_at": "2026-09-22T09:30:00Z",
          "revoked_at": "2026-09-22T09:30:00Z",
          "revoked_by": "string",
          "revoke_note": "string",
          "student_requested_at": "2026-09-22T09:30:00Z",
          "practice_deleted_at": "2026-09-22T09:30:00Z",
          "approved_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "shares": [
           "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"
     }
    }
   }
  },
  "/me/quran/links/{link_id}/ask-unlink": {
   "post": {
    "tags": [
     "Quran"
    ],
    "summary": "Ask to unlink (student)",
    "description": "The student asks; their parents are told and one of them ends the link.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `link_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one QuranLink.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student` | object | Always present. |\n| `partner` | object | Always present. |\n| `status` | string | Always present; one of `pending`, `active`, `revoked`, `expired`. |\n| `account_hint` | string | May be omitted; may be null; The first two characters of the app account, for support; the account itself is never stored. |\n| `display` | string | May be omitted; may be null. |\n| `requested_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `approved_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `linked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `revoked_by` | string | May be omitted; may be null. |\n| `revoke_note` | string | May be omitted; may be null. |\n| `student_requested_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `practice_deleted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `approved_by` | PersonRef | May be omitted. |\n| `shares` | array of string | Always present. |\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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "link_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/QuranLink"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "first_name": "string",
           "full_name": "string",
           "halaqa": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "label": "string"
           }
          },
          "partner": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string",
           "slug": "string"
          },
          "status": "pending",
          "account_hint": "string",
          "display": "string",
          "requested_at": "2026-09-22T09:30:00Z",
          "expires_at": "2026-09-22T09:30:00Z",
          "approved_at": "2026-09-22T09:30:00Z",
          "linked_at": "2026-09-22T09:30:00Z",
          "revoked_at": "2026-09-22T09:30:00Z",
          "revoked_by": "string",
          "revoke_note": "string",
          "student_requested_at": "2026-09-22T09:30:00Z",
          "practice_deleted_at": "2026-09-22T09:30:00Z",
          "approved_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "shares": [
           "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"
     }
    }
   }
  },
  "/me/students/{student_id}/quran/recitations": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "A child's recitations",
    "description": "Recordings the child sent from the Quran reading app, with the teacher's feedback once reviewed; newest first.\n\n**Who can call:** signed-in person (Bearer session token).\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 Recitation.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student` | object | Always present. |\n| `kind` | string | Always present; one of `student`, `model`, `juz_test`. |\n| `attempt` | integer | Always present. |\n| `status` | string | Always present; one of `uploading`, `submitted`, `reviewed`, `deleted`. |\n| `riwayah` | string | May be omitted; at most 20 characters. |\n| `surah` | integer | May be omitted; may be null. |\n| `surah_name` | string | May be omitted; may be null. |\n| `ayah_from` | integer | May be omitted; may be null. |\n| `ayah_to` | integer | May be omitted; may be null. |\n| `juz` | integer | May be omitted; may be null. |\n| `lesson` | QuranLessonPart | May be omitted. |\n| `duration_seconds` | integer | Always present. |\n| `content_type` | string | Always present; at most 20 characters. |\n| `ayah_marks` | array of object | Always present. |\n| `submitted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `waited_hours` | integer | May be omitted; may be null. |\n| `reviewed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `reviewed_by` | PersonRef | May be omitted. |\n| `grade` | string | May be omitted; may be null. |\n| `remark` | string | May be omitted; may be null. |\n| `notes` | array of TajweedNote | Always present. |\n| `model` | object | May be omitted. |\n| `pinned` | boolean | Always present. |\n| `delete_after` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `deleted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `playable` | boolean | Always present. |\n| `plays` | integer | Always present. |\n| `from_app` | 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- 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/student_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Recitation"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "student": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "first_name": "string",
            "full_name": "string",
            "halaqa": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "label": "string"
            }
           },
           "kind": "student",
           "attempt": 1,
           "status": "uploading",
           "riwayah": "string",
           "surah": 1,
           "surah_name": "string",
           "ayah_from": 1,
           "ayah_to": 1,
           "juz": 1,
           "lesson": {
            "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "on": "2026-09-01",
            "part": "sabaq",
            "kind": "range",
            "surah": 1,
            "ayah_from": 1,
            "ayah_to": 1,
            "juz_from": 1,
            "juz_to": 1,
            "qaida_lesson": 1,
            "riwayah": "string",
            "result": "pending",
            "grade": "mumtaz",
            "remark": "string",
            "graded_at": "2026-09-22T09:30:00Z",
            "due_date": "2026-09-01",
            "assignment_status": "assigned"
           },
           "duration_seconds": 1,
           "content_type": "string",
           "ayah_marks": [
            {
             "ayah": 1,
             "at_seconds": 1.5
            }
           ],
           "submitted_at": "2026-09-22T09:30:00Z",
           "waited_hours": 1,
           "reviewed_at": "2026-09-22T09:30:00Z",
           "reviewed_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "grade": "string",
           "remark": "string",
           "notes": [
            {
             "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "recitation_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "surah": 1,
             "ayah": 1,
             "word_index": 1,
             "word": "string",
             "category": "makharij",
             "text": "string",
             "written_at": "2026-09-22T09:30:00Z",
             "written_by": {
              "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "full_name": "string"
             },
             "at_seconds": 1
            }
           ],
           "model": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "duration_seconds": 1,
            "status": "string"
           },
           "pinned": true,
           "delete_after": "2026-09-22",
           "deleted_at": "2026-09-22T09:30:00Z",
           "playable": true,
           "plays": 1,
           "from_app": "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"
     }
    }
   }
  },
  "/me/quran/recitations/{recitation_id}": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "One of the child's recitations",
    "description": "With the notes and the model reading.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `recitation_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one Recitation.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student` | object | Always present. |\n| `kind` | string | Always present; one of `student`, `model`, `juz_test`. |\n| `attempt` | integer | Always present. |\n| `status` | string | Always present; one of `uploading`, `submitted`, `reviewed`, `deleted`. |\n| `riwayah` | string | May be omitted; at most 20 characters. |\n| `surah` | integer | May be omitted; may be null. |\n| `surah_name` | string | May be omitted; may be null. |\n| `ayah_from` | integer | May be omitted; may be null. |\n| `ayah_to` | integer | May be omitted; may be null. |\n| `juz` | integer | May be omitted; may be null. |\n| `lesson` | QuranLessonPart | May be omitted. |\n| `duration_seconds` | integer | Always present. |\n| `content_type` | string | Always present; at most 20 characters. |\n| `ayah_marks` | array of object | Always present. |\n| `submitted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `waited_hours` | integer | May be omitted; may be null. |\n| `reviewed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `reviewed_by` | PersonRef | May be omitted. |\n| `grade` | string | May be omitted; may be null. |\n| `remark` | string | May be omitted; may be null. |\n| `notes` | array of TajweedNote | Always present. |\n| `model` | object | May be omitted. |\n| `pinned` | boolean | Always present. |\n| `delete_after` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `deleted_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `playable` | boolean | Always present. |\n| `plays` | integer | Always present. |\n| `from_app` | 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- 404 NOT_FOUND (missing, or not visible to this caller)\n- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "recitation_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Recitation"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "first_name": "string",
           "full_name": "string",
           "halaqa": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "label": "string"
           }
          },
          "kind": "student",
          "attempt": 1,
          "status": "uploading",
          "riwayah": "string",
          "surah": 1,
          "surah_name": "string",
          "ayah_from": 1,
          "ayah_to": 1,
          "juz": 1,
          "lesson": {
           "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "on": "2026-09-01",
           "part": "sabaq",
           "kind": "range",
           "surah": 1,
           "ayah_from": 1,
           "ayah_to": 1,
           "juz_from": 1,
           "juz_to": 1,
           "qaida_lesson": 1,
           "riwayah": "string",
           "result": "pending",
           "grade": "mumtaz",
           "remark": "string",
           "graded_at": "2026-09-22T09:30:00Z",
           "due_date": "2026-09-01",
           "assignment_status": "assigned"
          },
          "duration_seconds": 1,
          "content_type": "string",
          "ayah_marks": [
           {
            "ayah": 1,
            "at_seconds": 1.5
           }
          ],
          "submitted_at": "2026-09-22T09:30:00Z",
          "waited_hours": 1,
          "reviewed_at": "2026-09-22T09:30:00Z",
          "reviewed_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "grade": "string",
          "remark": "string",
          "notes": [
           {
            "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "recitation_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "surah": 1,
            "ayah": 1,
            "word_index": 1,
            "word": "string",
            "category": "makharij",
            "text": "string",
            "written_at": "2026-09-22T09:30:00Z",
            "written_by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "at_seconds": 1
           }
          ],
          "model": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "duration_seconds": 1,
           "status": "string"
          },
          "pinned": true,
          "delete_after": "2026-09-22",
          "deleted_at": "2026-09-22T09:30:00Z",
          "playable": true,
          "plays": 1,
          "from_app": "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"
     }
    }
   }
  },
  "/me/quran/recitations/{recitation_id}/play": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "Play a recitation (family)",
    "description": "A 10-minute streaming link for the child's recording or the teacher's model reading; the play is logged.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `recitation_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one PlayLink.\n\n| Field | Type | Notes |\n|---|---|---|\n| `url` | string | Always present; https URL; Streams the audio for 10 minutes; every play is logged. |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `content_type` | string | Always present; at most 20 characters. |\n| `duration_seconds` | 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "recitation_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/PlayLink"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "url": "https://erp.example.pk/samas/hook",
          "expires_at": "2026-09-22T09:30:00Z",
          "content_type": "string",
          "duration_seconds": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/quran/practice": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "A child's home practice",
    "description": "Each day's ayahs read in the app (exact) and minutes (estimated), with the link state; from and to default to the last four weeks.\n\n**Who can call:** signed-in person (Bearer session token).\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\n**Response 200:** `{ data, meta }` where `data` is one QuranPractice.\n\n| Field | Type | Notes |\n|---|---|---|\n| `from` | string | Always present; date YYYY-MM-DD. |\n| `to` | string | Always present; date YYYY-MM-DD. |\n| `link` | object | Always present. |\n| `days` | array of object | 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- 429 RATE_LIMITED (see Retry-After)",
    "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
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/QuranPractice"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "from": "2026-09-01",
          "to": "2026-09-01",
          "link": {},
          "days": [
           {
            "on": "2026-09-01",
            "ayahs": 1,
            "minutes_estimated": 1,
            "ranges": [
             {
              "surah": 1,
              "from": 1,
              "to": 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"
     }
    }
   }
  },
  "/me/students/{student_id}/quran/week": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "A child's Quran week",
    "description": "The Monday-to-Sunday week holding the date (default this week): parts passed and repeated, ajza tested, home practice, the latest recitation, ijazahs and the teacher's last remark. The family and the student. Never a fee.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | path | string | Required; UUID; at most 40 characters. |\n| `week` | query | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one QuranWeek.\n\n| Field | Type | Notes |\n|---|---|---|\n| `from` | string | Always present; date YYYY-MM-DD. |\n| `to` | string | Always present; date YYYY-MM-DD. |\n| `student` | object | Always present. |\n| `parts` | object | Always present. |\n| `sabaq` | array of QuranLessonPart | Always present. |\n| `juz_tested` | array of object | Always present. |\n| `juz_memorized` | integer | Always present. |\n| `practice` | object | Always present. |\n| `notes` | integer | Always present. |\n| `latest_recitation` | Recitation | May be omitted. |\n| `ijazahs` | array of Ijazah | Always present. |\n| `teacher_message` | 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- 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/student_id"
     },
     {
      "name": "week",
      "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": {
           "$ref": "#/components/schemas/QuranWeek"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "from": "2026-09-01",
          "to": "2026-09-01",
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "first_name": "string",
           "full_name": "string"
          },
          "parts": {
           "passed": 1,
           "repeat": 1,
           "absent": 1,
           "days": 1
          },
          "sabaq": [
           {
            "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "on": "2026-09-01",
            "part": "sabaq",
            "kind": "range",
            "surah": 1,
            "ayah_from": 1,
            "ayah_to": 1,
            "juz_from": 1,
            "juz_to": 1,
            "qaida_lesson": 1,
            "riwayah": "string",
            "result": "pending",
            "grade": "mumtaz",
            "remark": "string",
            "graded_at": "2026-09-22T09:30:00Z",
            "due_date": "2026-09-01",
            "assignment_status": "assigned"
           }
          ],
          "juz_tested": [
           {
            "juz": 1,
            "grade": "string",
            "on": "2026-09-01"
           }
          ],
          "juz_memorized": 1,
          "practice": {
           "ayahs": 1,
           "minutes_estimated": 1,
           "days": 1
          },
          "notes": 1,
          "latest_recitation": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "student": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "first_name": "string",
            "full_name": "string",
            "halaqa": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "label": "string"
            }
           },
           "kind": "student",
           "attempt": 1,
           "status": "uploading",
           "riwayah": "string",
           "surah": 1,
           "surah_name": "string",
           "ayah_from": 1,
           "ayah_to": 1,
           "juz": 1,
           "lesson": {
            "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "on": "2026-09-01",
            "part": "sabaq",
            "kind": "range",
            "surah": 1,
            "ayah_from": 1,
            "ayah_to": 1,
            "juz_from": 1,
            "juz_to": 1,
            "qaida_lesson": 1,
            "riwayah": "string",
            "result": "pending",
            "grade": "mumtaz",
            "remark": "string",
            "graded_at": "2026-09-22T09:30:00Z",
            "due_date": "2026-09-01",
            "assignment_status": "assigned"
           },
           "duration_seconds": 1,
           "content_type": "string",
           "ayah_marks": [
            {
             "ayah": 1,
             "at_seconds": 1.5
            }
           ],
           "submitted_at": "2026-09-22T09:30:00Z",
           "waited_hours": 1,
           "reviewed_at": "2026-09-22T09:30:00Z",
           "reviewed_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "grade": "string",
           "remark": "string",
           "notes": [
            {
             "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "lesson_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "recitation_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "surah": 1,
             "ayah": 1,
             "word_index": 1,
             "word": "string",
             "category": "makharij",
             "text": "string",
             "written_at": "2026-09-22T09:30:00Z",
             "written_by": {
              "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "full_name": "string"
             },
             "at_seconds": 1
            }
           ],
           "model": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "duration_seconds": 1,
            "status": "string"
           },
           "pinned": true,
           "delete_after": "2026-09-22",
           "deleted_at": "2026-09-22T09:30:00Z",
           "playable": true,
           "plays": 1,
           "from_app": "string"
          },
          "ijazahs": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "number": "string",
            "student": {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string",
             "first_name": "string",
             "admission_no": "string"
            },
            "riwayah": "string",
            "riwayah_name": "string",
            "teacher": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "granted_on": "2026-09-01",
            "sanad_text": "string",
            "witness": "string",
            "annulled_at": "2026-09-22T09:30:00Z",
            "annul_reason": "string",
            "annulled_by": {
             "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "full_name": "string"
            },
            "school": {
             "name": "string"
            }
           }
          ],
          "teacher_message": "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"
     }
    }
   }
  },
  "/me/students/{student_id}/quran/ijazahs": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "A child's ijazahs",
    "description": "Each with the teacher's sanad and the number; an annulled one carries its reason.\n\n**Who can call:** signed-in person (Bearer session token).\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 Ijazah.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `number` | string | Always present; at most 20 characters; IJZ-<year>-<nnnn>, per school. |\n| `student` | object | Always present. |\n| `riwayah` | string | Always present; at most 30 characters. |\n| `riwayah_name` | string | May be omitted; may be null. |\n| `teacher` | NamedPerson | May be omitted. |\n| `granted_on` | string | Always present; date YYYY-MM-DD. |\n| `sanad_text` | string | Always present; at most 4000 characters; The teacher's sanad as it stood on the day; never changes. |\n| `witness` | string | May be omitted; may be null. |\n| `annulled_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `annul_reason` | string | May be omitted; may be null. |\n| `annulled_by` | NamedPerson | May be omitted. |\n| `school` | 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"
     },
     {
      "$ref": "#/components/parameters/student_id"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Ijazah"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "number": "string",
           "student": {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string",
            "first_name": "string",
            "admission_no": "string"
           },
           "riwayah": "string",
           "riwayah_name": "string",
           "teacher": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "granted_on": "2026-09-01",
           "sanad_text": "string",
           "witness": "string",
           "annulled_at": "2026-09-22T09:30:00Z",
           "annul_reason": "string",
           "annulled_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "school": {
            "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"
     }
    }
   }
  },
  "/me/quran/certificates/{kind}/{id}": {
   "get": {
    "tags": [
     "Quran"
    ],
    "summary": "Certificate data (family)",
    "description": "What the printed certificate shows: an ijazah or a tested juz of the caller's child.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `kind` | path | string | Required; one of `ijazah`, `juz`. |\n| `id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one QuranCertificate.\n\n| Field | Type | Notes |\n|---|---|---|\n| `kind` | string | Always present; one of `ijazah`, `juz`. |\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": "kind",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "enum": [
        "ijazah",
        "juz"
       ]
      }
     },
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/QuranCertificate"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "kind": "ijazah"
         },
         "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"
     }
    }
   }
  },
  "/me/learn/home": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "My courses",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. The signed-in scholar's courses with days of notes, the last note, the dates with notes in the last seven days and each course's lecture slots; today in the school's time zone; whether the school's admins can see notes (L1, L3, L54). One call for the whole page. The scholar herself; a school admin of her school uses the Admin API's /admin/learn operations. Nobody else sees a scholar's notes.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one LearnHome.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student` | object | Always present. |\n| `today` | string | Always present; date YYYY-MM-DD; Today in the school's time zone. |\n| `weekday` | integer | Always present. |\n| `timezone` | string | May be omitted; at most 60 characters. |\n| `staff_access` | boolean | Always present; Whether the school's admins (the incharges) can see notes (L54). |\n| `courses` | array of LearnCourse | 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/LearnHome"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "student": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "admission_no": "string"
          },
          "today": "2026-09-01",
          "weekday": 1,
          "timezone": "string",
          "staff_access": true,
          "courses": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "title": "string",
            "code": "string",
            "instructor": "string",
            "term": "string",
            "language": "english",
            "status": "active",
            "subject": {
             "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "name": "string"
            },
            "days_of_notes": 1,
            "last_note_date": "2026-09-22",
            "recent_dates": [
             "2026-09-01"
            ],
            "slots": [
             {
              "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
              "weekday": 1,
              "start": "12:30",
              "end": "14:30",
              "kind": "lecture",
              "room": "string",
              "instructor": "string"
             }
            ],
            "created_at": "2026-09-22T09:30:00Z",
            "updated_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"
     }
    }
   }
  },
  "/me/learn/courses": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "List my courses",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. The same courses as My courses, active first.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of LearnCourse.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `title` | string | Always present; at most 120 characters. |\n| `code` | string | May be omitted; may be null; at most 20 characters. |\n| `instructor` | string | May be omitted; may be null; at most 80 characters. |\n| `term` | string | May be omitted; may be null; at most 40 characters. |\n| `language` | string | Always present; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Always present; one of `active`, `finished`, `archived`. |\n| `subject` | object | May be omitted. |\n| `days_of_notes` | integer | Always present; Days with at least one picture. |\n| `last_note_date` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `recent_dates` | array of string | May be omitted. |\n| `slots` | array of LearnSlot | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_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- 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/LearnCourse"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "code": "string",
           "instructor": "string",
           "term": "string",
           "language": "english",
           "status": "active",
           "subject": {
            "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string"
           },
           "days_of_notes": 1,
           "last_note_date": "2026-09-22",
           "recent_dates": [
            "2026-09-01"
           ],
           "slots": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "weekday": 1,
             "start": "12:30",
             "end": "14:30",
             "kind": "lecture",
             "room": "string",
             "instructor": "string"
            }
           ],
           "created_at": "2026-09-22T09:30:00Z",
           "updated_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"
     }
    }
   },
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Add a course",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Title 2 to 120 characters (LEARN_COURSE_TITLE), code up to 20 (LEARN_COURSE_CODE_LONG); DUPLICATE_ITEM for a second course with the same title in the same term. section_subject_id links the course to a gradebook subject of her own section (L53).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `title` | string | Required; 2 to 120 characters. |\n| `code` | string | Optional; may be null; at most 20 characters. |\n| `instructor` | string | Optional; may be null; at most 80 characters. |\n| `term` | string | Optional; may be null; at most 40 characters. |\n| `language` | string | Optional; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Optional; one of `active`, `finished`, `archived`. |\n| `section_subject_id` | string | Optional; may be null; UUID; at most 40 characters; A subject of the scholar's own section (L53); NOT_FOUND otherwise. |\n\n**Response 201:** `{ data, meta }` where `data` is one LearnCourse.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `title` | string | Always present; at most 120 characters. |\n| `code` | string | May be omitted; may be null; at most 20 characters. |\n| `instructor` | string | May be omitted; may be null; at most 80 characters. |\n| `term` | string | May be omitted; may be null; at most 40 characters. |\n| `language` | string | Always present; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Always present; one of `active`, `finished`, `archived`. |\n| `subject` | object | May be omitted. |\n| `days_of_notes` | integer | Always present; Days with at least one picture. |\n| `last_note_date` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `recent_dates` | array of string | May be omitted. |\n| `slots` | array of LearnSlot | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_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- 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/LearnCourseCreate"
       },
       "example": {
        "title": "string",
        "code": "string",
        "instructor": "string",
        "term": "string",
        "language": "english",
        "status": "active",
        "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnCourse"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "code": "string",
          "instructor": "string",
          "term": "string",
          "language": "english",
          "status": "active",
          "subject": {
           "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "days_of_notes": 1,
          "last_note_date": "2026-09-22",
          "recent_dates": [
           "2026-09-01"
          ],
          "slots": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "weekday": 1,
            "start": "12:30",
            "end": "14:30",
            "kind": "lecture",
            "room": "string",
            "instructor": "string"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_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"
     }
    }
   }
  },
  "/me/learn/courses/{course_id}": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "One of my courses",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnCourse.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `title` | string | Always present; at most 120 characters. |\n| `code` | string | May be omitted; may be null; at most 20 characters. |\n| `instructor` | string | May be omitted; may be null; at most 80 characters. |\n| `term` | string | May be omitted; may be null; at most 40 characters. |\n| `language` | string | Always present; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Always present; one of `active`, `finished`, `archived`. |\n| `subject` | object | May be omitted. |\n| `days_of_notes` | integer | Always present; Days with at least one picture. |\n| `last_note_date` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `recent_dates` | array of string | May be omitted. |\n| `slots` | array of LearnSlot | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnCourse"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "code": "string",
          "instructor": "string",
          "term": "string",
          "language": "english",
          "status": "active",
          "subject": {
           "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "days_of_notes": 1,
          "last_note_date": "2026-09-22",
          "recent_dates": [
           "2026-09-01"
          ],
          "slots": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "weekday": 1,
            "start": "12:30",
            "end": "14:30",
            "kind": "lecture",
            "room": "string",
            "instructor": "string"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_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"
     }
    }
   },
   "patch": {
    "tags": [
     "Learn"
    ],
    "summary": "Change a course",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Rename it, finish it or archive it (archived keeps everything).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_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| `title` | string | Optional; 2 to 120 characters. |\n| `code` | string | Optional; may be null; at most 20 characters. |\n| `instructor` | string | Optional; may be null; at most 80 characters. |\n| `term` | string | Optional; may be null; at most 40 characters. |\n| `language` | string | Optional; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Optional; one of `active`, `finished`, `archived`. |\n| `section_subject_id` | string | Optional; may be null; UUID; at most 40 characters; A subject of the scholar's own section (L53); NOT_FOUND otherwise. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnCourse.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `title` | string | Always present; at most 120 characters. |\n| `code` | string | May be omitted; may be null; at most 20 characters. |\n| `instructor` | string | May be omitted; may be null; at most 80 characters. |\n| `term` | string | May be omitted; may be null; at most 40 characters. |\n| `language` | string | Always present; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Always present; one of `active`, `finished`, `archived`. |\n| `subject` | object | May be omitted. |\n| `days_of_notes` | integer | Always present; Days with at least one picture. |\n| `last_note_date` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `recent_dates` | array of string | May be omitted. |\n| `slots` | array of LearnSlot | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_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- 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"
     },
     {
      "name": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LearnCourseWrite"
       },
       "example": {
        "title": "string",
        "code": "string",
        "instructor": "string",
        "term": "string",
        "language": "english",
        "status": "active",
        "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnCourse"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "code": "string",
          "instructor": "string",
          "term": "string",
          "language": "english",
          "status": "active",
          "subject": {
           "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "days_of_notes": 1,
          "last_note_date": "2026-09-22",
          "recent_dates": [
           "2026-09-01"
          ],
          "slots": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "weekday": 1,
            "start": "12:30",
            "end": "14:30",
            "kind": "lecture",
            "room": "string",
            "instructor": "string"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_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"
     }
    }
   }
  },
  "/me/learn/courses/{course_id}/where": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "Where a course link leads",
    "description": "Whose course this is and how the caller may open it, in whichever school it is (L107): as the scholar herself, or as her school's admin or a super admin (her Learn record on the admin side). The evening email's links open the scholar's own notes; a page opened by anyone else asks this first, moves into school_id and opens the right view. Anyone else, and a course that does not exist, get NOT_FOUND. Does not need a working school. Never while viewing as someone, except for the scholar's own course.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnCourseWhere.\n\n| Field | Type | Notes |\n|---|---|---|\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `school_id` | string | Always present; UUID. |\n| `as` | string | Always present; one of `student`, `incharge`; 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). |\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": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnCourseWhere"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "school_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "as": "student"
         },
         "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-signed-in-only": true
   }
  },
  "/me/learn/courses/{course_id}/notes": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "A course's days of notes",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Newest first: each day's date, whether it has a summary and how many pictures (the day strip and the timeline, L5). from and to are optional.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_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\n**Response 200:** `{ data, meta }` where `data` is a list of LearnNoteDay.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary_set` | boolean | May be omitted. |\n| `pictures` | 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "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/LearnNoteDay"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "note_date": "2026-09-01",
           "summary_set": true,
           "pictures": 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"
     }
    }
   }
  },
  "/me/learn/courses/{course_id}/slots": {
   "put": {
    "tags": [
     "Learn"
    ],
    "summary": "Set a course's lectures",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Replaces the course's lecture and lab slots (at most 20, TOO_MANY): weekday 1 to 7, start and end as HH:MM in the school's time zone (L8).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_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| `slots` | array of LearnSlotInput | Required; at most 500 items. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnCourse.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `title` | string | Always present; at most 120 characters. |\n| `code` | string | May be omitted; may be null; at most 20 characters. |\n| `instructor` | string | May be omitted; may be null; at most 80 characters. |\n| `term` | string | May be omitted; may be null; at most 40 characters. |\n| `language` | string | Always present; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Always present; one of `active`, `finished`, `archived`. |\n| `subject` | object | May be omitted. |\n| `days_of_notes` | integer | Always present; Days with at least one picture. |\n| `last_note_date` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `recent_dates` | array of string | May be omitted. |\n| `slots` | array of LearnSlot | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_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- 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"
     },
     {
      "name": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "slots": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/LearnSlotInput"
          },
          "maxItems": 500
         }
        },
        "required": [
         "slots"
        ],
        "additionalProperties": false
       },
       "example": {
        "slots": [
         {
          "weekday": 1,
          "start": "string",
          "end": "string",
          "kind": "lecture",
          "room": "string",
          "instructor": "string"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnCourse"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "code": "string",
          "instructor": "string",
          "term": "string",
          "language": "english",
          "status": "active",
          "subject": {
           "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "days_of_notes": 1,
          "last_note_date": "2026-09-22",
          "recent_dates": [
           "2026-09-01"
          ],
          "slots": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "weekday": 1,
            "start": "12:30",
            "end": "14:30",
            "kind": "lecture",
            "room": "string",
            "instructor": "string"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_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"
     }
    }
   }
  },
  "/me/learn/week": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "My week",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Every active course's slots, Monday first, with today (L8).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one LearnWeek.\n\n| Field | Type | Notes |\n|---|---|---|\n| `today` | string | Always present; date YYYY-MM-DD. |\n| `weekday` | integer | Always present. |\n| `slots` | array of object | 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/LearnWeek"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "today": "2026-09-01",
          "weekday": 1,
          "slots": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "weekday": 1,
            "start": "12:30",
            "end": "14:30",
            "kind": "lecture",
            "room": "string",
            "instructor": "string",
            "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "course_title": "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"
     }
    }
   }
  },
  "/me/learn/notes": {
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Open a day of notes",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Opens (creates) the note for a course and a day; note_date defaults to today in the school's time zone and cannot be in the future (OUT_OF_RANGE). One note per course per day (L4).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `course_id` | string | Required; UUID; at most 40 characters. |\n| `note_date` | string | Optional; date YYYY-MM-DD; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | 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": {
         "course_id": {
          "type": "string",
          "format": "uuid",
          "maxLength": 40
         },
         "note_date": {
          "type": "string",
          "format": "date",
          "example": "2026-09-01",
          "maxLength": 40
         }
        },
        "required": [
         "course_id"
        ],
        "additionalProperties": false
       },
       "example": {
        "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "note_date": "2026-09-01"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "A day of notes",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. The note with its pictures, each with a link valid 10 minutes.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | 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": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 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"
     }
    }
   },
   "patch": {
    "tags": [
     "Learn"
    ],
    "summary": "Write the day's line",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. One line about the day, up to 200 characters.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_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| `summary` | string | Required; may be null; at most 200 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | 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"
     },
     {
      "name": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "summary": {
          "type": [
           "string",
           "null"
          ],
          "maxLength": 200
         }
        },
        "required": [
         "summary"
        ],
        "additionalProperties": false
       },
       "example": {
        "summary": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/upload-url": {
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Get an upload URL for a picture",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. A one-shot link valid 15 minutes. PUT the file with its Content-Type, then add it with the upload_id. JPEG, PNG or WebP up to 20 MB, a PDF up to 10 MB. Nothing leaves the school (L7).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_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| `file_name` | string | Required; 1 to 200 characters. |\n| `content_type` | string | Required; one of `image/jpeg`, `image/png`, `image/webp`, `application/pdf`, `application/zip`; application/zip only for a guide's ZIP (kind guide, role bundle_zip). |\n| `size_bytes` | integer | Required; from 1 to 20971520; 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. |\n| `kind` | string | Optional; one of `note`, `guide`; guide: a study guide's file (L55), uploaded by the incharge only; it never counts towards the 12 pictures a day. Default note. |\n| `role` | string | Optional; one of `page`, `thumbnail`, `thumbnail_wide`, `bundle_pdf`, `bundle_zip`; For kind guide: page, thumbnail (1080 square), thumbnail_wide (1200 by 630), bundle_pdf or bundle_zip. |\n| `ordinal` | integer | Optional; from 1 to 40; For kind guide: the page number; 1 for the other roles. |\n| `width` | integer | Optional; from 1 to 4000. |\n| `height` | integer | Optional; from 1 to 4000. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnUpload.\n\n| Field | Type | Notes |\n|---|---|---|\n| `upload_id` | string | Always present; UUID. |\n| `url` | string | Always present; https URL; at most 2000 characters. |\n| `expires_at` | string | Always present; 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LearnUploadRequest"
       },
       "example": {
        "file_name": "string",
        "content_type": "image/jpeg",
        "size_bytes": 1,
        "kind": "note",
        "role": "page",
        "ordinal": 1,
        "width": 1,
        "height": 1
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnUpload"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/pictures": {
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Add a picture to the day",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Adds the uploaded file. Up to 12 pictures a day (LEARN_TOO_MANY_PICTURES).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_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| `upload_id` | string | Required; UUID; at most 40 characters. |\n| `label` | string | Optional; may be null; at most 60 characters. |\n\n**Response 201:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | May be omitted. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | 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"
     },
     {
      "name": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LearnPictureAdd"
       },
       "example": {
        "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "label": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 1
          }
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "200": {
      "description": "Already on this day (the same picture): nothing added; duplicate_of names it (L85).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/pictures/{picture_id}": {
   "delete": {
    "tags": [
     "Learn"
    ],
    "summary": "Remove a picture",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Only the scholar removes a picture (L6).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_id` | path | string | Required; UUID; at most 40 characters. |\n| `picture_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | 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"
     },
     {
      "name": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "picture_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/guide": {
   "patch": {
    "tags": [
     "Learn"
    ],
    "summary": "Hide or show my study guide",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Hides the note's study guide from her own note page, or shows it again (L60). Only her view changes; nothing is written to History and her share link keeps working. Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_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| `hidden` | boolean | Required. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | 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"
     },
     {
      "name": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "hidden": {
          "type": "boolean"
         }
        },
        "required": [
         "hidden"
        ],
        "additionalProperties": false
       },
       "example": {
        "hidden": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 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"
     }
    }
   }
  },
  "/me/learn/first-run": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "My welcome screens",
    "description": "Whether the three welcome screens are due (HM27): true for a scholar at a school with Learn who has not seen them, existing scholars included, once. Always false for a parent. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one FirstRun.\n\n| Field | Type | Notes |\n|---|---|---|\n| `show` | boolean | Always present. |\n| `seen_at` | string | Always present; 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": {
           "$ref": "#/components/schemas/FirstRun"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "show": true,
          "seen_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"
     }
    }
   },
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "I have seen the welcome",
    "description": "Marks the three screens seen (on Skip or at the end). A History line. Only the scholar. The public site, school requests, onboarding and the first run: docs/api/public-site.md (PRD 5.16).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one FirstRun.\n\n| Field | Type | Notes |\n|---|---|---|\n| `show` | boolean | Always present. |\n| `seen_at` | string | Always present; may be null; 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- 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"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/FirstRun"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "show": true,
          "seen_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"
     }
    }
   }
  },
  "/me/learn/settings": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "My Learn settings",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Whether her first name shows on shared study guides (L66). Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one LearnScholarSettings.\n\n| Field | Type | Notes |\n|---|---|---|\n| `share_name_publicly` | boolean | Always present; Her first name on shared study guides (L66). Off: \"A saMas scholar\". |\n| `changed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `changed_by` | object | May be omitted. |\n| `changed_by_kind` | string | May be omitted; may be null; one of `student`, `incharge`, `None`. |\n| `reason` | string | May be omitted; may be null; at most 300 characters. |\n| `daily_email` | boolean | Always present; Email me my notes each evening (L77). On by default. |\n| `daily_email_changed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `daily_email_changed_by_kind` | string | May be omitted; may be null; one of `student`, `incharge`, `None`. |\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/LearnScholarSettings"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "share_name_publicly": true,
          "changed_at": "2026-09-22T09:30:00Z",
          "changed_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "changed_by_kind": "student",
          "reason": "string",
          "daily_email": true,
          "daily_email_changed_at": "2026-09-22T09:30:00Z",
          "daily_email_changed_by_kind": "student"
         },
         "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"
     }
    }
   },
   "patch": {
    "tags": [
     "Learn"
    ],
    "summary": "Change my Learn settings",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. share_name_publicly: her first name on shared study guides instead of \"A saMas scholar\" (L66). daily_email: the evening email of her notes (L77). Send one or both (MISSING_FIELD with neither). Each change is written to her History. Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `share_name_publicly` | boolean | Optional. |\n| `daily_email` | boolean | Optional. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnScholarSettings.\n\n| Field | Type | Notes |\n|---|---|---|\n| `share_name_publicly` | boolean | Always present; Her first name on shared study guides (L66). Off: \"A saMas scholar\". |\n| `changed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `changed_by` | object | May be omitted. |\n| `changed_by_kind` | string | May be omitted; may be null; one of `student`, `incharge`, `None`. |\n| `reason` | string | May be omitted; may be null; at most 300 characters. |\n| `daily_email` | boolean | Always present; Email me my notes each evening (L77). On by default. |\n| `daily_email_changed_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `daily_email_changed_by_kind` | string | May be omitted; may be null; one of `student`, `incharge`, `None`. |\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": {
         "share_name_publicly": {
          "type": "boolean"
         },
         "daily_email": {
          "type": "boolean"
         }
        },
        "additionalProperties": false
       },
       "example": {
        "share_name_publicly": true,
        "daily_email": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnScholarSettings"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "share_name_publicly": true,
          "changed_at": "2026-09-22T09:30:00Z",
          "changed_by": {
           "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string"
          },
          "changed_by_kind": "student",
          "reason": "string",
          "daily_email": true,
          "daily_email_changed_at": "2026-09-22T09:30:00Z",
          "daily_email_changed_by_kind": "student"
         },
         "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"
     }
    }
   }
  },
  "/me/learn/courses/{course_id}/materials": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "A course's materials",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Files from her books and classes (L88): PDF, PowerPoint or pictures, oldest first, each with a 10-minute download link. Her coordinator sees them too (L90).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of LearnMaterial.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `title` | string | Always present; may be null; at most 120 characters. |\n| `name` | string | May be omitted; may be null; at most 200 characters. |\n| `content_type` | string | Always present; one of `application/pdf`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`, `application/vnd.ms-powerpoint`, `image/jpeg`, `image/png`, `image/webp`; A PDF, PowerPoint or picture (L88). |\n| `size_bytes` | integer | Always present; from 1 to 20971520. |\n| `sha256` | string | May be omitted; may be null; pattern `^[0-9a-f]{64}$`. |\n| `added_by_kind` | string | Always present; one of `student`, `incharge`, `coordinator`; Who added it: her, her incharge or her coordinator. |\n| `mine` | boolean | May be omitted; The caller added it (a coordinator or incharge removes only these, L90). |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `url` | string | May be omitted; may be null; https URL; at most 2000 characters; A 10-minute download link. |\n| `duplicate_of` | string | May be omitted; UUID; On add: the same file is already in the course; nothing was added (L89). |\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": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/LearnMaterial"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "title": "string",
           "name": "string",
           "content_type": "application/pdf",
           "size_bytes": 1,
           "sha256": "string",
           "added_by_kind": "student",
           "mine": true,
           "created_at": "2026-09-22T09:30:00Z",
           "url": "https://erp.example.pk/samas/hook",
           "duplicate_of": "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"
     }
    }
   },
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Add a material",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Adds the uploaded file with a title. The same file already in the course answers 200 with duplicate_of and adds nothing (L89). Up to 40 a course (TOO_MANY). A History line.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_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| `upload_id` | string | Required; UUID; at most 40 characters. |\n| `title` | string | Optional; may be null; at most 120 characters; Defaults to the file name without its extension. |\n\n**Response 201:** `{ data, meta }` where `data` is one LearnMaterial.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `title` | string | Always present; may be null; at most 120 characters. |\n| `name` | string | May be omitted; may be null; at most 200 characters. |\n| `content_type` | string | Always present; one of `application/pdf`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`, `application/vnd.ms-powerpoint`, `image/jpeg`, `image/png`, `image/webp`; A PDF, PowerPoint or picture (L88). |\n| `size_bytes` | integer | Always present; from 1 to 20971520. |\n| `sha256` | string | May be omitted; may be null; pattern `^[0-9a-f]{64}$`. |\n| `added_by_kind` | string | Always present; one of `student`, `incharge`, `coordinator`; Who added it: her, her incharge or her coordinator. |\n| `mine` | boolean | May be omitted; The caller added it (a coordinator or incharge removes only these, L90). |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `url` | string | May be omitted; may be null; https URL; at most 2000 characters; A 10-minute download link. |\n| `duplicate_of` | string | May be omitted; UUID; On add: the same file is already in the course; nothing was added (L89). |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnMaterial.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `title` | string | Always present; may be null; at most 120 characters. |\n| `name` | string | May be omitted; may be null; at most 200 characters. |\n| `content_type` | string | Always present; one of `application/pdf`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`, `application/vnd.ms-powerpoint`, `image/jpeg`, `image/png`, `image/webp`; A PDF, PowerPoint or picture (L88). |\n| `size_bytes` | integer | Always present; from 1 to 20971520. |\n| `sha256` | string | May be omitted; may be null; pattern `^[0-9a-f]{64}$`. |\n| `added_by_kind` | string | Always present; one of `student`, `incharge`, `coordinator`; Who added it: her, her incharge or her coordinator. |\n| `mine` | boolean | May be omitted; The caller added it (a coordinator or incharge removes only these, L90). |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `url` | string | May be omitted; may be null; https URL; at most 2000 characters; A 10-minute download link. |\n| `duplicate_of` | string | May be omitted; UUID; On add: the same file is already in the course; nothing was added (L89). |\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"
     },
     {
      "name": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LearnMaterialAdd"
       },
       "example": {
        "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "title": "string"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnMaterial"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "name": "string",
          "content_type": "application/pdf",
          "size_bytes": 1,
          "sha256": "string",
          "added_by_kind": "student",
          "mine": true,
          "created_at": "2026-09-22T09:30:00Z",
          "url": "https://erp.example.pk/samas/hook",
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
         },
         "meta": {
          "request_id": "req_01J8Z3X4",
          "generated_at": "2026-09-22T09:30:00Z"
         }
        }
       }
      }
     },
     "200": {
      "description": "Already in this course: nothing added.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnMaterial"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "name": "string",
          "content_type": "application/pdf",
          "size_bytes": 1,
          "sha256": "string",
          "added_by_kind": "student",
          "mine": true,
          "created_at": "2026-09-22T09:30:00Z",
          "url": "https://erp.example.pk/samas/hook",
          "duplicate_of": "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"
     }
    }
   }
  },
  "/me/learn/courses/{course_id}/materials/upload-url": {
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Get an upload URL for a material",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. A one-shot link valid 15 minutes, up to 20 MB: PUT the file with its Content-Type, then add it with the upload_id (L88).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_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| `file_name` | string | Required; 1 to 200 characters. |\n| `content_type` | string | Required; one of `application/pdf`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`, `application/vnd.ms-powerpoint`, `image/jpeg`, `image/png`, `image/webp`. |\n| `size_bytes` | integer | Required; from 1 to 20971520. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnUpload.\n\n| Field | Type | Notes |\n|---|---|---|\n| `upload_id` | string | Always present; UUID. |\n| `url` | string | Always present; https URL; at most 2000 characters. |\n| `expires_at` | string | Always present; 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LearnMaterialUploadRequest"
       },
       "example": {
        "file_name": "string",
        "content_type": "application/pdf",
        "size_bytes": 1
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnUpload"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "upload_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "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"
     }
    }
   }
  },
  "/me/learn/courses/{course_id}/materials/{material_id}": {
   "delete": {
    "tags": [
     "Learn"
    ],
    "summary": "Remove a material",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. She removes any; her coordinator only what she added (L90). A History line.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `course_id` | path | string | Required; UUID; at most 40 characters. |\n| `material_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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "course_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "material_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "204": {
      "description": "Removed."
     },
     "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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/guide-versions": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "My study guide's versions",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Every version of her guide, newest first, with what changed and why (L99, L102). Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of LearnGuideVersion.\n\n| Field | Type | Notes |\n|---|---|---|\n| `number` | integer | Always present; from 1 to any. |\n| `current` | boolean | Always present. |\n| `change_kind` | string | Always present; one of `first`, `fix`, `improve`, `expand`, `restore`, `earlier`; earlier: a version from before versions were kept; only its pictures were kept. |\n| `ask` | string | May be omitted; may be null; at most 1000 characters. |\n| `ask_section` | integer | May be omitted; may be null. |\n| `asked_by_kind` | string | May be omitted; may be null; one of `incharge`, `student`, `None`. |\n| `asked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `change_note` | string | May be omitted; may be null; at most 500 characters. |\n| `restored_from` | integer | May be omitted; may be null. |\n| `text_kept` | boolean | Always present. |\n| `title` | string | May be omitted; may be null; at most 120 characters. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `pages` | integer | May be omitted; from 0 to any. |\n| `rating` | integer | May be omitted; may be null; from 1 to 5; Her stars for this version (L104). |\n| `guide` | object | May be omitted. |\n| `page_files` | array of LearnGuideFile | May be omitted. |\n| `files` | array of LearnGuideFile | May be omitted. |\n| `rating_suggestion` | string | May be omitted; may be null; at most 1000 characters. |\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": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/LearnGuideVersion"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "number": 1,
           "current": true,
           "change_kind": "first",
           "ask": "string",
           "ask_section": 1,
           "asked_by_kind": "incharge",
           "asked_at": "2026-09-22T09:30:00Z",
           "change_note": "string",
           "restored_from": 1,
           "text_kept": true,
           "title": "string",
           "created_at": "2026-09-22T09:30:00Z",
           "pages": 1,
           "rating": 1,
           "guide": {
            "title": "string",
            "big_idea": "string",
            "quote": {
             "text": "string",
             "by": "string"
            },
            "subject": "string",
            "sections": [
             {
              "heading": "string",
              "emoji": "string",
              "hook": "string",
              "lines": [],
              "spot": "string"
             }
            ],
            "formulas": [
             {
              "label": "string",
              "expr": "string",
              "note": "string",
              "section": 1
             }
            ],
            "tables": [
             {
              "title": "string",
              "columns": [],
              "rows": [],
              "section": 1
             }
            ],
            "checks": [
             {
              "says": "string",
              "correct": "string",
              "where": "string",
              "section": 1
             }
            ],
            "practice": [
             "string"
            ],
            "style": "poster-v2"
           },
           "page_files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "rating_suggestion": "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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/guide-versions/{number}": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "One version of my study guide",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Its text and its own pages, as it was (L102). Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_id` | path | string | Required; UUID; at most 40 characters. |\n| `number` | path | integer | Required; from 1 to 200. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnGuideVersion.\n\n| Field | Type | Notes |\n|---|---|---|\n| `number` | integer | Always present; from 1 to any. |\n| `current` | boolean | Always present. |\n| `change_kind` | string | Always present; one of `first`, `fix`, `improve`, `expand`, `restore`, `earlier`; earlier: a version from before versions were kept; only its pictures were kept. |\n| `ask` | string | May be omitted; may be null; at most 1000 characters. |\n| `ask_section` | integer | May be omitted; may be null. |\n| `asked_by_kind` | string | May be omitted; may be null; one of `incharge`, `student`, `None`. |\n| `asked_at` | string | May be omitted; may be null; timestamp with offset or Z (RFC 3339). |\n| `change_note` | string | May be omitted; may be null; at most 500 characters. |\n| `restored_from` | integer | May be omitted; may be null. |\n| `text_kept` | boolean | Always present. |\n| `title` | string | May be omitted; may be null; at most 120 characters. |\n| `created_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `pages` | integer | May be omitted; from 0 to any. |\n| `rating` | integer | May be omitted; may be null; from 1 to 5; Her stars for this version (L104). |\n| `guide` | object | May be omitted. |\n| `page_files` | array of LearnGuideFile | May be omitted. |\n| `files` | array of LearnGuideFile | May be omitted. |\n| `rating_suggestion` | string | May be omitted; may be null; at most 1000 characters. |\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": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     },
     {
      "name": "number",
      "in": "path",
      "required": true,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 200
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnGuideVersion"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "number": 1,
          "current": true,
          "change_kind": "first",
          "ask": "string",
          "ask_section": 1,
          "asked_by_kind": "incharge",
          "asked_at": "2026-09-22T09:30:00Z",
          "change_note": "string",
          "restored_from": 1,
          "text_kept": true,
          "title": "string",
          "created_at": "2026-09-22T09:30:00Z",
          "pages": 1,
          "rating": 1,
          "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2"
          },
          "page_files": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "role": "page",
            "ordinal": 1,
            "content_type": "string",
            "size_bytes": 1,
            "width": 1,
            "height": 1,
            "created_at": "2026-09-22T09:30:00Z",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "files": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "role": "page",
            "ordinal": 1,
            "content_type": "string",
            "size_bytes": 1,
            "width": 1,
            "height": 1,
            "created_at": "2026-09-22T09:30:00Z",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "rating_suggestion": "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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/guide-ask": {
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Ask for a new version of my study guide",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Fix a mistake, improve it, or go deeper on an idea (L101). Goes to the guide run like her incharge's Make it again, usually within the hour. kind is required; fix needs what; expand needs what or section. At most 2 asks a day (LEARN_ASK_LIMIT). NOT_FOUND on a day with no guide yet. A History line. Study guides: docs/api/learn-guides.md (PRD 5.11.10).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_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| `kind` | string | Optional; one of `fix`, `improve`, `expand`; fix: a mistake to put right (say what); improve: make it better; expand: go deeper on one idea (pick the section or say which). |\n| `what` | string | Optional; may be null; at most 1000 characters; What to change, in her or your words. |\n| `section` | integer | Optional; may be null; from 1 to 12; The section to go deeper on (its number in the guide). |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnNote.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `course_id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `note_date` | string | Always present; date YYYY-MM-DD. |\n| `summary` | string | May be omitted; may be null; at most 200 characters. |\n| `status` | string | Always present; one of `draft`, `queued`, `reading`, `extracted`, `reviewed`, `drafted`, `published`. |\n| `pictures` | array of LearnPicture | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `guide` | LearnGuide | May be omitted. |\n| `share_link` | LearnShareLink | May be omitted. |\n| `guide_source_hash` | string | May be omitted; at most 64 characters; Fingerprint of the note's own pictures now (SHA-256 hex); a guide made from other pictures is stale (L58). |\n| `picture_count` | integer | May be omitted; How many pictures the day holds (at most 12). A coordinator's pictures list shows only hers; this counts all (L83). |\n| `fingerprints` | array of string | May be omitted; The SHA-256 of every picture on the day, so a page can skip one already there before uploading (L85). |\n| `duplicate_of` | string | May be omitted; may be null; UUID; 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. |\n| `guide_request` | LearnGuideRequest | 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"
     },
     {
      "name": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LearnGuideAsk"
       },
       "example": {
        "kind": "fix",
        "what": "string",
        "section": 1
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnNote"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_date": "2026-09-01",
          "summary": "string",
          "status": "draft",
          "pictures": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "name": "string",
            "content_type": "image/jpeg",
            "size_bytes": 1,
            "position": 1,
            "label": "string",
            "added_by_kind": "student",
            "created_at": "2026-09-22T09:30:00Z",
            "sha256": "string",
            "url": "https://erp.example.pk/samas/hook"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "guide": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "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
            }
           ],
           "checks": [
            {
             "says": "string",
             "correct": "string",
             "where": "string",
             "section": 1
            }
           ],
           "practice": [
            "string"
           ],
           "style": "poster-v2",
           "show_checks_publicly": true,
           "hidden_by_scholar": true,
           "stale": true,
           "source_hash": "string",
           "pages": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "files": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "role": "page",
             "ordinal": 1,
             "content_type": "string",
             "size_bytes": 1,
             "width": 1,
             "height": 1,
             "created_at": "2026-09-22T09:30:00Z",
             "url": "https://erp.example.pk/samas/hook"
            }
           ],
           "created_by": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "created_at": "2026-09-22T09:30:00Z",
           "updated_at": "2026-09-22T09:30:00Z"
          },
          "share_link": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "active": true,
           "token": "string",
           "url": "https://erp.example.pk/samas/hook",
           "created_at": "2026-09-22T09:30:00Z",
           "expires_at": "2026-09-22T09:30:00Z",
           "view_count": 1,
           "last_viewed_at": "2026-09-22T09:30:00Z"
          },
          "guide_source_hash": "string",
          "picture_count": 1,
          "fingerprints": [
           "string"
          ],
          "duplicate_of": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "guide_request": {
           "waiting": true,
           "requested_at": "2026-09-22T09:30:00Z",
           "skipped": true,
           "skip_reason": "string",
           "skipped_at": "2026-09-22T09:30:00Z",
           "kind": "fix",
           "what": "string",
           "section": 1,
           "asked_by_kind": "incharge",
           "versions": 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"
     }
    }
   }
  },
  "/me/learn/notes/{note_id}/feedback": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "My rating of a study guide",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Her stars, suggestion and her incharge's answer, or null (L91, L93).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnFeedback.\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": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/LearnFeedback"
            },
            {
             "type": "null"
            }
           ]
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "rating": 1,
          "suggestion": "string",
          "status": "none",
          "answer": "string",
          "answered_at": "2026-09-22T09:30:00Z",
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "first_name": "string",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course": "string",
          "note_date": "2026-09-01",
          "guide_title": "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"
     }
    }
   },
   "put": {
    "tags": [
     "Learn"
    ],
    "summary": "Rate a study guide",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. 1 to 5 stars and, if she likes, what should change (L91). A new or changed suggestion goes to her incharge as open; a rating alone does not. NOT_FOUND when the day has no guide. A History line.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `note_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| `rating` | integer | Required; from 1 to 5. |\n| `suggestion` | string | Optional; may be null; at most 1000 characters; What should change, in her words. Empty for a rating only. |\n\n**Response 200:** `{ data, meta }` where `data` is one LearnFeedback.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `note_id` | string | Always present; UUID. |\n| `rating` | integer | Always present; from 1 to 5. |\n| `suggestion` | string | May be omitted; may be null; at most 1000 characters. |\n| `status` | string | Always present; one of `none`, `open`, `changed`, `kept`; none: a rating only; open: a suggestion waiting for her incharge; changed or kept: answered (L93). |\n| `answer` | string | May be omitted; may be null; at most 300 characters. |\n| `answered_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| `updated_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `student_id` | string | May be omitted; UUID; Her incharge's view only. |\n| `first_name` | string | May be omitted; may be null; at most 60 characters. |\n| `course_id` | string | May be omitted; UUID. |\n| `course` | string | May be omitted; may be null; at most 120 characters. |\n| `note_date` | string | May be omitted; date YYYY-MM-DD. |\n| `guide_title` | string | May be omitted; may be null; at most 120 characters. |\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"
     },
     {
      "name": "note_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LearnFeedbackWrite"
       },
       "example": {
        "rating": 1,
        "suggestion": "string"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnFeedback"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "rating": 1,
          "suggestion": "string",
          "status": "none",
          "answer": "string",
          "answered_at": "2026-09-22T09:30:00Z",
          "created_at": "2026-09-22T09:30:00Z",
          "updated_at": "2026-09-22T09:30:00Z",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "first_name": "string",
          "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "course": "string",
          "note_date": "2026-09-01",
          "guide_title": "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"
     }
    }
   }
  },
  "/me/learn/coordinating": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "Scholars I coordinate",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. The scholars whose incharge named me their coordinator (L82), each with her active courses, for Add notes. Empty when none. With a course id from here, POST /me/learn/notes opens her day and the upload calls add her photos (L83).\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of LearnCoordinated.\n\n| Field | Type | Notes |\n|---|---|---|\n| `student_id` | string | Always present; UUID. |\n| `full_name` | string | Always present; at most 120 characters. |\n| `admission_no` | string | May be omitted; may be null; at most 40 characters. |\n| `since` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `today` | string | May be omitted; date YYYY-MM-DD. |\n| `courses` | array of object | 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": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/LearnCoordinated"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "full_name": "string",
           "admission_no": "string",
           "since": "2026-09-22T09:30:00Z",
           "today": "2026-09-01",
           "courses": [
            {
             "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
             "title": "string",
             "status": "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"
     }
    }
   }
  },
  "/me/learn/coordinating/{student_id}/courses": {
   "post": {
    "tags": [
     "Learn"
    ],
    "summary": "Add a course for a scholar I coordinate",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. As her own Add a course; she sees it in History as added by her coordinator (L83). NOT_ALLOWED unless I am her coordinator.\n\n**Who can call:** signed-in person (Bearer session token).\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| `title` | string | Required; 2 to 120 characters. |\n| `code` | string | Optional; may be null; at most 20 characters. |\n| `instructor` | string | Optional; may be null; at most 80 characters. |\n| `term` | string | Optional; may be null; at most 40 characters. |\n| `language` | string | Optional; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Optional; one of `active`, `finished`, `archived`. |\n| `section_subject_id` | string | Optional; may be null; UUID; at most 40 characters; A subject of the scholar's own section (L53); NOT_FOUND otherwise. |\n\n**Response 201:** `{ data, meta }` where `data` is one LearnCourse.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Always present; UUID. |\n| `student_id` | string | Always present; UUID. |\n| `title` | string | Always present; at most 120 characters. |\n| `code` | string | May be omitted; may be null; at most 20 characters. |\n| `instructor` | string | May be omitted; may be null; at most 80 characters. |\n| `term` | string | May be omitted; may be null; at most 40 characters. |\n| `language` | string | Always present; one of `english`, `urdu`, `mixed`. |\n| `status` | string | Always present; one of `active`, `finished`, `archived`. |\n| `subject` | object | May be omitted. |\n| `days_of_notes` | integer | Always present; Days with at least one picture. |\n| `last_note_date` | string | May be omitted; may be null; date YYYY-MM-DD. |\n| `recent_dates` | array of string | May be omitted. |\n| `slots` | array of LearnSlot | Always present. |\n| `created_at` | string | May be omitted; timestamp with offset or Z (RFC 3339). |\n| `updated_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- 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/LearnCourseCreate"
       },
       "example": {
        "title": "string",
        "code": "string",
        "instructor": "string",
        "term": "string",
        "language": "english",
        "status": "active",
        "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LearnCourse"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
          "title": "string",
          "code": "string",
          "instructor": "string",
          "term": "string",
          "language": "english",
          "status": "active",
          "subject": {
           "section_subject_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "name": "string"
          },
          "days_of_notes": 1,
          "last_note_date": "2026-09-22",
          "recent_dates": [
           "2026-09-01"
          ],
          "slots": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "weekday": 1,
            "start": "12:30",
            "end": "14:30",
            "kind": "lecture",
            "room": "string",
            "instructor": "string"
           }
          ],
          "created_at": "2026-09-22T09:30:00Z",
          "updated_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"
     }
    }
   }
  },
  "/me/learn/history": {
   "get": {
    "tags": [
     "Learn"
    ],
    "summary": "History",
    "description": "Learn add-on: ADD_ON_OFF when the school has not turned it on. Everything that happened to the scholar's Learn record, newest first, append only (L49): what she did and what a school admin did, reads included. kinds filters, before is the id to page from, size up to 200.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `kinds` | query | string | Optional; at most 200 characters. |\n| `before` | query | integer | Optional. |\n| `size` | query | integer | Optional; from 1 to 200. |\n\n**Response 200:** `{ data, meta }` where `data` is a list of LearnEvent.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | integer | Always present. |\n| `kind` | string | Always present; one of `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`. |\n| `actor_kind` | string | Always present; one of `student`, `incharge`, `system`, `coordinator`; 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. |\n| `actor` | object | May be omitted. |\n| `course_id` | string | May be omitted; may be null; UUID. |\n| `note_id` | string | May be omitted; may be null; UUID. |\n| `detail` | object | May be omitted. |\n| `at` | string | Always present; 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"
     },
     {
      "name": "kinds",
      "in": "query",
      "schema": {
       "type": "string",
       "maxLength": 200
      },
      "description": "Comma-separated kinds."
     },
     {
      "name": "before",
      "in": "query",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "size",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 200
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/LearnEvent"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {
           "id": 1,
           "kind": "learn_opened",
           "actor_kind": "student",
           "actor": {
            "user_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "full_name": "string"
           },
           "course_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "note_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "detail": {},
           "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"
     }
    }
   }
  },
  "/me/live/sessions": {
   "get": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "My children's live sessions",
    "description": "Live Sabaq add-on (it needs the Quran Madrasa add-on): LIVE_ADDON_OFF when the school has not turned it on, LIVE_DISABLED while the school or saMas has paused it. Each child with the consent and the sessions live now, in the next 7 days and the last 10. can_join and join_blocked say whether Join works.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is one LiveAny.\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/LiveAny"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {},
         "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"
     }
    }
   }
  },
  "/me/live/consent": {
   "get": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "A child's consent",
    "description": "Live sessions and recording, as given.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `student_id` | query | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LiveAny.\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": "student_id",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LiveAny"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {},
         "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"
     }
    }
   },
   "put": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "Give or withdraw consent",
    "description": "A guardian, themselves (never while viewing as someone). Both live and recording are needed to join, because every session is recorded.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Request body** (JSON; unknown fields are rejected)\n\n| Field | Type | Rules |\n|---|---|---|\n| `student_id` | string | Required; UUID; at most 40 characters. |\n| `live` | boolean | Required. |\n| `recording` | boolean | Required. |\n| `language` | string | Optional; one of `en`, `ur`. |\n\n**Response 200:** `{ data, meta }` where `data` is one LiveAny.\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": {
         "student_id": {
          "type": "string",
          "format": "uuid",
          "maxLength": 40
         },
         "live": {
          "type": "boolean"
         },
         "recording": {
          "type": "boolean"
         },
         "language": {
          "type": "string",
          "enum": [
           "en",
           "ur"
          ]
         }
        },
        "required": [
         "student_id",
         "live",
         "recording"
        ],
        "additionalProperties": false
       },
       "example": {
        "student_id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
        "live": true,
        "recording": true,
        "language": "en"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LiveAny"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {},
         "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"
     }
    }
   }
  },
  "/me/live/sessions/{session_id}/join": {
   "post": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "Join a live session",
    "description": "The student, or a guardian (listen, or join for the child on this phone). LIVE_NOT_STARTED, LIVE_ENDED, LIVE_CANCELLED, LIVE_NO_CONSENT, LIVE_NOT_ON_ROSTER, LIVE_CAP_REACHED, LIVE_SESSION_TIME_UP.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `session_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| `as` | string | Optional; one of `listener`, `child`; Guardians: listener (default) listens without being heard; child joins for the child on this phone, logged as via the guardian. |\n| `device` | object | Optional; What the person joins from, for the session record. |\n\n**Response 200:** `{ data, meta }` where `data` is one LiveJoin.\n\n| Field | Type | Notes |\n|---|---|---|\n| `url` | string | Always present; The audio service address the app connects to. Never shown to anyone. |\n| `token` | string | Always present; The join pass, valid 10 minutes; refresh it with the token route. |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `role` | string | Always present; one of `teacher`, `student`, `listener`. |\n| `mode` | string | Always present; one of `audio`. |\n| `recording` | boolean | Always present. |\n| `ends_by` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `page` | object | Always present. |\n| `marks` | array of LiveMark | Always present. |\n| `identity` | string | May be omitted; The caller's id in the room. |\n| `name` | string | May be omitted; The name shown to everyone in the room. |\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"
     },
     {
      "name": "session_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "description": "The live session."
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "as": {
          "type": "string",
          "enum": [
           "listener",
           "child"
          ],
          "description": "Guardians: listener (default) listens without being heard; child joins for the child on this phone, logged as via the guardian."
         },
         "device": {
          "type": "object",
          "properties": {
           "platform": {
            "type": "string",
            "maxLength": 20
           },
           "app_version": {
            "type": "string",
            "maxLength": 20
           }
          },
          "description": "What the person joins from, for the session record.",
          "additionalProperties": false
         }
        },
        "additionalProperties": false
       },
       "example": {
        "as": "listener",
        "device": {
         "platform": "string",
         "app_version": "string"
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LiveJoin"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "url": "string",
          "token": "string",
          "expires_at": "2026-09-22T09:30:00Z",
          "role": "teacher",
          "mode": "audio",
          "recording": true,
          "ends_by": "2026-09-22T09:30:00Z",
          "page": {
           "surah": 1,
           "ayah": 1
          },
          "marks": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "surah": 1,
            "ayah": 1,
            "word": 1,
            "kind": "tajweed",
            "rule": "ghunnah",
            "note": "string",
            "at": "2026-09-22T09:30:00Z",
            "removed": true
           }
          ],
          "identity": "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"
     }
    }
   }
  },
  "/me/live/sessions/{session_id}/token": {
   "post": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "Refresh the join pass",
    "description": "A fresh 10-minute pass inside the session.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `session_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| `as` | string | Optional; one of `listener`, `child`. |\n\n**Response 200:** `{ data, meta }` where `data` is one LiveToken.\n\n| Field | Type | Notes |\n|---|---|---|\n| `token` | string | Always present. |\n| `expires_at` | string | Always present; 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)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "$ref": "#/components/parameters/idempotency"
     },
     {
      "name": "session_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "description": "The live session."
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "as": {
          "type": "string",
          "enum": [
           "listener",
           "child"
          ]
         }
        },
        "additionalProperties": false
       },
       "example": {
        "as": "listener"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LiveToken"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "token": "string",
          "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"
     }
    }
   }
  },
  "/me/live/sessions/{session_id}/review": {
   "get": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "Session review",
    "description": "Marks, result, the next assignment and the recording's state.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `session_id` | path | string | Required; UUID; at most 40 characters. |\n\n**Response 200:** `{ data, meta }` where `data` is one LiveReview.\n\n| Field | Type | Notes |\n|---|---|---|\n| `session` | LiveSession | Always present. |\n| `attendance` | object | Always present. |\n| `marks` | array of LiveMark | Always present. |\n| `result` | LiveResultParts | Always present. |\n| `next_assignment` | string | Always present; may be null. |\n| `note` | string | Always present; may be null. |\n| `recording` | object | 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "session_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "description": "The live session."
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LiveReview"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "session": {
           "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
           "kind": "sabaq",
           "status": "scheduled",
           "starts_at": "2026-09-22T09:30:00Z",
           "minutes": 1,
           "ends_by": "2026-09-22T09:30:00Z",
           "mode": "audio",
           "recorded": true,
           "teacher": {
            "name": "string"
           },
           "range": {
            "surah": 1,
            "ayah_from": 1,
            "ayah_to": 1
           },
           "can_join": true,
           "join_blocked": "LIVE_NO_CONSENT",
           "result": {
            "sabaq": "pass",
            "sabqi": "pass",
            "manzil": "pass"
           }
          },
          "attendance": {
           "joined_at": "2026-09-22T09:30:00Z",
           "left_at": "2026-09-22T09:30:00Z",
           "seconds": 1
          },
          "marks": [
           {
            "id": "2f1c6a0e-8d7b-4c1e-9a55-3b1f0c2d4e6f",
            "surah": 1,
            "ayah": 1,
            "word": 1,
            "kind": "tajweed",
            "rule": "ghunnah",
            "note": "string",
            "at": "2026-09-22T09:30:00Z",
            "removed": true
           }
          ],
          "result": {
           "sabaq": "pass",
           "sabqi": "pass",
           "manzil": "pass"
          },
          "next_assignment": "string",
          "note": "string",
          "recording": {
           "available": true,
           "started_at": "2026-09-22T09:30:00Z",
           "duration_seconds": 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"
     }
    }
   }
  },
  "/me/live/sessions/{session_id}/play": {
   "get": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "Play the recording",
    "description": "A 10-minute streaming link; the play is logged. No downloads.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `session_id` | path | string | Required; UUID; at most 40 characters. |\n| `segment` | query | integer | Optional; from 1 to 50. |\n\n**Response 200:** `{ data, meta }` where `data` is one LivePlayLink.\n\n| Field | Type | Notes |\n|---|---|---|\n| `url` | string | Always present; A 10-minute link on the API host that streams the recording. Each play is logged; recordings are never downloadable. |\n| `expires_at` | string | Always present; timestamp with offset or Z (RFC 3339). |\n| `content_type` | string | Always present; at most 40 characters. |\n| `duration_seconds` | 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- 429 RATE_LIMITED (see Retry-After)",
    "parameters": [
     {
      "$ref": "#/components/parameters/request_id"
     },
     {
      "name": "session_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "description": "The live session."
     },
     {
      "name": "segment",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 50
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LivePlayLink"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {
          "url": "string",
          "expires_at": "2026-09-22T09:30:00Z",
          "content_type": "string",
          "duration_seconds": 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"
     }
    }
   }
  },
  "/me/live/sessions/{session_id}/report": {
   "post": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "Report a problem",
    "description": "Goes to the saMas safety team and the principal (only saMas when it is about the principal), answered within 24 hours.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Parameters**\n\n| Parameter | In | Type | Rules |\n|---|---|---|---|\n| `session_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| `kind` | string | Required; one of `sound`, `unsafe`, `other`. |\n| `note` | string | Optional; at most 500 characters. |\n| `about_principal` | boolean | Optional; The report is about the principal: only the saMas safety team receives it. |\n\n**Response 201:** `{ data, meta }` where `data` is one LiveAny.\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"
     },
     {
      "name": "session_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "description": "The live session."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "kind": {
          "type": "string",
          "enum": [
           "sound",
           "unsafe",
           "other"
          ]
         },
         "note": {
          "type": "string",
          "maxLength": 500
         },
         "about_principal": {
          "type": "boolean",
          "description": "The report is about the principal: only the saMas safety team receives it."
         }
        },
        "required": [
         "kind"
        ],
        "additionalProperties": false
       },
       "example": {
        "kind": "sound",
        "note": "string",
        "about_principal": true
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/LiveAny"
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": {},
         "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"
     }
    }
   }
  },
  "/me/live/activity": {
   "get": {
    "tags": [
     "Live Sabaq"
    ],
    "summary": "Activity about my children",
    "description": "Your own actions and staff actions on your children (a play of a recording, a result, a cancellation), newest first.\n\n**Who can call:** signed-in person (Bearer session token).\n\n**Response 200:** `{ data, meta }` where `data` is a list of LiveAny.\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/LiveAny"
           }
          }
         },
         "required": [
          "data"
         ]
        },
        "example": {
         "data": [
          {}
         ],
         "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"
     }
    }
   }
  }
 },
 "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}$"
     },
     "full_name": {
      "type": "string",
      "pattern": "^[A-Za-z'\\- ]{2,80}$"
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "female",
       "male",
       "other",
       null
      ]
     },
     "section_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "roll_no": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 999
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "inactive",
       "graduated",
       "withdrawn"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email"
     },
     "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)."
     }
    },
    "required": [
     "admission_no",
     "full_name",
     "date_of_birth"
    ]
   },
   "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}$"
     },
     "date_of_birth": {
      "type": "string",
      "format": "date",
      "example": "2026-09-01"
     },
     "gender": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "female",
       "male",
       "other",
       null
      ]
     },
     "section_id": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "roll_no": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 1,
      "maximum": 999
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "inactive",
       "graduated",
       "withdrawn"
      ]
     },
     "phone": {
      "type": [
       "string",
       "null"
      ]
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email"
     },
     "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)."
     },
     "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"
     },
     "status": {
      "$ref": "#/components/schemas/AttendanceStatus"
     },
     "note": {
      "type": "string",
      "maxLength": 200,
      "description": "Required when status is excused."
     }
    },
    "required": [
     "student_id",
     "status"
    ]
   },
   "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",
      "maxLength": 40
     },
     "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",
      "maxLength": 40
     },
     "note": {
      "type": "string",
      "description": "The work the child chose.",
      "minLength": 1,
      "maxLength": 120
     }
    },
    "required": [
     "minutes",
     "note"
    ],
    "additionalProperties": false
   },
   "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"
      }
     },
     "section_ids": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      }
     },
     "publish_at": {
      "type": "string",
      "format": "date-time"
     },
     "expires_at": {
      "type": "string",
      "format": "date-time"
     },
     "pinned": {
      "type": "boolean"
     }
    },
    "required": [
     "title",
     "body"
    ]
   },
   "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"
     },
     "amount": {
      "type": "string",
      "description": "Amount with two decimals, as a string; never negative.",
      "pattern": "^\\d+\\.\\d{2}$",
      "example": "12500.00"
     },
     "method": {
      "$ref": "#/components/schemas/PaymentMethod"
     },
     "paid_on": {
      "type": "string",
      "description": "Today or earlier.",
      "format": "date",
      "example": "2026-09-01"
     },
     "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"
      },
      "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."
     }
    },
    "required": [
     "family_account_id",
     "amount",
     "method",
     "paid_on"
    ]
   },
   "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",
       "maxLength": 40
      },
      "minItems": 1,
      "maxItems": 8
     },
     "fee_month": {
      "type": "string",
      "description": "Any day; normalised to the first of the month.",
      "format": "date",
      "example": "2026-09-01",
      "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": {
      "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.",
      "maxLength": 200
     }
    },
    "required": [
     "student_ids",
     "fee_month",
     "amount",
     "method"
    ],
    "additionalProperties": false
   },
   "ClaimWrite": {
    "type": "object",
    "properties": {
     "paid_on": {
      "type": "string",
      "description": "The day the parent paid; not in the future.",
      "format": "date",
      "example": "2026-09-01",
      "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": {
      "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.",
      "maxLength": 200
     }
    },
    "required": [
     "paid_on",
     "amount",
     "method"
    ],
    "description": "A parent's answer to a fee reminder: I have paid.",
    "additionalProperties": false
   },
   "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"
      ],
      "additionalProperties": false
     },
     {
      "type": "object",
      "properties": {
       "id": {
        "type": "string",
        "maxLength": 64,
        "description": "A file already attached, to keep it."
       }
      },
      "required": [
       "id"
      ],
      "additionalProperties": false
     }
    ]
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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."
     }
    },
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "LearnPictureAdd": {
    "type": "object",
    "properties": {
     "upload_id": {
      "type": "string",
      "format": "uuid",
      "maxLength": 40
     },
     "label": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 60
     }
    },
    "required": [
     "upload_id"
    ],
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "LearnMaterialAdd": {
    "type": "object",
    "properties": {
     "upload_id": {
      "type": "string",
      "format": "uuid",
      "maxLength": 40
     },
     "title": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 120,
      "description": "Defaults to the file name without its extension."
     }
    },
    "required": [
     "upload_id"
    ],
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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)."
     }
    },
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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",
      "maxLength": 80
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "format": "email",
      "maxLength": 200
     }
    },
    "required": [
     "name",
     "relation"
    ],
    "additionalProperties": false
   },
   "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",
      "maxLength": 80
     },
     "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",
      "maxLength": 40
     },
     "class_id": {
      "type": "string",
      "format": "uuid",
      "maxLength": 40
     },
     "child_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": [
       "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",
      "maxLength": 80
     },
     "notes": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     }
    },
    "required": [
     "school",
     "academic_year_id",
     "class_id",
     "child_name",
     "date_of_birth",
     "guardians"
    ],
    "additionalProperties": false
   },
   "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",
      "maxLength": 80
     },
     "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",
      "maxLength": 80
     },
     "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.",
      "maxLength": 40
     },
     "to": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid",
       "maxLength": 40
      },
      "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"
    ],
    "additionalProperties": false
   },
   "MessageWrite": {
    "type": "object",
    "properties": {
     "body": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000
     },
     "files": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/HomeworkFileRef"
      },
      "maxItems": 5
     }
    },
    "additionalProperties": false
   },
   "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",
      "maxLength": 80
     },
     "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",
      "maxLength": 80
     },
     "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",
      "maxLength": 80
     },
     "query": {
      "type": "string",
      "description": "The words of a search that found nothing. Needed for search_empty.",
      "minLength": 1,
      "maxLength": 80
     }
    },
    "required": [
     "kind"
    ],
    "additionalProperties": false
   },
   "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",
    "maxLength": 80
   },
   "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).",
      "additionalProperties": false
     }
    },
    "required": [
     "question"
    ],
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "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}$",
       "maxLength": 80
      },
      "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.",
      "maxLength": 80
     },
     "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"
    ],
    "additionalProperties": false
   },
   "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
       }
      }
     },
     "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"
    ]
   },
   "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"
    ]
   },
   "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}$"
     }
    },
    "required": [
     "token"
    ]
   },
   "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",
      "maxLength": 40
     },
     "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"
    ],
    "additionalProperties": false
   },
   "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."
   },
   "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.",
    "additionalProperties": false
   },
   "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"
    ],
    "additionalProperties": false
   },
   "IssueNote": {
    "type": "object",
    "properties": {
     "note": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 2000,
      "description": "A message the reporter sees, left out for none."
     }
    },
    "additionalProperties": false
   },
   "IssueReason": {
    "type": "object",
    "properties": {
     "reason": {
      "type": "string",
      "description": "Why the screenshot is being removed.",
      "minLength": 3,
      "maxLength": 200
     }
    },
    "required": [
     "reason"
    ],
    "additionalProperties": false
   },
   "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",
      "maxLength": 40
     },
     "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"
    ],
    "additionalProperties": false
   },
   "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."
   }
  }
 }
}