# REST API Documentation

Base URL: `https://yourdomain.com/api/`

All responses are JSON with this shape:
```json
{ "success": true|false, "message": "...", "data": { ... }, "meta": { ... } }
```
Paginated endpoints include `meta.pagination`:
```json
{ "current_page": 1, "per_page": 20, "total_items": 57, "total_pages": 3 }
```

## Discovering the API base URL (dynamic domain)

Don't hardcode this backend's domain in the Flutter app. Instead, hardcode
only the resolver URL (a separate, stable service — see
`mytoolshub-resolver/README.md`), and fetch the real API base URL from it
at startup:

```
GET https://mytoolshub.co.in/resolver/resolve.php?app_key=YOUR_APP_KEY
→ { "success": true, "data": { "api_base_url": "https://current-domain.com", ... } }
```

Cache the result locally (e.g. `shared_preferences`) and use it as the
prefix for every endpoint in this document. Refresh it on app startup and
app resume so that if the admin changes the domain (Super Admin > App
Connection Settings), the app picks it up automatically — no app-store
update required. Fall back to the last cached value if the resolver is
temporarily unreachable.

## Authentication

All endpoints except `auth/*` require a Bearer token:
```
Authorization: Bearer <access_token>
```

Access tokens expire in 24 hours (`JWT_ACCESS_TOKEN_TTL`). Use the refresh
token (valid 30 days) to get a new one without re-prompting for a password.

---

## Auth

### `POST /api/auth/login.php`
```json
{ "role": "admin|teacher|parent", "identifier": "email or phone", "password": "..." }
```
Admin/teacher log in with **email**; parents log in with **phone**.
Returns `access_token`, `refresh_token`, `expires_in`, `user`, `role`, `school`.

Rate-limited: 5 failed attempts locks the identifier for 15 minutes.

### `POST /api/auth/refresh-token.php`
```json
{ "refresh_token": "..." }
```
Returns a fresh `access_token`.

### `POST /api/auth/logout.php` *(auth required)*
```json
{ "refresh_token": "..." }
```
Revokes the refresh token. Discard the access token client-side too.

### `POST /api/auth/forgot-password.php`
```json
{ "role": "admin|teacher|parent", "identifier": "..." }
```
For `admin`/`teacher` (email-based): attempts to email a 6-character reset
code via the school's configured SMTP. Response `data`:
- If email sent: `{ "email_sent": true }`
- If SMTP isn't configured yet, or the send fails: `{ "email_sent": false, "code": "ABC123" }`
  — the app should show this code directly (e.g. "we couldn't email you,
  here's your code") rather than blocking the user, exactly like the web
  admin panels' Forgot Password page falls back to showing the reset link
  on screen. Once SMTP is configured, this direct-reveal stops happening
  automatically.
- If the account doesn't exist: `data: null` (same message either way, but
  note this does mean the response shape differs based on whether an
  account exists — see `SECURITY_NOTES.md` for the reasoning).

For `parent` (phone-based): same pattern via SMS (MSG91/Fast2SMS, school
configures under Settings > SMS). Response `data`:
- If SMS sent: `{ "sms_sent": true }`
- If SMS isn't configured yet, or the send fails: `{ "sms_sent": false, "code": "ABC123" }`
  — same "show the code directly, don't block the user" fallback as the email path.

### `POST /api/auth/reset-password.php`
```json
{ "role": "...", "identifier": "...", "token": "6-char code", "new_password": "..." }
```

---

## Profile

### `GET /api/profile/index.php` *(auth)*
Returns the caller's own profile.

### `POST /api/profile/update.php` *(auth, multipart for photo)*
Fields: `name`, `email` (parent) or `phone` (admin/teacher), `photo` (file).

### `POST /api/profile/change-password.php` *(auth)*
```json
{ "current_password": "...", "new_password": "..." }
```

---

## Dashboard

### `GET /api/dashboard/admin.php` *(admin/teacher)*
Total students/teachers, today's attendance breakdown, fees collected this
month, fees due, 5 most recent notices, 5 upcoming exams.

### `GET /api/dashboard/parent.php?student_id=` *(parent)*
`student_id` optional — defaults to first linked child. Returns the
child's profile, this month's attendance summary, pending homework, latest
published result, fee due amount + next due date, recent notices.

---

## Parent

### `GET /api/parent/children.php` *(parent)*
Lists every student linked to the logged-in parent (for a "switch child"
selector — one parent account may have multiple siblings enrolled).

---

## Students

### `GET /api/students/list.php` *(admin/teacher)*
Query: `class_id`, `section_id`, `status` (default `active`), `search`,
`page`, `limit`.

### `GET /api/students/get.php?id=` *(auth)*
Admin/teacher: any student in their school. Parent: only their own linked
children (403 otherwise).

---

## Attendance

