Complete Integration Documentation for School Management Systems
SIM800L CompatibleThe biometric device uses SIM800L GSM module which only supports HTTP protocol. HTTPS will not work.
Note: All attendance data is sent to this single endpoint via HTTP POST with JSON payload.
{
"sID": "2",
"name": "Philip Nganda",
"tIn": 1766383045,
"Adm": "ADM001",
"lt": 1,
"cl": "Staff",
"mcn": "a",
"apikey": "school123",
"school": "Your School Name"
}
{
"sID": "2",
"name": "Philip Nganda",
"tOut": 1766397445,
"Adm": "ADM001",
"lt": 4,
"cl": "Staff",
"mcn": "a",
"apikey": "school123",
"school": "Your School Name"
}
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
sID |
String | Required | Unique student/staff ID from biometric device | "101" |
name |
String | Required | Full name as stored in device | "John Doe" |
tIn |
Integer | Conditional | Check-in time (Unix timestamp seconds) | 1766383045 |
tOut |
Integer | Conditional | Check-out time (Unix timestamp seconds) | 1766397445 |
Adm |
String | Required | Admission/Employee number (use "_" for none) | "STU101" |
lt |
Integer | Required | Late/Early status (1-5 as explained above) | 3 |
cl |
String | Required | Class/Category/Department | "Form 1A", "Staff", "Grade 5" |
mcn |
String | Optional | Machine identifier (default "a") | "gate1" |
apikey |
String | Required | Your API key file name | "school123" |
school |
String | Required | School/Institution name | "Your School Name" |
sIDnametIntOutAdmltclmcnapikeyschooltIn or tOut must be present to indicate check-in or check-out.
Device requests student list:
{
"dwnld": "1",
"mcn": "a",
"apikey": "school123",
"school": "Your School Name"
}
Returns students in CSV-like format with * separators:
ID,FullName,Phone,AdmissionNo,ParentPhone,ExpectedIn,ExpectedOut*
Example Response:
1,John Doe,0727xxxxxx,STU101,0727xxxxxx,07:30,16:00* 2,Jane Smith,0727xxxxxx,STU102,optional,07:30,16:00* 101,Peter Johnson,0723222222,STU103,,07:30,16:00*^
* - Record separator^ - End of download (complete)} - Continue marker (more data available)ID_* - Suspended/Inactive studentThe nameslist uses a comma-separated CSV format with class/group headers embedded within the list:
1,staff 2025,,,,,* 2,Philip N,0727506940,4553,448,7:00,17:00* 3,Jason Mumo N,0727506940,4205,555,,* 4,_,,,,,* 5,Ruth Mueni,0727506940,22,,8:00,18:00* 6,Rodgers Kyanzu,0791226117,4567,0727506940,7:00,18:00* 7,Grace N,0727506940,333,,8:30,17:00* 8,Marlon Macharia,0727506940,11,0727506940,8:00,18:00* 9,,,,,,* 10,,,,,,* 11,,,,,,* 12,,,,,,* 13,,,,,,* 14,,,,,,* 15,,,,,,* 16,,,,,,* 17,,,,,,* 18,,,,,,* 19,,,,,,* 20,Form 1s,,,,,* 21,Suleiman Ogollah Otieno,,,,,* 22,Evans Ogola,,,,,* 23,Jane Mboya,,,,,* 24,Moses Odhiambo Otieno,0727506940,1195,103074847,6:00,17:00* 25,Nancy Kavai,0727506940,1525,103074847,7:00,18:00* 26,Vincent Onyango,0727506940,184,103074847,6:00,17:00* 27,Lilian Simiyu,0727506940,1546,103074847,6:00,17:00* 28,Zainab Mohamed,0727506940,134,103074847,6:00,17:00*^
| Column | Content | Example |
|---|---|---|
| 1 | ID Number | 1, 2, 20, 21 |
| 2 | Name/Category | "staff 2025", "Form 1s", "Philie N" |
| 3 | Phone Number | 0727506940 replace with "'0000" if not needed |
| 4 | Admission No | 4205 |
| 5 | Parent Phone 2 | optional |
| 6 | Expected In | 7:00 (optional) |
| 7 | Expected Out | 17:00 |
| End | Record Separator | * (required for each line) |
_ = inactive* per line_ to remove (or empty name slot)* terminator on every line
// Generate comma-separated CSV
foreach ($students as $student) {
$line = $student['id'] . ',' .
$student['name'] . ',' .
$student['phone'] . ',' .
$student['admission_no'] . ',' .
$student['parent_phone'] . ',' .
$student['expected_in'] . ',' .
$student['expected_out'] . '*';
echo $line;
}
When students are deleted from your system, notify the device:
{
"dlt": "1-5,7,9-11",
"mcn": "a",
"apikey": "school123",
"school": "Your School Name"
}
| Format | Meaning | Example IDs |
|---|---|---|
"1-5" |
Delete IDs 1 through 5 | 1, 2, 3, 4, 5 |
"7" |
Delete single ID 7 | 7 |
"9-11" |
Delete IDs 9 through 11 | 9, 10, 11 |
"1-5,7,9-11" |
Combination of ranges | 1,2,3,4,5,7,9,10,11 |
After processing deletions, the device will automatically request the updated student list. Deleted students are marked with underscores in their fields (soft delete).
// Parse deletion ranges
function parseDeletionRanges($rangeString) {
$deletedIds = [];
$ranges = explode(',', $rangeString);
foreach ($ranges as $range) {
if (strpos($range, '-') !== false) {
list($start, $end) = explode('-', $range);
$deletedIds = array_merge($deletedIds, range($start, $end));
} else {
$deletedIds[] = (int)$range;
}
}
return array_unique($deletedIds);
}
// Process deletions
$deletedIds = parseDeletionRanges($data['dlt']);
foreach ($deletedIds as $id) {
// Soft delete in your database
$stmt = $pdo->prepare("
UPDATE students
SET status = 'deleted',
deleted_at = NOW()
WHERE student_id = ?
");
$stmt->execute([$id]);
}
{
"gen": "2",
"apikey": "school123",
"school": "Your School Name"
}
Response Format: dd~mm~yyyy~hh~mm~ss
Example: 22~12~2024~14~30~45
{
"gen": "3",
"apikey": "school123",
"school": "Your School Name"
}
Response: kauwi:ins
{
"dwnld": "2",
"mcn": "a",
"apikey": "school123",
"school": "Your School Name"
}
StudentName,StudentPhone,ParentPhone,Eng:A,Kis:B,Maths:C,...*
Eng - EnglishKis - KiswahiliMaths - MathematicsBio - BiologyPhy - PhysicsChem - ChemistryMEAN - Average Grade{
"dwnld": "3",
"mcn": "a",
"apikey": "school123",
"school": "Your School Name"
}
Day,Time1,Time2,...,Time22*
Example: Mon,08:00,09:00,10:00,...,16:00*
<?php
// Configure API
define('API_ENDPOINT', 'http://your-domain.com/attendance/post_attendance');
define('API_KEY', 'school123');
define('SCHOOL_NAME', 'Your School Name');
class BiometricIntegration {
/**
* Process attendance data from biometric device
*/
public function processAttendance($data) {
// Validate API key
if (!isset($data['apikey']) || $data['apikey'] !== API_KEY) {
return ['error' => 'Invalid API key'];
}
// Extract data
$studentId = $data['sID'] ?? '';
$name = $data['name'] ?? '';
$admission = ($data['Adm'] === '_') ? null : $data['Adm'];
$category = $data['cl'] ?? '';
$lateStatus = (int)($data['lt'] ?? 5);
$machineId = $data['mcn'] ?? 'a';
// Determine operation
$isCheckIn = isset($data['tIn']);
$timestamp = $isCheckIn ? (int)$data['tIn'] : (int)$data['tOut'];
// Convert timestamp
$dateTime = date('Y-m-d H:i:s', $timestamp);
$date = date('Y-m-d', $timestamp);
// Process based on operation type
if ($isCheckIn) {
return $this->handleCheckIn($studentId, $name, $admission,
$category, $lateStatus, $machineId,
$dateTime, $date);
} else {
return $this->handleCheckOut($studentId, $dateTime, $date);
}
}
/**
* Handle check-in operation
*/
private function handleCheckIn($studentId, $name, $admission, $category,
$lateStatus, $machineId, $dateTime, $date) {
// Prevent duplicate check-ins within 1 minute
$stmt = $this->pdo->prepare("
SELECT id FROM attendance
WHERE student_id = ?
AND attendance_date = ?
AND check_in_time >= DATE_SUB(?, INTERVAL 1 MINUTE)
");
$stmt->execute([$studentId, $date, $dateTime]);
if ($stmt->rowCount() > 0) {
return ['success' => true, 'message' => 'Duplicate prevented'];
}
// Insert new check-in
$stmt = $this->pdo->prepare("
INSERT INTO attendance
(student_id, student_name, admission_no, class_category,
check_in_time, attendance_date, late_status, machine_id, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'present')
");
$stmt->execute([
$studentId, $name, $admission, $category,
$dateTime, $date, $lateStatus, $machineId
]);
return ['success' => true, 'id' => $this->pdo->lastInsertId()];
}
/**
* Handle check-out operation
*/
private function handleCheckOut($studentId, $dateTime, $date) {
$stmt = $this->pdo->prepare("
UPDATE attendance
SET check_out_time = ?, status = 'completed'
WHERE student_id = ?
AND attendance_date = ?
AND check_out_time IS NULL
ORDER BY id DESC
LIMIT 1
");
$stmt->execute([$dateTime, $studentId, $date]);
return ['success' => true, 'updated' => $stmt->rowCount()];
}
/**
* Handle student list download
*/
public function handleDownload($data) {
// Get all active students
$stmt = $this->pdo->prepare("
SELECT
id,
CONCAT(first_name, ' ', COALESCE(middle_name, ''), ' ', last_name) as name,
phone,
admission_no,
parent_phone,
expected_in_time,
expected_out_time,
status
FROM students
WHERE status = 'active'
ORDER BY id ASC
");
$stmt->execute();
$students = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Format response
$response = '';
$counter = 0;
$outputLength = 0;
foreach ($students as $student) {
$counter++;
if ($student['status'] !== 'active') {
$response .= $student['id'] . '_*';
continue;
}
$row = sprintf('%s,%s,%s,%s,%s,%s,%s*',
$student['id'],
$student['name'],
$student['phone'] ?? '',
$student['admission_no'] ?? '',
$student['parent_phone'] ?? '',
$student['expected_in_time'] ?? '07:30',
$student['expected_out_time'] ?? '16:00'
);
$response .= $row;
$outputLength += strlen($row);
// Batch processing: Send in chunks
if ($outputLength > 20000) { // 20KB
$response .= '}';
break;
}
}
if ($outputLength <= 20000) {
$response .= '^';
}
return $response;
}
/**
* Handle student deletions
*/
public function handleDeletion($rangeString) {
$deletedIds = $this->parseRange($rangeString);
foreach ($deletedIds as $id) {
// Soft delete
$stmt = $this->pdo->prepare("
UPDATE students
SET status = 'deleted',
deleted_at = NOW()
WHERE id = ?
");
$stmt->execute([$id]);
}
return count($deletedIds) . ' students deleted';
}
/**
* Parse range string (e.g., "1-5,7,9-11")
*/
private function parseRange($rangeString) {
$ids = [];
$ranges = explode(',', $rangeString);
foreach ($ranges as $range) {
if (strpos($range, '-') !== false) {
list($start, $end) = explode('-', $range);
$ids = array_merge($ids, range($start, $end));
} else {
$ids[] = (int)$range;
}
}
return array_unique($ids);
}
}
// Usage example
$integration = new BiometricIntegration();
// Get JSON input
$json = file_get_contents('php://input');
$data = json_decode($json, true);
// Route the request
if (isset($data['sID']) || isset($data['tIn']) || isset($data['tOut'])) {
$result = $integration->processAttendance($data);
} elseif (isset($data['dwnld'])) {
$result = $integration->handleDownload($data);
} elseif (isset($data['dlt'])) {
$result = $integration->handleDeletion($data['dlt']);
} else {
$result = ['error' => 'Invalid request'];
}
// Send response (device expects simple responses)
echo is_string($result) ? $result : '1';
?>
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json());
// Database configuration
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'school',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Main endpoint
app.post('/api/attendance', async (req, res) => {
try {
const data = req.body;
// Validate API key
if (data.apikey !== process.env.API_KEY) {
return res.status(401).send('Unauthorized');
}
// Route based on request type
if (data.sID || data.tIn || data.tOut) {
await handleAttendance(data);
res.send('1');
} else if (data.dwnld) {
const result = await handleDownload(data);
res.send(result);
} else if (data.dlt) {
await handleDeletion(data.dlt);
res.send('1');
} else if (data.gen === '2') {
// Return current datetime
const now = new Date();
const response = `${now.getDate()}~${now.getMonth()+1}~${now.getFullYear()}~${now.getHours()}~${now.getMinutes()}~${now.getSeconds()}`;
res.send(response);
} else if (data.gen === '3') {
res.send('kauwi:ins');
} else {
res.status(400).send('Invalid request');
}
} catch (error) {
console.error('Error:', error);
res.status(500).send('1'); // Still respond to prevent device timeout
}
});
async function handleAttendance(data) {
const connection = await pool.getConnection();
try {
const studentId = data.sID;
const name = data.name || '';
const admission = data.Adm === '_' ? null : data.Adm;
const category = data.cl || '';
const lateStatus = parseInt(data.lt) || 5;
const machineId = data.mcn || 'a';
// Determine operation
const isCheckIn = 'tIn' in data;
const timestamp = isCheckIn ? parseInt(data.tIn) : parseInt(data.tOut);
// Convert timestamp
const dateTime = new Date(timestamp * 1000);
const dateStr = dateTime.toISOString().split('T')[0];
const timeStr = dateTime.toISOString().split('T')[1].split('.')[0];
if (isCheckIn) {
// Check for duplicate
const [existing] = await connection.execute(`
SELECT id FROM attendance
WHERE student_id = ?
AND DATE(check_in_time) = ?
AND TIMESTAMPDIFF(MINUTE, check_in_time, ?) BETWEEN -1 AND 1
`, [studentId, dateStr, dateTime]);
if (existing.length > 0) {
return; // Duplicate, don't insert
}
// Insert check-in
await connection.execute(`
INSERT INTO attendance
(student_id, student_name, admission_no, class_category,
check_in_time, attendance_date, late_status, machine_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, [studentId, name, admission, category,
dateTime, dateStr, lateStatus, machineId]);
} else {
// Update check-out
await connection.execute(`
UPDATE attendance
SET check_out_time = ?
WHERE student_id = ?
AND DATE(attendance_date) = ?
AND check_out_time IS NULL
ORDER BY id DESC
LIMIT 1
`, [dateTime, studentId, dateStr]);
}
} finally {
connection.release();
}
}
async function handleDownload(data) {
const connection = await pool.getConnection();
try {
const [students] = await connection.execute(`
SELECT
id,
CONCAT(first_name, ' ', COALESCE(middle_name, ''), ' ', last_name) as name,
phone,
admission_no,
parent_phone,
expected_in_time,
expected_out_time,
status
FROM students
WHERE status = 'active'
ORDER BY id ASC
`);
let response = '';
let outputLength = 0;
for (const student of students) {
if (student.status !== 'active') {
response += `${student.id}_*`;
continue;
}
const row = `${student.id},${student.name},${student.phone || ''},${student.admission_no || ''},${student.parent_phone || ''},${student.expected_in_time || '07:30'},${student.expected_out_time || '16:00'}*`;
response += row;
outputLength += row.length;
if (outputLength > 20000) {
response += '}';
break;
}
}
if (outputLength <= 20000) {
response += '^';
}
return response;
} finally {
connection.release();
}
}
async function handleDeletion(rangeString) {
const ids = parseRange(rangeString);
const connection = await pool.getConnection();
try {
await connection.execute(`
UPDATE students
SET status = 'deleted',
deleted_at = NOW()
WHERE id IN (?)
`, [ids]);
} finally {
connection.release();
}
}
function parseRange(rangeString) {
const ids = new Set();
const ranges = rangeString.split(',');
for (const range of ranges) {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
ids.add(i);
}
} else {
ids.add(Number(range));
}
}
return Array.from(ids);
}
app.listen(3000, () => {
console.log('Server running on port 3000');
});
from flask import Flask, request, jsonify
import mysql.connector
from datetime import datetime
import json
import os
app = Flask(__name__)
# Configuration
API_KEY = os.getenv('BIOMETRIC_API_KEY', 'school123')
def get_db_connection():
return mysql.connector.connect(
host='localhost',
user='root',
password='password',
database='school'
)
@app.route('/attendance/post_attendance', methods=['POST'])
def handle_attendance():
try:
data = request.get_json()
# Validate API key
if data.get('apikey') != API_KEY:
return 'Unauthorized', 401
# Route based on request type
if 'sID' in data or 'tIn' in data or 'tOut' in data:
handle_attendance_data(data)
return '1'
elif 'dwnld' in data:
result = handle_download(data)
return result
elif 'dlt' in data:
handle_deletion(data['dlt'])
return '1'
elif data.get('gen') == '2':
# Return current datetime
now = datetime.now()
response = f"{now.day}~{now.month}~{now.year}~{now.hour}~{now.minute}~{now.second}"
return response
elif data.get('gen') == '3':
return 'kauwi:ins'
else:
return 'Invalid request', 400
except Exception as e:
print(f"Error: {e}")
return '1' # Always respond to prevent device timeout
def handle_attendance_data(data):
conn = get_db_connection()
cursor = conn.cursor()
try:
student_id = data.get('sID')
name = data.get('name', '')
admission = None if data.get('Adm') == '_' else data.get('Adm')
category = data.get('cl', '')
late_status = int(data.get('lt', 5))
machine_id = data.get('mcn', 'a')
# Determine operation
is_check_in = 'tIn' in data
timestamp = int(data.get('tIn' if is_check_in else 'tOut'))
# Convert timestamp
dt = datetime.fromtimestamp(timestamp)
date_str = dt.strftime('%Y-%m-%d')
time_str = dt.strftime('%Y-%m-%d %H:%M:%S')
if is_check_in:
# Check for duplicate
cursor.execute("""
SELECT id FROM attendance
WHERE student_id = %s
AND attendance_date = %s
AND TIMESTAMPDIFF(MINUTE, check_in_time, %s) BETWEEN -1 AND 1
""", (student_id, date_str, time_str))
if cursor.fetchone():
return # Duplicate found
# Insert check-in
cursor.execute("""
INSERT INTO attendance
(student_id, student_name, admission_no, class_category,
check_in_time, attendance_date, late_status, machine_id)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""", (student_id, name, admission, category,
time_str, date_str, late_status, machine_id))
else:
# Update check-out
cursor.execute("""
UPDATE attendance
SET check_out_time = %s
WHERE student_id = %s
AND attendance_date = %s
AND check_out_time IS NULL
ORDER BY id DESC
LIMIT 1
""", (time_str, student_id, date_str))
conn.commit()
finally:
cursor.close()
conn.close()
def handle_download(data):
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
try:
cursor.execute("""
SELECT
id,
CONCAT(first_name, ' ', COALESCE(middle_name, ''), ' ', last_name) as name,
phone,
admission_no,
parent_phone,
expected_in_time,
expected_out_time,
status
FROM students
WHERE status = 'active'
ORDER BY id ASC
""")
response = ''
output_length = 0
for student in cursor.fetchall():
if student['status'] != 'active':
response += f"{student['id']}_*"
continue
row = f"{student['id']},{student['name']},{student['phone'] or ''},{student['admission_no'] or ''},{student['parent_phone'] or ''},{student['expected_in_time'] or '07:30'},{student['expected_out_time'] or '16:00'}*"
response += row
output_length += len(row)
if output_length > 20000:
response += '}'
break
if output_length <= 20000:
response += '^'
return response
finally:
cursor.close()
conn.close()
def handle_deletion(range_string):
ids = parse_range(range_string)
conn = get_db_connection()
cursor = conn.cursor()
try:
placeholders = ','.join(['%s'] * len(ids))
cursor.execute(f"""
UPDATE students
SET status = 'deleted',
deleted_at = NOW()
WHERE id IN ({placeholders})
""", ids)
conn.commit()
finally:
cursor.close()
conn.close()
def parse_range(range_string):
ids = set()
ranges = range_string.split(',')
for r in ranges:
if '-' in r:
start, end = map(int, r.split('-'))
ids.update(range(start, end + 1))
else:
ids.add(int(r))
return list(ids)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
| Issue | Symptoms | Solution |
|---|---|---|
| No Data Received | Empty POST body, no errors in logs | Check php://input or request body parsing |
| Device Timeout | Device retries same request | Optimize queries, add database indexes |
| Wrong Timestamps | Incorrect dates/times stored | Verify Unix timestamp (seconds, not milliseconds) |
| Partial Downloads | Only some students downloaded | Check chunk size and continuation logic |
| Deletion Issues | Students not marked as deleted | Verify range parsing logic |
| Duplicate Records | Same student checked in multiple times | Implement 1-minute duplicate check window |
If the biometric device stops sending data: