Portal SDK & PHP Kit
Build custom parent/student portals with the OptiCRM Portal Auth API and PHP Portal Kit
Overview
OptiCRM provides two ways for parents and students to access their data:
- Built-in Portal — the
/portal/*pages inside the Next.js app (no setup needed) - External Portal via PHP Kit — a separate PHP MVC application that connects to OptiCRM via REST API
The PHP Portal Kit is for institutions or implementors who want a fully customizable portal without touching the OptiCRM core codebase. Any PHP developer can modify the templates, add pages, and change the look — all powered by OptiCRM's data through the Portal Auth API.
Architecture
┌──────────────────────────┐ ┌────────────────────────┐
│ PHP Portal Kit │ │ OptiCRM Core │
│ (Implementor's code) │─────▶│ (Your closed code) │
│ │ REST │ │
│ portal.opticrm.app │ API │ school.opticrm.app │
└──────────────────────────┘ └────────────────────────┘
↑ ↑
Parent/Student Staff/Admin
browser browser- The PHP app handles all presentation (HTML, CSS, templates)
- OptiCRM handles all data, auth, and business logic
- API tokens stay server-side — never exposed to the browser
- One PHP deployment serves all institutions (multi-tenant)
Portal Auth API
The foundation for all external portal integrations. Returns a JWT token that grants access to all portal data endpoints.
Login
POST https://{slug}.opticrm.app/api/portal/auth
Content-Type: application/json{
"slug": "rashtrothana",
"email": "parent@example.com",
"password": "their-password"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"expiresIn": "24h",
"contact": {
"id": "clx...",
"firstName": "Rajesh",
"lastName": "Sharma",
"email": "parent@example.com",
"contactType": "CONTACT_PARENT"
},
"institution": {
"id": "clx...",
"name": "Rashtrothana Vidhya Kendra",
"slug": "rashtrothana"
},
"students": [
{
"id": "clx...",
"firstName": "Aarav",
"lastName": "Sharma",
"studentCode": "STU001",
"relation": "FATHER"
}
]
}Key fields:
token— JWT valid for 24 hours. Store server-side, never in browser.students— linked children. Usestudents[].idasstudentIdin data calls.contactType—CONTACT_PARENTorCONTACT_STUDENT
Verify Token
GET https://{slug}.opticrm.app/api/portal/auth
Authorization: Bearer {token}Returns contact info if token is valid, 401 if expired or invalid.
Data Endpoints
All endpoints require Authorization: Bearer {token} header. Most require ?studentId={id}.
| Endpoint | Method | Description |
|---|---|---|
/api/portal/my-children | GET | Linked students with class/section |
/api/portal/fees?studentId=xxx | GET | Invoices, amounts, payment status |
/api/portal/attendance?studentId=xxx | GET | Attendance records |
/api/portal/timetable?studentId=xxx | GET | Weekly timetable grid |
/api/portal/results?studentId=xxx | GET | Exam results, marks, grades |
/api/portal/assignments?studentId=xxx | GET | Assignments with due dates |
/api/portal/transport?studentId=xxx | GET | Route, vehicle, driver info |
/api/portal/health?studentId=xxx | GET | Health profile, screenings, vaccinations |
/api/portal/events | GET | Upcoming school events |
/api/portal/grievances | GET/POST | Submit or view grievances |
/api/portal/profile | GET/PATCH | View or update contact profile |
/api/portal/change-password | POST | Change portal password |
/api/portal/analytics | GET | Summary statistics |
/api/portal/announcements | GET | School announcements |
/api/portal/appointments | GET | PTM appointment slots |
/api/portal/consent | GET/POST | Consent form submissions |
/api/portal/payments | GET | Payment history |
Access Control
- Parents can only access data for their linked students (via
ContactStudentLink) - Students can only access their own data
- Accessing another student's data returns
403 Access Denied - The
studentsarray in the login response lists all accessible student IDs
Money Values
All amounts are stored in paise (1 rupee = 100 paise). Divide by 100 for display:
625000= ₹6,250.002500000= ₹25,000.00
PHP Portal Kit
Repository
github.com/hemanthopti/opticrm-portalRequirements
- PHP 8.0+ with curl extension
- Any web server (Apache, Nginx) or PHP built-in server
- No Composer, no framework dependencies
Quick Start
git clone https://github.com/hemanthopti/opticrm-portal.git
cd opticrm-portal
cp .env.example .env
# Edit .env — set OPTICRM_DOMAIN=opticrm.app
php -S localhost:8080 -t public
# Open http://localhost:8080Project Structure
opticrm-portal/
├── public/
│ ├── index.php ← Front controller (routes all requests)
│ └── .htaccess ← Apache URL rewriting
├── app/
│ ├── config.php ← Loads .env, defines constants
│ ├── Router.php ← Maps URLs to controllers
│ ├── Controller.php ← Base class (render, redirect, SDK)
│ ├── Middleware.php ← Session auth check
│ ├── models/
│ │ └── OptiCRM.php ← SDK (API client, single file)
│ ├── controllers/
│ │ ├── AuthController.php
│ │ ├── DashboardController.php
│ │ ├── FeesController.php
│ │ ├── AttendanceController.php
│ │ ├── TimetableController.php
│ │ ├── ResultsController.php
│ │ ├── ProfileController.php
│ │ ├── GrievanceController.php
│ │ └── AnnouncementController.php
│ └── views/
│ ├── layout.php ← Base layout (header + sidebar + content)
│ ├── dashboard.php
│ ├── fees.php
│ ├── attendance.php
│ └── partials/
│ ├── header.php
│ ├── sidebar.php
│ └── student-picker.php
├── .env.example
├── Dockerfile ← For Docker/Coolify deployment
└── README.mdConfiguration
.env file:
OPTICRM_DOMAIN=opticrm.app
APP_NAME=OptiCRM Portal
APP_PRIMARY_COLOR=#2563ebThe portal is multi-tenant — the institution slug is entered at login, not configured in .env. One deployment serves all institutions.
How It Works
- Parent visits
portal.opticrm.app/login - Enters institution slug (e.g.,
rashtrothana), email, and password - PHP calls
POST https://rashtrothana.opticrm.app/api/portal/authserver-side - JWT token stored in PHP session (never sent to browser)
- All subsequent pages call OptiCRM API with the token
- PHP renders HTML using Tailwind CSS templates
- On logout, session is destroyed
The SDK (OptiCRM.php)
One file, zero dependencies. Available in every controller as $this->crm.
// Authentication
$result = $this->crm->login($email, $password);
$valid = $this->crm->verify();
// Student data
$fees = $this->crm->getFees($studentId);
$attendance = $this->crm->getAttendance($studentId);
$timetable = $this->crm->getTimetable($studentId);
$results = $this->crm->getResults($studentId);
$transport = $this->crm->getTransport($studentId);
$health = $this->crm->getHealth($studentId);
// Portal actions
$this->crm->submitGrievance(['title' => '...', 'description' => '...']);
$this->crm->changePassword($currentPassword, $newPassword);
$this->crm->updateProfile(['mobile' => '9876543210']);
// OptiQL queries
$result = $this->crm->query("SELECT firstName, lastName FROM Student LIMIT 10");
// Generic API calls
$data = $this->crm->get('/api/portal/any-endpoint', ['param' => 'value']);
$result = $this->crm->post('/api/portal/any-endpoint', ['key' => 'value']);Customization Guide
Change the Look
Edit the view files in app/views/. They're plain PHP + HTML with Tailwind CSS.
layout.php— overall page structure (header, sidebar, content area)partials/header.php— top navigation barpartials/sidebar.php— left sidebar menu- Individual page files —
fees.php,attendance.php, etc.
Replace <script src="https://cdn.tailwindcss.com"> with your own CSS framework (Bootstrap, custom CSS, etc.).
Add a New Page
Create a Controller
Create app/controllers/MyPageController.php:
<?php
class MyPageController extends Controller
{
public function index(): void
{
// Fetch data from OptiCRM
$result = $this->crm->query(
"SELECT className, COUNT(id) as count FROM Student GROUP BY className"
);
$this->render('my-page', [
'classes' => $result['data']['rows'] ?? [],
]);
}
}Create a View
Create app/views/my-page.php:
<h2 class="text-xl font-bold mb-4">Students by Class</h2>
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50">
<th class="text-left px-4 py-2">Class</th>
<th class="text-right px-4 py-2">Count</th>
</tr>
</thead>
<tbody>
<?php foreach ($classes as $c): ?>
<tr class="border-b">
<td class="px-4 py-2"><?= htmlspecialchars($c['className']) ?></td>
<td class="px-4 py-2 text-right"><?= $c['count'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>Register the Route
In public/index.php, add:
$router->get('/my-page', 'MyPageController', 'index');Add to Sidebar
In app/views/partials/sidebar.php, add to the $navItems array:
['label' => 'My Page', 'href' => '/my-page', 'icon' => '📊'],Create a Custom Form
Forms POST to a controller which calls the OptiCRM API:
// In the view (my-form.php):
<form method="POST" action="/my-form">
<input type="text" name="title" placeholder="Title" required>
<textarea name="description" placeholder="Description"></textarea>
<button type="submit">Submit</button>
</form>
// In the controller:
public function submit(): void
{
$result = $this->crm->post('/api/portal/grievances', [
'title' => $_POST['title'],
'description' => $_POST['description'],
]);
if (isset($result['error'])) {
$_SESSION['flash_error'] = $result['error'];
} else {
$_SESSION['flash_success'] = 'Submitted successfully!';
}
$this->redirect('/my-form');
}Use OptiQL for Custom Reports
// Any controller can run OptiQL queries
$fees = $this->crm->query("
SELECT feeHeadName, SUM(paidAmount) as collected, COUNT(id) as count
FROM Invoice
WHERE status IN ('PAID', 'PARTIAL')
GROUP BY feeHeadName
ORDER BY collected DESC
");
$overdue = $this->crm->query("
SELECT invoiceNo, studentName, totalAmount, dueDate
FROM Invoice
WHERE status = 'OVERDUE' AND totalAmount > 1000000
ORDER BY totalAmount DESC
LIMIT 20
");Deployment
Docker / Coolify
The repo includes a Dockerfile. In Coolify:
- Add new resource > connect to
hemanthopti/opticrm-portal - Build method: Dockerfile
- Port:
80 - Domain:
portal.opticrm.app - Environment variable:
OPTICRM_DOMAIN=opticrm.app - Deploy
Shared Hosting (cPanel)
- Upload files to your hosting
- Point document root to the
public/folder - Create
.envfile withOPTICRM_DOMAIN=opticrm.app - Ensure mod_rewrite is enabled (Apache)
Nginx
server {
listen 80;
server_name portal.yourschool.com;
root /var/www/opticrm-portal/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}Security
| Concern | How It's Handled |
|---|---|
| API token exposure | Token stored in PHP server-side session, never sent to browser |
| XSS | All output uses htmlspecialchars() |
| CSRF | Session-based (implement token in forms for production) |
| SQL injection | No direct database access — all data via API |
| Password storage | Passwords never stored locally — auth delegated to OptiCRM |
| HTTPS | Required in production for both portal and API |
Integration with Other Apps
The Portal Auth API isn't limited to the PHP kit. Any application can use it:
- Mobile apps — call the API from iOS/Android
- OptiLearn LMS — authenticate students against OptiCRM
- Custom dashboards — build React/Vue apps that consume the API
- Chatbots — authenticate parents and fetch student data
The JWT token works with all 18 portal endpoints. See the complete endpoint reference in the table above.