Webhooks

Events, pushed to you

Instead of asking School360 every few minutes, let it call you: a student marked absent, a fee payment received, vouchers issued, results published. Each event is a signed HTTPS POST with JSON, retried for two days if your system is down.

Adding an endpoint

Webhooks come with the API (the Enterprise package). The school's owner or admin adds endpoints in School Setup › Integrations › Webhooks: the address (https), a description, the events it should get and, optionally, one campus. School360 then shows the endpoint's signing secret (whsec_…) — keep it like a password; it can be revealed again there or rolled (replaced) at any time. A school can have up to 20 endpoints.

What is sent

Every event is a POST with Content-Type: application/json and this body. data depends on the event (below); its field names match the API's resources, and new fields may be added (never removed or renamed), so ignore what you don't know. Bodies are at most 256 KB.

json
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "webhook.test",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "message": "This is a test event from School360.",
    "endpointId": 12
  }
}

Headers sent with each event:

School360-Event
the event name, e.g. student.absent
School360-Delivery
the event's id (the same as id in the body); the same for retries and resends
School360-Signature
t=<unix time>,v1=<signature> (see Checking the signature)
User-Agent
School360-Webhooks/1.0

Events

Pick the events an endpoint gets when you add it. An endpoint limited to a campus only gets events about that campus (batch events list only that campus's vouchers or classes).

Students

student.admitted

A new student is admitted: on the student form, by enrolling an application (in the portal, on School360 Desktop or through POST /applications/{id}/enrol). With the family's phone numbers.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "student.admitted",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "student": {
      "id": 412,
      "admissionNo": "ADM-2026-00412",
      "firstName": "Ayesha",
      "lastName": "Siddiqui",
      "classId": 7,
      "className": "Class 5",
      "sectionId": 19,
      "sectionName": "A",
      "campusId": 1,
      "rollNo": 14,
      "gender": "female",
      "dob": "2016-03-14",
      "status": "active",
      "admissionDate": "2026-10-08",
      "cardNo": "0004417365",
      "updatedAt": "2026-10-08T05:15:03.000Z"
    },
    "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"
    },
    "applicationId": 88
  }
}

student.updated

A student's details, class, section or status (other than leaving) changed, in the portal or through PATCH /students/{id}. Coalesced: at most one event per student every 5 minutes, sent at the end of those 5 minutes with the student as they are then.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "student.updated",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "student": {
      "id": 412,
      "admissionNo": "ADM-2026-00412",
      "firstName": "Ayesha",
      "lastName": "Siddiqui",
      "classId": 7,
      "className": "Class 5",
      "sectionId": 19,
      "sectionName": "A",
      "campusId": 1,
      "rollNo": 14,
      "gender": "female",
      "dob": "2016-03-14",
      "status": "active",
      "admissionDate": "2026-04-01",
      "cardNo": "0004417365",
      "updatedAt": "2026-10-08T05:15:03.000Z"
    }
  }
}

student.left

A student is marked left or withdrawn, or a leaving certificate marks them left.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "student.left",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "student": {
      "id": 412,
      "admissionNo": "ADM-2026-00412",
      "firstName": "Ayesha",
      "lastName": "Siddiqui",
      "classId": 7,
      "className": "Class 5",
      "sectionId": 19,
      "sectionName": "A",
      "campusId": 1,
      "rollNo": null,
      "gender": "female",
      "dob": "2016-03-14",
      "status": "left",
      "admissionDate": "2023-04-03",
      "cardNo": null,
      "updatedAt": "2026-10-08T05:15:03.000Z"
    },
    "status": "left",
    "note": "Moved to another city"
  }
}

student.promoted

The year-end promotion was run: one event for the whole school, with the sessions and how many were promoted, repeated, graduated, left and admitted. Read the students again (GET /students?updatedSince=) for their new classes.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "student.promoted",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "id": 4,
    "fromYear": {
      "id": 3,
      "name": "2026-27"
    },
    "toYear": {
      "id": 4,
      "name": "2027-28"
    },
    "counts": {
      "promoted": 412,
      "repeated": 6,
      "graduated": 38,
      "left": 9,
      "admitted": 57
    },
    "runAt": "2027-03-31T10:02:11.000Z"
  }
}

Admissions

application.received

