Recipes
Zapier, Make and Google Sheets
No-code and low-code automations: webhooks as Zapier and Make triggers, GET /v1/changes for polling, and a Google Apps Script that keeps a sheet of absences.
Goal
Connect School360 to the hundreds of apps Zapier and Make reach (Google Sheets, Slack, email, CRMs) without running a server, and keep simple reports in a spreadsheet.
Webhooks as triggers
- 1In Zapier, start a Zap with Webhooks by Zapier › Catch Hook (in Make: Webhooks › Custom webhook) and copy the address it gives.
- 2In School Setup › Integrations › Webhooks, add an endpoint with that address and the events you want (e.g.
application.received,fee.payment_received). - 3Press Send test event, or make a real one: Zapier and Make read the fields from the first event (
data.student.firstName,data.family.phone…). - 4Add the actions: a row in Google Sheets, a Slack message, an email.
Zapier and Make can't check School360's signature. Their hook addresses are long and random: keep them private, and don't use a no-code hook for anything that moves money.
Polling instead
Some steps need to pull: a scheduled Zap or Make scenario can call GET /v1/changes?since= with the key in an Authorization header and keep the nextCursor it gets for the next run (in a Zapier Storage key or a Make data store). See Polling for changes.
Google Sheets with Apps Script
This script keeps an Absences sheet up to date. Every 10 minutes it asks GET /changes whether any register changed (one cheap call), and only then reads today's registers and adds the absent children, with their family's phone, skipping ones already on the sheet.
// Google Sheets › Extensions › Apps Script. Project settings › Script properties:
// SCHOOL360_KEY = s360_live_… (attendance:read and students:read, families:read for phones)
// Triggers › Add trigger › pollAbsences, time-driven, every 10 minutes.
const BASE = "https://api.school360.cloud/v1"
function api(path) {
const res = UrlFetchApp.fetch(BASE + path, {
headers: { Authorization: "Bearer " + PropertiesService.getScriptProperties().getProperty("SCHOOL360_KEY") },
muteHttpExceptions: true,
})
const body = JSON.parse(res.getContentText())
if (res.getResponseCode() >= 300) throw new Error(res.getResponseCode() + " " + body.error.code)
return body
}
function pollAbsences() {
const props = PropertiesService.getScriptProperties()
// 1. Anything new in the registers since last time? (one cheap call)
let since = props.getProperty("cursor") || ""
let registersChanged = false
let page
do {
page = api("/changes" + (since ? "?since=" + since : ""))
if (page.data.some((c) => c.entity === "registers")) registersChanged = true
since = page.nextCursor
} while (page.hasMore)
props.setProperty("cursor", since)
if (!registersChanged) return
// 2. Read today's registers and add the absences not on the sheet yet
const tz = api("/school").timezone
const today = Utilities.formatDate(new Date(), tz, "yyyy-MM-dd")
const sheet = SpreadsheetApp.getActive().getSheetByName("Absences") || SpreadsheetApp.getActive().insertSheet("Absences")
if (sheet.getLastRow() === 0) sheet.appendRow(["Date", "Admission no.", "Student", "Class", "Section", "Phone", "Key"])
const done = new Set(sheet.getLastRow() > 1 ? sheet.getRange(2, 7, sheet.getLastRow() - 1, 1).getValues().flat() : [])
const classes = Object.fromEntries(api("/classes").data.map((c) => [c.id, c.name]))
for (const section of api("/sections").data) {
const register = api("/attendance?sectionId=" + section.id + "&date=" + today)
const absent = register.marks.filter((m) => m.status === "absent" && !done.has(today + ":" + m.studentId))
if (!absent.length) continue
const roll = Object.fromEntries(api("/students?sectionId=" + section.id + "&status=active&limit=200").data.map((s) => [s.id, s]))
for (const m of absent) {
const s = roll[m.studentId] || {}
sheet.appendRow([today, s.admissionNo, (s.firstName + " " + (s.lastName || "")).trim(), classes[section.classId], section.name, s.family ? s.family.phone : "", today + ":" + m.studentId])
}
Utilities.sleep(300) // stay well under the rate limit
}
}Pitfalls
- Don't point a webhook at an Apps Script web app: it answers POSTs with a 302 redirect, which School360 counts as a failure and retries. Use polling as above, or Zapier / Make in between.
- Keep the key in Script properties, not in the code, and share the sheet carefully: it holds parents' phone numbers.
- Apps Script runs are limited to a few minutes: for a big school, read the sections in chunks over several runs.
- Mind the rate limit: the script pauses briefly between sections.