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

  1. 1In Zapier, start a Zap with Webhooks by Zapier › Catch Hook (in Make: Webhooks › Custom webhook) and copy the address it gives.
  2. 2In School Setup › Integrations › Webhooks, add an endpoint with that address and the events you want (e.g. application.received, fee.payment_received).
  3. 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…).
  4. 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.

Apps Script
// 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.
Next recipeZKTeco and other biometric machines