A new admission application: taken in the portal, sent from the school's website, taken on School360 Desktop or through POST /applications. With the parents' phones for a quick follow-up.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "application.received",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "id": 88,
    "number": "ADM-2026-0088",
    "status": "inquiry",
    "yearId": 3,
    "classId": 2,
    "className": "Prep",
    "campusId": 1,
    "firstName": "Hamza",
    "lastName": "Siddiqui",
    "gender": "male",
    "dob": "2021-05-02",
    "source": "website",
    "appliedOn": "2026-10-08",
    "father": {
      "name": "Imran Siddiqui",
      "phone": "0300-1234567",
      "email": "imran@example.com"
    },
    "mother": {
      "name": "Sana Imran",
      "phone": "0321-7654321"
    },
    "familyId": 233,
    "studentId": null
  }
}

application.status_changed

An application moves on: form received (applied), test scheduled, offered (approved), refused (rejected), reopened or enrolled. previousStatus says where it was.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "application.status_changed",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "application": {
      "id": 88,
      "number": "ADM-2026-0088",
      "status": "approved",
      "yearId": 3,
      "classId": 2,
      "className": "Prep",
      "campusId": 1,
      "firstName": "Hamza",
      "lastName": "Siddiqui",
      "gender": "male",
      "dob": "2021-05-02",
      "source": "website",
      "appliedOn": "2026-10-08",
      "father": {
        "name": "Imran Siddiqui",
        "phone": "0300-1234567",
        "email": "imran@example.com"
      },
      "mother": {
        "name": "Sana Imran",
        "phone": "0321-7654321"
      },
      "familyId": 233,
      "studentId": null
    },
    "previousStatus": "applied",
    "status": "approved"
  }
}

Attendance

student.absent

A student is marked absent in a register (in the portal or on School360 Desktop). One event per student per day, with the family's phone numbers for SMS or WhatsApp.

Example
{
  "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"
    }
  }
}

student.late

A student is marked late in a register, or arrives after the late time by a biometric punch. One event per student per day, with the family's phone numbers.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "student.late",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "date": "2026-10-08",
    "status": "late",
    "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"
    }
  }
}

attendance.register_saved

A section's register is marked or corrected (in the portal, on School360 Desktop or through POST /attendance/registers), with each exception and the day's counts. Biometric punches don't send it.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "attendance.register_saved",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "id": 9120,
    "date": "2026-10-08",
    "sectionId": 19,
    "sectionName": "A",
    "classId": 7,
    "className": "Class 5",
    "campusId": 1,
    "markedBy": "Nadia Khan",
    "markedAt": "2026-10-08T03:20:41.000Z",
    "onRoll": 32,
    "counts": {
      "present": 30,
      "absent": 1,
      "late": 1,
      "leave": 0,
      "halfDay": 0
    },
    "marks": [
      {
        "studentId": 412,
        "status": "absent",
        "remark": null
      },
      {
        "studentId": 415,
        "status": "late",
        "remark": "Bus late"
      }
    ]
  }
}

leave.requested

A leave request for a student or a member of staff is made and waits for a decision.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "leave.requested",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "leave": {
      "id": 301,
      "kind": "student",
      "type": "sick",
      "from": "2026-10-09",
      "to": "2026-10-10",
      "days": 2,
      "reason": "Fever",
      "status": "pending",
      "requestedOn": "2026-10-08",
      "requestedBy": "Imran Siddiqui",
      "decidedOn": null,
      "comment": null
    },
    "person": {
      "type": "student",
      "id": 412,
      "admissionNo": "ADM-2026-00412",
      "firstName": "Ayesha",
      "lastName": "Siddiqui",
      "classId": 7,
      "className": "Class 5",
      "sectionId": 19,
      "sectionName": "A",
      "campusId": 1,
      "rollNo": 14
    }
  }
}

leave.decided

A leave request is approved or refused.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "leave.decided",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "leave": {
      "id": 302,
      "kind": "staff",
      "type": "casual",
      "from": "2026-10-12",
      "to": "2026-10-12",
      "days": 1,
      "reason": "Family wedding",
      "status": "approved",
      "requestedOn": "2026-10-06",
      "requestedBy": "Nadia Khan",
      "decidedOn": "2026-10-07",
      "comment": null
    },
    "person": {
      "type": "staff",
      "id": 17,
      "code": "STF-0017",
      "firstName": "Nadia",
      "lastName": "Khan",
      "campusId": 1
    }
  }
}

