Recipes
SMS and WhatsApp to parents on absence
Text parents the moment their child is marked absent or late, through any SMS or WhatsApp gateway, using webhooks.
Goal
When a teacher saves a register, School360 sends student.absent (and student.late) to your endpoint, one event per child per day, with the family's phone numbers. Your endpoint checks the signature and asks your SMS or WhatsApp provider to send the message.
Setting it up
- 1Put the receiver below on a server with an https address (any small VPS or serverless function).
- 2In School Setup › Integrations › Webhooks, add an endpoint with that address and the events
student.absentandstudent.late. Copy the signing secret it shows into the receiver'sSCHOOL360_WEBHOOK_SECRET. - 3Press Send test event and check the delivery log shows a 2xx.
- 4Mark a test student absent in a register and watch the SMS arrive.
What arrives
family.phone is the family's primary contact; father, mother and guardian have each phone if you'd rather text both parents.
{
"id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
"event": "student.absent",
"createdAt": "2026-10-08T05:15:03.412Z",
"school": {
"code": "SCH00001",
"name": "Axiom School"
},
"data": {
"date": "2026-10-08",
"status": "absent",
"student": {
"id": 412,
"admissionNo": "ADM-2026-00412",
"firstName": "Ayesha",
"lastName": "Siddiqui",
"classId": 7,
"className": "Class 5",
"sectionId": 19,
"sectionName": "A",
"campusId": 1,
"rollNo": 14
},
"family": {
"id": 233,
"code": "FAM-00233",
"primaryContact": "father",
"father": {
"name": "Imran Siddiqui",
"phone": "0300-1234567"
},
"mother": {
"name": "Sana Imran",
"phone": "0321-7654321"
},
"guardian": null,
"phone": "0300-1234567"
}
}
}The receiver
Answer first, then send: School360 waits at most 10 seconds and retries anything that isn't a 2xx, so a slow gateway must not hold up the answer. Each delivery is remembered by its School360-Delivery id, so a retry or a Resend never texts a parent twice.
// sms-on-absence.mjs: a webhook endpoint for student.absent and student.late (Node 18+, Express)
import crypto from "node:crypto"
import express from "express"
const SECRET = process.env.SCHOOL360_WEBHOOK_SECRET // whsec_…
const GATEWAY = process.env.SMS_GATEWAY_URL // your SMS / WhatsApp provider's send address
const seen = new Set() // use a database table in production
const verified = (header = "", body) => {
const p = Object.fromEntries(header.split(",").map((x) => x.split("=", 2)))
const expected = crypto.createHmac("sha256", SECRET).update(`${p.t}.${body}`).digest("hex")
return p.v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(p.v1), Buffer.from(expected)) && Math.abs(Date.now() / 1000 - Number(p.t)) < 300
}
// 0300-1234567 → 923001234567
const msisdn = (phone) => phone.replace(/\D/g, "").replace(/^0/, "92")
const app = express()
app.post("/school360/attendance", express.raw({ type: "application/json" }), (req, res) => {
const body = req.body.toString("utf8")
if (!verified(req.get("School360-Signature"), body)) return res.status(400).end()
const delivery = req.get("School360-Delivery")
res.sendStatus(204) // answer within 10 s; send the SMS afterwards
if (seen.has(delivery)) return // a retry or a Resend: already texted
seen.add(delivery)
const { event, school, data } = JSON.parse(body)
if (!data.family?.phone) return
const name = `${data.student.firstName} ${data.student.lastName}`.trim()
const text =
event === "student.absent"
? `${name} (${data.student.className} ${data.student.sectionName}) is absent today, ${data.date}. Please inform the school if this is unexpected. — ${school.name}`
: `${name} arrived late today, ${data.date}. — ${school.name}`
fetch(GATEWAY, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SMS_GATEWAY_KEY}` },
body: JSON.stringify({ to: msisdn(data.family.phone), text }),
}).catch((err) => console.error("SMS failed", delivery, err.message))
})
app.listen(3000)<?php
// sms-on-absence.php: a webhook endpoint for student.absent and student.late
$secret = getenv('SCHOOL360_WEBHOOK_SECRET'); // whsec_…
$gateway = getenv('SMS_GATEWAY_URL'); // your SMS / WhatsApp provider's send address
$seenDir = __DIR__ . '/seen'; // one empty file per delivery handled
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_SCHOOL360_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
http_response_code(400);
exit;
}
// A retry or a Resend of something already texted: say yes and stop
$delivery = preg_replace('/[^a-f0-9-]/', '', $_SERVER['HTTP_SCHOOL360_DELIVERY'] ?? '');
@mkdir($seenDir);
if ($delivery === '' || !@fopen("$seenDir/$delivery", 'x')) {
http_response_code(204);
exit;
}
$event = json_decode($body, true);
$data = $event['data'];
$phone = preg_replace('/^0/', '92', preg_replace('/\D/', '', $data['family']['phone'] ?? ''));
$name = trim($data['student']['firstName'] . ' ' . $data['student']['lastName']);
$text = $event['event'] === 'student.absent'
? "$name ({$data['student']['className']} {$data['student']['sectionName']}) is absent today, {$data['date']}. — {$event['school']['name']}"
: "$name arrived late today, {$data['date']}. — {$event['school']['name']}";
// Answer School360 first (it waits at most 10 s), then call the gateway
http_response_code(204);
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
if ($phone !== '') {
$ch = curl_init($gateway);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('SMS_GATEWAY_KEY')],
CURLOPT_POSTFIELDS => json_encode(['to' => $phone, 'text' => $text]),
]);
curl_exec($ch);
curl_close($ch);
}Your gateway
The samples post { to, text } to a generic gateway address. Swap that call for your provider's API: local SMS aggregators take an HTTP request with the number and message; WhatsApp Business (Cloud API or a provider such as Twilio) needs an approved message template for messages the parent didn't start, with the child's name and date as template variables.
Numbers are stored as the school typed them (0300-1234567); most gateways want the international form (923001234567), which the samples make.
Pitfalls
- Check the signature on the raw body, before any JSON parsing or framework body-parser touches it.
- Dedupe by
School360-Delivery, not by student and date alone: a register corrected from late to absent sends a newstudent.absentevent. - School360 already sends at most one absent and one late event per child per day, but a Resend from the delivery log sends the same id again.
- Respect the school's hours: queue messages that arrive at night and send them in the morning, if your gateway allows scheduling.
- Phone numbers are personal data. Log delivery ids, not full payloads.