### `POST /api/attendance/mark.php` *(admin/teacher)*
Bulk class marking:
```json
{
  "class_id": 1, "section_id": 2, "attendance_date": "2026-08-09",
  "marked_via": "manual",
  "records": [ { "student_id": 10, "status": "present", "remarks": "" } ]
}
```
Single-scan (QR/RFID hardware or the Flutter app's scanner) shortcut:
```json
{ "qr_code": "STU5-42-A1B2C3D4", "status": "present" }
```
or
```json
{ "rfid_card_number": "04A2B3C4", "status": "present", "marked_via": "rfid" }
```
`status` is one of `present|absent|late|half_day`. Notifications are queued
to parents **only** for `absent`/`late` (never `present`), per spec.
Every `student_id` is re-validated to belong to the caller's school **and**
the given class/section before writing — non-matching records are silently
skipped, not written.

### `GET /api/attendance/get.php?student_id=&month=YYYY-MM` *(auth)*
Parent: own child only. Returns `records[]` + a `summary` count by status.

### `GET /api/attendance/get.php?class_id=&section_id=&date=YYYY-MM-DD` *(admin/teacher)*
Class-wide view for a single day.

---

## Homework

### `GET /api/homework/list.php` *(auth)*
- Parent: **requires** `student_id`, returns that child's class/section
  homework.
- Teacher: their own created homework.
- Admin: school-wide, optional `class_id` filter.

### `GET /api/homework/get.php?id=` *(auth)*
Includes `attachments[]` with `url`.

### `POST /api/homework/create.php` *(admin/teacher, multipart)*
Fields: `class_id`, `section_id`, `subject_id` (optional), `title`,
`description`, `due_date`, `teacher_id` (**required if role=admin**),
`attachments[]` (files — images and/or PDFs, multiple allowed).
`class_id`/`section_id`/`subject_id`/`teacher_id` are all verified to
belong to the caller's school before insert.

---

## Exams & Results

### `GET /api/exams/list.php?class_id=&student_id=` *(auth)*
Parent must pass `student_id` (class is inferred from it). Admin/teacher
may pass `class_id` to filter, or omit for all exams in the school.

### `GET /api/results/get.php?exam_id=&student_id=` *(auth)*
Subject-wise marks + max/passing marks + overall `summary`
(percentage/grade/class_rank) once published. `published: false` until the
admin publishes results for that exam.

### `POST /api/results/enter.php` *(admin/teacher, multipart for marksheet)*
```json
{ "exam_id": 5, "subject_id": 3, "student_id": 10, "marks_obtained": 78, "remarks": "" }
```
Or attach a photographed/scanned marksheet via the `marksheet` file field.
Both `exam_id` (school-owned) and `student_id` (school-owned) are verified
before write.

---

## Fees

### `GET /api/fees/status.php?student_id=` *(auth)*
All fee line items for the student + `total_due`.

### `GET /api/fees/receipts.php?student_id=` *(auth)*
Payment history (receipts) for the student.

### `GET /api/fees/due-list.php` *(admin/teacher)*
School-wide pending/overdue fees, optional `class_id` filter, paginated.

### `POST /api/fees/create-order.php` *(parent, multipart n/a — JSON)*
```json
{ "student_fee_id": 10 }
```
Creates a Razorpay Order for the outstanding balance on that fee (must
belong to one of the caller's own children). Returns:
```json
{ "order_id": "order_ABC123", "amount": 500000, "currency": "INR", "razorpay_key_id": "rzp_live_..." }
```
`amount` is in **paise** (Razorpay's unit), as expected by Razorpay
Checkout. Returns 503 if the school hasn't configured Razorpay yet
(Settings > Payment) — the app should hide/disable "Pay Online" in that case
and fall back to showing fee status only.

### `POST /api/fees/verify-payment.php` *(parent)*
```json
{ "student_fee_id": 10, "razorpay_order_id": "...", "razorpay_payment_id": "...", "razorpay_signature": "..." }
```
Call this immediately after Razorpay Checkout succeeds on the client, with
the three values Razorpay's SDK returns. The signature is verified
server-side (HMAC-SHA256 against the school's Razorpay secret) before the
payment is recorded — a forged/faked "success" from a compromised client
is rejected with a 400, never silently trusted. Safe to call twice for the
same `razorpay_payment_id` (returns success without double-recording).

---

## Notices

### `GET /api/notices/list.php` *(auth)*
Returns notices targeted at `all` plus the caller's own role audience
(`teachers` for teachers; `parents`/`students` for parents). Paginated.

### `GET /api/notices/get.php?id=` *(auth)*
Includes `attachments[]`. 403 if the notice isn't targeted at your role.

---

## Classes

### `GET /api/classes/list.php` *(auth)*
Every class in the school with nested `sections[]` and `subjects[]` —
used to populate dropdowns (e.g. homework creation) in the Flutter app.

---

## Institute

### `GET /api/institute/profile.php` *(auth)*
The school's public profile (name, logo, address, contact, principal,
academic year) for the "Institute Details" screen.

---

## Notifications

### `POST /api/notifications/register-token.php` *(auth)*
```json
{ "token": "fcm-device-token", "device_type": "android|ios|web" }
```
Call after login and whenever Firebase issues a refreshed token.

### `GET /api/notifications/list.php?page=&limit=` *(auth)*
In-app notification history for the logged-in user.

---

## Error codes

| Code | Meaning |
|---|---|
| 401 | Missing/invalid/expired token, or wrong credentials |
| 403 | Authenticated but not allowed to access this resource (wrong role or wrong tenant/child) |
| 404 | Resource not found (or, for tenant isolation, exists but not yours — same response, no leak) |
| 422 | Validation failed — see `data.errors` |
| 429 | Rate limited (login attempts) |
| 500 | Server error (message only, no stack trace unless `APP_DEBUG=true`) |

## Multi-tenancy & security

Every endpoint scopes queries by the `school_id` embedded in the JWT — it
is never trusted from the request body. Any foreign key referenced in a
write (student_id, class_id, teacher_id, exam_id, etc.) is re-validated to
belong to that same school before the write happens, so a compromised or
malicious client on School A's account cannot read or modify School B's
data even by guessing valid-looking IDs. See `SECURITY_NOTES.md` for the
audit trail.
