{
  "openapi": "3.1.0",
  "info": {
    "title": "betterSLCM API",
    "version": "1.0.0",
    "x-api-version": "1",
    "x-deprecation-policy": {
      "notice_days": 90,
      "details": "https://slcm.vercel.app/docs#deprecation-policy",
      "signalling": "Announced in /docs and in this document. No Sunset or Deprecation header is sent, because only one major version has ever existed and there is nothing to deprecate; those headers are added in the same commit that introduces a second major."
    },
    "description": "Public HTTP API for betterSLCM, a faster interface for the MUJ SLCM student portal. Endpoints under /api/data and most others require a session cookie obtained via POST /api/auth/login with MUJ SLCM credentials. Stateless public endpoints (contact, warm, session-check) need no credentials. All responses use the envelope { success: boolean, data?: ..., error?: string, code?: string }, including 404, 405 and unhandled errors, so a client can parse every response the same way. Versioning: the API is at major version 1. Send API-Version: 1 (optional; 1 is the default) and read the API-Version response header, which is present on every /api response. Any other version is rejected with 400 code UNSUPPORTED_VERSION. Breaking changes ship as a new major version; the old major keeps working for at least 90 days with a notice in /docs and here.",
    "contact": {
      "name": "betterSLCM",
      "url": "https://slcm.vercel.app/contact"
    }
  },
  "servers": [{ "url": "https://slcm.vercel.app" }],
  "paths": {
    "/api/warm": {
      "get": {
        "operationId": "getWarm",
        "summary": "Liveness check",
        "description": "Returns 200 when the server is up. No credentials needed. Useful for uptime checks and cold-start warming.",
        "responses": {
          "200": {
            "description": "Server is warm.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": { "type": "string" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["message", "success"]
                }
              }
            }
          }
        }
      }
    },
    "/api/slcm-status": {
      "get": {
        "operationId": "getSlcmStatus",
        "summary": "Last observed MUJ SLCM availability",
        "description": "Reports what the scheduled probe last observed about the MUJ SLCM upstream. Performs no upstream call, so it is cheap and cannot itself time out. States are 'healthy' (a service-account login returned the expected dashboard redirect), 'degraded' (upstream answered promptly but the login did not work, e.g. it redirected somewhere useless), 'unreachable' (the request timed out or failed outright), 'misconfigured' (our own service account was rejected, so upstream state is genuinely unknown), and 'unknown' (no recent observation, including when the backing store is unavailable). Always returns 200: 'unknown' is a successful answer, not a failure of this endpoint. Treat 'unknown' as unknown rather than healthy, and do not collapse it into healthy: use lastVerdict to tell a stopped probe from a recovery. The recorded failure message is intentionally not returned, since this route is unauthenticated.",
        "responses": {
          "200": {
            "description": "Current known state of the upstream, or unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "state": {
                      "type": "string",
                      "enum": [
                        "healthy",
                        "degraded",
                        "unreachable",
                        "misconfigured",
                        "unknown"
                      ]
                    },
                    "success": { "type": "boolean" },
                    "lastCheckedAt": {
                      "description": "Unix seconds of the most recent probe run, from a record that does not expire. Null before the first probe has ever run.",
                      "type": ["integer", "null"]
                    },
                    "lastVerdict": {
                      "description": "The most recent verdict ever recorded, from a record that does not expire. This is what makes `state: \"unknown\"` actionable: the current record carries a 30 minute TTL that only the probe refreshes, so a stopped probe turns a known outage into `unknown` with no other signal. A previous `degraded` or `unreachable` alongside an `unknown` state means the probe stopped, not that the upstream recovered. Null before the first probe has ever run.",
                      "type": ["string", "null"],
                      "enum": [
                        "healthy",
                        "degraded",
                        "unreachable",
                        "misconfigured",
                        null
                      ]
                    },
                    "unhealthySince": {
                      "description": "Unix seconds when the current run of upstream-attributable bad verdicts began, or null. Carried forward across ticks while the verdict stays upstream-attributable, which includes alternating between degraded and unreachable, and reset on recovery or when the verdict becomes misconfigured. Null on a healthy, misconfigured, or unknown state, so read it together with `state` rather than on its own. This is a fact from our own probe and is the honest thing to display, because the upstream publishes no ETA for its outages.",
                      "type": ["integer", "null"]
                    }
                  },
                  "required": ["state", "success"]
                }
              }
            }
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "operationId": "submitContact",
        "summary": "Send an anonymous report or feature request",
        "description": "Accepts a bug report, feature request, or other note. Name is optional and blank means anonymous. Rate limited to 5 messages per hour per IP: every response carries RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers, and a 429 also carries Retry-After. Request bodies larger than 8KB are rejected.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ContactRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report accepted.",
            "headers": { "$ref": "#/components/headers/RateLimitHeaders" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "success": { "type": "boolean" } },
                  "required": ["success"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid body (unknown type, message outside 10-2000 chars, or body over 8KB).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry after the seconds in the Retry-After header.",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders",
              "Retry-After": {
                "description": "Seconds until the quota window resets.",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "502": {
            "description": "Delivery backends temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/auth/session-check": {
      "get": {
        "operationId": "getSessionCheck",
        "summary": "Check login state",
        "description": "Returns whether the current session cookie holds a live SLCM login, plus the username when logged in. No credentials needed.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Session state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/SessionState" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "operationId": "postLogin",
        "summary": "Log in with MUJ SLCM credentials",
        "description": "Exchanges MUJ SLCM credentials for an encrypted session cookie. Credentials are used against the SLCM portal and are never stored. Returns 401 when SLCM rejects the credentials and 503 when SLCM itself is unreachable or broken, so an upstream outage is never reported as a credential problem. Honour Retry-After on a 503.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "username": {
                    "type": "string",
                    "description": "SLCM username, e.g. NAME.REGNO"
                  },
                  "password": {
                    "type": "string",
                    "description": "SLCM portal password"
                  }
                },
                "required": ["username", "password"],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logged in; session cookie set.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": { "type": "string" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["message", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Credentials rejected by SLCM.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "503": {
            "description": "SLCM is unreachable, timed out, or redirected the login somewhere other than the dashboard. The credentials were not necessarily wrong. Includes a Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. This escalates the longer the outage has been observed running, from 120 seconds up to 1800, because each retry is a further request to an upstream that is already struggling. The ceiling equals the interval at which this service's own health verdict can change, so an obedient client never polls faster than new information can arrive. 120 is also the answer when no health record is available, which can happen during a long outage if the probe's record has expired, so treat a 120 as a hint rather than a measurement.",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": ["UPSTREAM_UNAVAILABLE"]
                    },
                    "error": { "type": "string" },
                    "resolution": { "type": "string" },
                    "retryAfterSeconds": { "type": "integer" },
                    "success": { "type": "boolean" },
                    "verdict": {
                      "description": "How this particular login failed, classified the same way the scheduled probe classifies its own login, so a client can distinguish a portal that answered and redirected from one that never answered. It describes the caller's own attempt, not the current state of the service, so two concurrent callers can receive different values. Present so the two can show different copy; branch on `code`, not on this.",
                      "type": "string",
                      "enum": ["degraded", "unreachable"]
                    }
                  },
                  "required": ["code", "error", "success", "verdict"]
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/logout": {
      "post": {
        "operationId": "postLogout",
        "summary": "Log out",
        "description": "Clears the session cookie.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Logged out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "success": { "type": "boolean" } },
                  "required": ["success"]
                }
              }
            }
          }
        }
      }
    },
    "/api/user": {
      "get": {
        "operationId": "getUser",
        "summary": "Current username",
        "description": "Returns the logged-in username, or null when logged out. Never errors on missing sessions.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Username payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/UsernameData" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          }
        }
      }
    },
    "/api/info": {
      "get": {
        "operationId": "getInfo",
        "summary": "Student profile and attendance history",
        "description": "Session-gated. Returns profile rows plus normalized attendance history entries used by the info page.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Profile payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/InfoData" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired (code SESSION_EXPIRED) — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/getpersonalattendance": {
      "get": {
        "operationId": "getPersonalAttendance",
        "summary": "Personal attendance records",
        "description": "Session-gated. Returns per-subject attendance records and records history.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Attendance payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/AttendanceData" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "triggerAttendanceCron",
        "summary": "Trigger the attendance check with the service account",
        "description": "Runs the scheduled attendance job: logs into SLCM with the stored service-account credentials, records attendance history, sends a notification email if anything changed, and updates the upstream health verdict. POST rather than GET because it writes. Authenticates with the shared cron secret, sent either as `Authorization: Bearer <CRON_SECRET>` or as a JSON body of `{\"secret\":\"<CRON_SECRET>\"}`. Cross-origin requests are refused. Not intended for interactive use; use GET with a session cookie instead.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "security": [{ "cronSecret": [] }],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "secret": {
                    "type": "string",
                    "description": "The cron secret. Omit if sending the Authorization header instead."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Attendance checked; body says whether anything changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": { "type": "string" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["message", "success"]
                }
              }
            }
          },
          "400": {
            "description": "Body present but unreadable: not JSON, or carrying keys other than `secret` (code INVALID_BODY).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "401": {
            "description": "No valid cron secret.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "403": {
            "description": "Cross-origin request refused.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "500": {
            "description": "The deployment has no SLCM service-account credentials configured. This is a server misconfiguration, not a caller error.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "502": {
            "description": "SLCM upstream trouble. When the upstream login is what failed, the response also carries Retry-After, escalating with outage age; when the attendance fetch is what failed, it does not.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/data/attendance": {
      "get": {
        "operationId": "getDataAttendance",
        "summary": "Attendance summary",
        "description": "Session-gated. Returns the attendance summary used by the dashboard: one item per course with present/absent/total counts and a percentage string. Upstream SLCM outages surface as 502 and must not trigger logout; only 401 with code SESSION_EXPIRED means re-login.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Attendance payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/AttendanceData" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "502": {
            "description": "SLCM upstream trouble — retry later, stay logged in.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/data/internal-marks": {
      "get": {
        "operationId": "getDataInternalMarks",
        "summary": "Internal marks",
        "description": "Session-gated. Returns assessment-wise internal marks (CWS, MTE1, MTE2, ETE, PRE, PRS components) per course.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Marks payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/InternalMarksData"
                    },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/data/grades": {
      "get": {
        "operationId": "getDataGrades",
        "summary": "Semester grades",
        "description": "Session-gated. Returns grade history across semesters with course codes, credits, and grades.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Grades payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/GradesData" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/data/cgpa": {
      "get": {
        "operationId": "getDataCgpa",
        "summary": "CGPA/GPA breakdown",
        "description": "Session-gated. Returns semester-wise GPA entries plus per-semester credit and GPA summaries.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "CGPA payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/CGPAData" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/data/timetable": {
      "get": {
        "operationId": "getDataTimetable",
        "summary": "Class timetable",
        "description": "Session-gated. Returns the weekly class timetable as a list of events with course, room, section, and start/end dates.",
        "parameters": [{ "$ref": "#/components/parameters/ApiVersion" }],
        "responses": {
          "200": {
            "description": "Timetable payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/TimetableData" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/data/timetable/fetch-missing": {
      "get": {
        "operationId": "getTimetableFetchMissing",
        "summary": "Backfill missing timetable entries",
        "description": "Session-gated. Fetches timetable entries missing from cache. Accepts optional Dated and EventID/EventIDs filters.",
        "parameters": [
          { "$ref": "#/components/parameters/ApiVersion" },
          {
            "name": "Dated",
            "in": "query",
            "required": false,
            "description": "Date filter for missing entries.",
            "schema": { "type": "string" }
          },
          {
            "name": "EventID",
            "in": "query",
            "required": false,
            "description": "Single event id to fetch.",
            "schema": { "type": "string" }
          },
          {
            "name": "EventIDs",
            "in": "query",
            "required": false,
            "description": "Repeated event id to fetch (repeat the parameter).",
            "schema": { "type": "array", "items": { "type": "string" } }
          }
        ],
        "responses": {
          "200": {
            "description": "Backfill payload: event ids mapped to attendance types.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/EventDetailMap" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    },
    "/api/data/timetable/detail": {
      "get": {
        "operationId": "getTimetableDetail",
        "summary": "Timetable event detail",
        "description": "Session-gated. Returns detail for one timetable event: topic, room, slot, class type, and attendance type.",
        "parameters": [
          { "$ref": "#/components/parameters/ApiVersion" },
          {
            "name": "Id",
            "in": "query",
            "required": false,
            "description": "SLCM timetable event id.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Event detail payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/EventDetail" },
                    "success": { "type": "boolean" }
                  },
                  "required": ["data", "success"]
                }
              }
            }
          },
          "401": {
            "description": "Session expired — re-login.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cronSecret": {
        "type": "http",
        "scheme": "bearer",
        "description": "Shared secret for the scheduled attendance job. Sent as `Authorization: Bearer <CRON_SECRET>`, or as `{\"secret\":\"<CRON_SECRET>\"}` in a JSON body for clients that cannot set headers."
      }
    },
    "parameters": {
      "ApiVersion": {
        "name": "API-Version",
        "in": "header",
        "required": false,
        "description": "API major version. Send 1 (the default when omitted). Any other value is rejected with 400 code UNSUPPORTED_VERSION. Every response echoes the serving major in the API-Version header.",
        "schema": { "type": "string", "enum": ["1"], "default": "1" }
      }
    },
    "headers": {
      "RateLimitHeaders": {
        "RateLimit-Limit": {
          "description": "Max messages per quota window (5 per hour per IP).",
          "schema": { "type": "integer" }
        },
        "RateLimit-Remaining": {
          "description": "Messages left in the current window.",
          "schema": { "type": "integer" }
        },
        "RateLimit-Reset": {
          "description": "Seconds until the quota window resets.",
          "schema": { "type": "integer" }
        }
      }
    },
    "schemas": {
      "ContactRequest": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": ["bug", "feature", "other"],
            "description": "What the message is about."
          },
          "message": {
            "type": "string",
            "minLength": 10,
            "maxLength": 2000,
            "description": "The report or request, 10-2000 characters."
          },
          "name": {
            "type": "string",
            "maxLength": 100,
            "description": "Optional display name. Omit or leave blank to stay anonymous."
          }
        },
        "required": ["type", "message"],
        "additionalProperties": false
      },
      "SessionState": {
        "type": "object",
        "properties": {
          "isLoggedIn": { "type": "boolean" },
          "username": { "type": ["string", "null"] }
        },
        "required": ["isLoggedIn", "username"]
      },
      "UsernameData": {
        "type": "object",
        "properties": {
          "username": { "type": ["string", "null"] }
        },
        "required": ["username"]
      },
      "InfoData": {
        "type": "object",
        "description": "Student profile rows plus normalized attendance history entries { c, np, nt, op, ot, t }.",
        "properties": {
          "history": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/HistoryEntry" }
          },
          "rows": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Profile info rows."
          }
        }
      },
      "HistoryEntry": {
        "type": "object",
        "properties": {
          "c": { "type": "string", "description": "Course id." },
          "np": { "type": "number", "description": "New present count." },
          "nt": { "type": "number", "description": "New total count." },
          "op": { "type": "number", "description": "Old present count." },
          "ot": { "type": "number", "description": "Old total count." },
          "t": { "type": "string", "description": "Timestamp." }
        },
        "required": ["c", "np", "nt", "op", "ot", "t"]
      },
      "AttendanceItem": {
        "type": "object",
        "properties": {
          "Absent": { "type": "string" },
          "CourseID": { "type": "string" },
          "Percentage": { "type": "string" },
          "Present": { "type": "string" },
          "Total": { "type": "string" }
        },
        "required": ["Absent", "CourseID", "Percentage", "Present", "Total"]
      },
      "AttendanceData": {
        "type": "object",
        "properties": {
          "AttendanceSummaryList": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AttendanceItem" }
          },
          "ErrorMessage": { "type": ["string", "null"] },
          "IsSuccessfull": { "type": "boolean" }
        },
        "required": ["AttendanceSummaryList"]
      },
      "GradeItem": {
        "type": "object",
        "properties": {
          "AcademicSession": { "type": ["string", "null"] },
          "CourseCode": { "type": ["string", "null"] },
          "CourseID": { "type": ["string", "null"] },
          "Credits": { "type": ["string", "null"] },
          "Grade": { "type": ["string", "null"] },
          "sno": { "type": "number" }
        },
        "required": ["sno"]
      },
      "GradesData": {
        "type": "object",
        "properties": {
          "ErrorMessage": { "type": ["string", "null"] },
          "InternalMarksList": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/GradeItem" }
          },
          "IsSuccessfull": { "type": "boolean" }
        },
        "required": ["InternalMarksList"]
      },
      "InternalMarkItem": {
        "type": "object",
        "properties": {
          "CourseCode": { "type": ["string", "null"] },
          "CourseID": { "type": ["string", "null"] },
          "CWS": { "type": ["string", "null"] },
          "ETE": { "type": ["string", "null"] },
          "MTE1": { "type": ["string", "null"] },
          "MTE2": { "type": ["string", "null"] },
          "PRE": { "type": ["string", "null"] },
          "PRS": { "type": ["string", "null"] },
          "RESESSION": { "type": ["string", "null"] },
          "Total": { "type": ["string", "null"] },
          "sno": { "type": "number" }
        },
        "required": ["sno"]
      },
      "InternalMarksData": {
        "type": "object",
        "properties": {
          "ErrorMessage": { "type": ["string", "null"] },
          "InternalMarksList": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/InternalMarkItem" }
          },
          "IsSuccessfull": { "type": "boolean" }
        },
        "required": ["InternalMarksList"]
      },
      "SemesterEntry": {
        "type": "object",
        "properties": {
          "GPASemester": { "type": ["string", "null"] },
          "sno": { "type": "number" }
        },
        "required": ["sno"]
      },
      "CGPASemesterSummary": {
        "type": "object",
        "properties": {
          "CGPA": { "type": ["string", "null"] },
          "RegistrationNo": { "type": ["string", "null"] },
          "StudentName": { "type": ["string", "null"] },
          "TotalCredits": { "type": ["string", "null"] },
          "sno": { "type": "number" }
        },
        "required": ["sno"]
      },
      "CGPAData": {
        "type": "object",
        "properties": {
          "ErrorMessage": { "type": ["string", "null"] },
          "GPAList": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/SemesterEntry" }
          },
          "InternalMarksList": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CGPASemesterSummary" }
          },
          "IsSuccessfull": { "type": "boolean" },
          "RegistrationNo": { "type": ["string", "null"] },
          "StudentName": { "type": ["string", "null"] }
        },
        "required": ["GPAList", "InternalMarksList"]
      },
      "TimetableEvent": {
        "type": "object",
        "properties": {
          "CourseID": { "type": "string" },
          "Description": { "type": "string" },
          "EndDate": { "type": "string" },
          "EntryNo": { "type": "string" },
          "ProgramCode": { "type": "string" },
          "RoomNo": { "type": "string" },
          "Section": { "type": "string" },
          "Semester": { "type": "string" },
          "StartDate": { "type": "string" },
          "eventColorCode": { "type": "string" }
        },
        "required": [
          "CourseID",
          "Description",
          "EndDate",
          "EntryNo",
          "ProgramCode",
          "RoomNo",
          "Section",
          "Semester",
          "StartDate"
        ]
      },
      "TimetableData": {
        "type": "array",
        "items": { "$ref": "#/components/schemas/TimetableEvent" }
      },
      "EventDetail": {
        "type": "object",
        "properties": {
          "AttendanceType": { "type": "string" },
          "BatchNo": { "type": "string" },
          "CourseID": { "type": "string" },
          "CourseOutcome": { "type": "string" },
          "ModeofAssessing": { "type": "string" },
          "ProgramCode": { "type": "string" },
          "RoomNumber": { "type": "string" },
          "Section": { "type": "string" },
          "SlotScheme": { "type": "string" },
          "Topic": { "type": "string" },
          "Typeofclass": { "type": "string" }
        }
      },
      "EventDetailMap": {
        "type": "object",
        "description": "Event ids mapped to objects carrying at least AttendanceType.",
        "additionalProperties": {
          "type": "object",
          "properties": {
            "AttendanceType": { "type": "string" }
          },
          "required": ["AttendanceType"]
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "error": {
            "type": "string",
            "description": "Human-readable failure reason."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable code, e.g. SESSION_EXPIRED, NOT_FOUND, or UNSUPPORTED_VERSION."
          },
          "resolution": {
            "type": "string",
            "description": "What to do next."
          }
        },
        "required": ["success", "error"]
      }
    }
  }
}