Fees

fee.payment_received

A fee receipt is made: at the fee counter, on School360 Desktop or through POST /payments.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "fee.payment_received",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "id": 981,
    "number": "RCT-2026-00981",
    "amount": 8500,
    "lateFine": 0,
    "familyId": 233,
    "campusId": 1,
    "method": "ibft",
    "reference": "FT26100812345",
    "receivedOn": "2026-10-08",
    "createdAt": "2026-10-08T05:15:02.000Z",
    "lines": [
      {
        "voucherId": 5120,
        "voucherNumber": "FV-2026-10-00123",
        "studentId": 412,
        "month": "2026-10",
        "amount": 8500,
        "fine": 0
      }
    ],
    "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"
    }
  }
}

fee.vouchers_issued

A month's vouchers are issued. One event per issue run; up to 500 vouchers are listed, with more: true when there were more (fetch them with GET /vouchers?month=).

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "fee.vouchers_issued",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "month": "2026-10",
    "count": 2,
    "vouchers": [
      {
        "id": 5120,
        "number": "FV-2026-10-00123",
        "studentId": 412,
        "familyId": 233,
        "classId": 7,
        "sectionId": 19,
        "campusId": 1,
        "total": 8500,
        "dueOn": "2026-10-10"
      },
      {
        "id": 5121,
        "number": "FV-2026-10-00124",
        "studentId": 413,
        "familyId": 233,
        "classId": 4,
        "sectionId": 11,
        "campusId": 1,
        "total": 7200,
        "dueOn": "2026-10-10"
      }
    ],
    "more": false
  }
}

fee.voucher_overdue

An unpaid voucher's due date has passed. Sent once per voucher, at 8 am school time the day after it fell due, with the family's phone numbers.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "fee.voucher_overdue",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "voucher": {
      "id": 5120,
      "number": "FV-2026-10-00123",
      "studentId": 412,
      "familyId": 233,
      "classId": 7,
      "sectionId": 19,
      "campusId": 1,
      "month": "2026-10",
      "issuedOn": "2026-10-01",
      "dueOn": "2026-10-10",
      "total": 8500,
      "paid": 0,
      "balance": 8500,
      "fineAmount": 500,
      "lateFine": 0,
      "status": "overdue"
    },
    "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"
    }
  }
}

Exams

exam.results_published

An exam's results are published to families.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "exam.results_published",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "id": 31,
    "name": "Mid-Term 2026",
    "type": "mid-term",
    "academicYearId": 3,
    "termId": 5,
    "startDate": "2026-09-21",
    "endDate": "2026-09-30",
    "publishedOn": "2026-10-08",
    "classIds": [
      7,
      8
    ],
    "classes": [
      {
        "id": 7,
        "name": "Class 5",
        "campusId": 1
      },
      {
        "id": 8,
        "name": "Class 6",
        "campusId": 1
      }
    ]
  }
}

Notices

notice.published

A notice goes live on the notice board (straight away, or at its scheduled time).

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "notice.published",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "id": 57,
    "title": "School closed on Friday",
    "category": "holiday",
    "audience": "everyone",
    "classIds": [],
    "campusId": null,
    "pinned": true,
    "text": "The school will be closed on Friday 10 October for the public holiday.",
    "publishFrom": null,
    "publishUntil": "2026-10-11T00:00:00.000Z",
    "publishedAt": "2026-10-08T04:30:00.000Z"
  }
}

Staff

staff.joined

A new member of staff is added in Staff & HR. Work contacts only: never pay or CNIC.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "staff.joined",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "staff": {
      "id": 58,
      "code": "STF-0058",
      "firstName": "Usman",
      "lastName": "Raza",
      "gender": "male",
      "category": "academic",
      "designation": {
        "id": 4,
        "name": "Teacher"
      },
      "department": {
        "id": 2,
        "name": "Science"
      },
      "campusId": 1,
      "employmentType": "full-time",
      "phone": "0333-5550101",
      "email": "usman.raza@example.com",
      "cardNo": "0004417401",
      "joinedOn": "2026-10-08",
      "status": "active"
    }
  }
}

staff.left

A member of staff's employment ends (resigned, retired, let go), with the last day.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "staff.left",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "staff": {
      "id": 21,
      "code": "STF-0021",
      "firstName": "Farah",
      "lastName": "Ali",
      "gender": "female",
      "category": "non-academic",
      "designation": {
        "id": 9,
        "name": "Receptionist"
      },
      "department": {
        "id": 5,
        "name": "Administration"
      },
      "campusId": 1,
      "employmentType": "full-time",
      "phone": "0300-5550199",
      "email": "",
      "cardNo": null,
      "joinedOn": "2022-08-01",
      "status": "left"
    },
    "leftOn": "2026-10-31",
    "reason": "resigned"
  }
}

Gate

pickup.handed

Children are handed over (or dropped off) at the gate: one event per handover, with the collector and each child's family phone numbers.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "pickup.handed",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "result": "handed",
    "at": "2026-10-08T08:35:12.000Z",
    "direction": "pickup",
    "campusId": 1,
    "collector": {
      "id": 77,
      "name": "Akram Transport",
      "kind": "transporter",
      "vehicleNo": "LEA-1234"
    },
    "students": [
      {
        "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"
        }
      }
    ]
  }
}

pickup.denied

The gate refused to hand children over, with the school's reason and the guard's note.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "pickup.denied",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "result": "denied",
    "at": "2026-10-08T08:36:40.000Z",
    "direction": "pickup",
    "campusId": 1,
    "collector": {
      "id": null,
      "name": "Unknown driver",
      "kind": null,
      "vehicleNo": null
    },
    "reason": "not-authorised",
    "note": "Not on the family's list",
    "students": [
      {
        "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"
        }
      }
    ]
  }
}

Other

webhook.test

Sent when someone presses Send test event. Not subscribable.

Example
{
  "id": "6f1d2c3e-8a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "event": "webhook.test",
  "createdAt": "2026-10-08T05:15:03.412Z",
  "school": {
    "code": "SCH00001",
    "name": "Axiom School"
  },
  "data": {
    "message": "This is a test event from School360.",
    "endpointId": 12
  }
}

Checking the signature

Check every event before trusting it. Compute an HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret and compare it, in constant time, with v1 from the School360-Signature header. Use the raw body exactly as received (before parsing JSON), and refuse events whose t is more than 5 minutes from your clock, so an old event can't be replayed.

School360-Signature
School360-Signature: t=1791443703,v1=5f0c8e…c2a1

signed = HMAC-SHA256(secret, t + "." + rawBody) → hex
# Check a saved event by hand. body.json holds the raw body exactly as received.
SECRET="whsec_…"
HEADER="t=1791443703,v1=5f0c8e…"   # the School360-Signature header

T=$(echo "$HEADER" | sed -E 's/.*t=([0-9]+).*/\1/')
V1=$(echo "$HEADER" | sed -E 's/.*v1=([0-9a-f]+).*/\1/')
EXPECTED=$(printf '%s.%s' "$T" "$(cat body.json)" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

[ "$EXPECTED" = "$V1" ] && echo "valid" || echo "NOT valid"

Answers, timeouts and retries

Answer with any 2xx status within 10 seconds; the body of your answer is ignored (the first 1 KB is kept in the delivery log). Anything else — another status, a timeout, a refused connection — is retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours and 24 hours, then given up. Redirects aren't followed. An endpoint that fails without a single success for 3 days is turned off, and the school's owner gets an email; turn it back on in Settings once it's fixed. Endpoints must be public https:// addresses: private and local network addresses are refused.

Answer quickly and do the work afterwards (put the event on your own queue). An SMS gateway that takes 15 seconds to answer would be retried and send the SMS twice.

Duplicates and order

An event can arrive more than once (a retry after your answer was lost, or someone pressing Resend), so use School360-Delivery (the body's id) to skip ones you've already handled. Events aren't guaranteed to arrive in order; use createdAt and the data's own dates. School360 itself sends student.absent and student.late once per student per day, fee.voucher_overdue once per voucher, and student.updated at most once per student every 5 minutes.

Testing

Send test event on an endpoint (Settings › Integrations › Webhooks) sends webhook.test at once, so you can check your signature code. The delivery log shows every attempt with its status code, time and your answer, and Resend sends an event again with the same id. For local development, a tunnel (ngrok, Cloudflare Tunnel) gives your laptop an https address.

Ready-made receivers: SMS and WhatsApp on absence and accounting entries per payment.