Recipes
Accounting export: QuickBooks, Xero, Tally
Post fee income to the school's books: a daily journal from GET /v1/receipts, or one entry per payment from the fee.payment_received webhook.
Goal
Every fee receipt becomes a journal entry: money into the cash, bank or wallet account it was received in, against fee income (and late-fine income). Run it as a daily export file the accountant imports, or post each payment as it happens.
Mapping receipts to accounts
A receipt has an amount (late fines included), its lateFine part, and the method it was paid by. One receipt becomes two or three journal lines:
- Debit the account for the method (Cash in Hand for
cash, the school's bank account foribftandbank-deposit, a wallet clearing account forjazzcash/easypaisa): the full amount. - Credit Tuition Fee Income: the amount less the late fine.
- Credit Late Fine Income: the late fine, when there is one.
Use the receipt number as the journal reference so the entry is easy to trace and never imported twice. Cancelled receipts come with cancelled: true: skip them (or reverse them if they were exported before).
A daily export
Run once a day, after the school closes, for the day before. It pages through GET /v1/receipts?from=&to= and writes a CSV most accounting tools can import (QuickBooks and Xero journal import, or Tally through a CSV-to-XML import tool).
// journal.mjs: yesterday's fee receipts as journal lines, for QuickBooks, Xero or Tally import.
import fs from "node:fs/promises"
const headers = { Authorization: `Bearer ${process.env.SCHOOL360_KEY}` }
// School360 payment method → the ledger account the money went into
const DEBIT = { cash: "Cash in Hand", "bank-deposit": "HBL Current A/C", ibft: "HBL Current A/C", jazzcash: "JazzCash Wallet", easypaisa: "Easypaisa Wallet" }
const FEES = "Tuition Fee Income"
const FINES = "Late Fine Income"
const day = new Date(Date.now() - 86_400_000).toLocaleDateString("en-CA", { timeZone: "Asia/Karachi" }) // YYYY-MM-DD
const rows = [["Date", "Journal No", "Account", "Debit", "Credit", "Memo"]]
let cursor = null
do {
const q = new URLSearchParams({ from: day, to: day, limit: "200", ...(cursor ? { cursor } : {}) })
const res = await fetch(`https://api.school360.cloud/v1/receipts?${q}`, { headers })
const page = await res.json()
if (!res.ok) throw new Error(`${res.status} ${page.error.code}`)
for (const r of page.data) {
if (r.cancelled) continue
const memo = [r.number, r.reference].filter(Boolean).join(" ")
rows.push([r.receivedOn, r.number, DEBIT[r.method] ?? "Suspense", r.amount, "", memo])
rows.push([r.receivedOn, r.number, FEES, "", r.amount - r.lateFine, memo])
if (r.lateFine > 0) rows.push([r.receivedOn, r.number, FINES, "", r.lateFine, memo])
}
cursor = page.hasMore ? page.nextCursor : null
} while (cursor)
const csv = rows.map((r) => r.map((v) => (/[",\n]/.test(String(v)) ? `"${String(v).replace(/"/g, '""')}"` : v)).join(",")).join("\n")
await fs.writeFile(`journal-${day}.csv`, csv + "\n")# journal.py: yesterday's fee receipts as journal lines, for QuickBooks, Xero or Tally import.
import csv, os
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import requests
api = requests.Session()
api.headers["Authorization"] = f"Bearer {os.environ['SCHOOL360_KEY']}"
DEBIT = {"cash": "Cash in Hand", "bank-deposit": "HBL Current A/C", "ibft": "HBL Current A/C",
"jazzcash": "JazzCash Wallet", "easypaisa": "Easypaisa Wallet"}
day = (datetime.now(ZoneInfo("Asia/Karachi")) - timedelta(days=1)).date().isoformat()
with open(f"journal-{day}.csv", "w", newline="") as f:
out = csv.writer(f)
out.writerow(["Date", "Journal No", "Account", "Debit", "Credit", "Memo"])
cursor = None
while True:
params = {"from": day, "to": day, "limit": 200, **({"cursor": cursor} if cursor else {})}
res = api.get("https://api.school360.cloud/v1/receipts", params=params, timeout=30)
res.raise_for_status()
page = res.json()
for r in page["data"]:
if r["cancelled"]:
continue
memo = " ".join(filter(None, [r["number"], r["reference"]]))
out.writerow([r["receivedOn"], r["number"], DEBIT.get(r["method"], "Suspense"), r["amount"], "", memo])
out.writerow([r["receivedOn"], r["number"], "Tuition Fee Income", "", r["amount"] - r["lateFine"], memo])
if r["lateFine"] > 0:
out.writerow([r["receivedOn"], r["number"], "Late Fine Income", "", r["lateFine"], memo])
if not page["hasMore"]:
break
cursor = page["nextCursor"]The file
Two receipts, one with a late fine:
Date,Journal No,Account,Debit,Credit,Memo
2026-10-07,RCT-2026-00981,HBL Current A/C,8700,,RCT-2026-00981 FT26100812345
2026-10-07,RCT-2026-00981,Tuition Fee Income,,8500,RCT-2026-00981 FT26100812345
2026-10-07,RCT-2026-00981,Late Fine Income,,200,RCT-2026-00981 FT26100812345
2026-10-07,RCT-2026-00982,Cash in Hand,4200,,RCT-2026-00982
2026-10-07,RCT-2026-00982,Tuition Fee Income,,4200,RCT-2026-00982As it happens: webhooks
For books that update straight away, subscribe an endpoint to fee.payment_received. Its body is the receipt with the vouchers and months it paid and the family, so each line can carry the student and month too.
{
"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"
}
}
}// The same journal lines as each payment happens: the fee.payment_received webhook
// (check the signature first, as in the Webhooks guide)
function journalLines(event) {
const r = event.data // the receipt, with its lines (vouchers and months paid) and the family
const memo = `${r.number} ${r.family?.code ?? ""} ${r.lines.map((l) => l.voucherNumber).join(" ")}`.trim()
return [
{ date: r.receivedOn, ref: r.number, account: DEBIT[r.method] ?? "Suspense", debit: r.amount, memo },
{ date: r.receivedOn, ref: r.number, account: "Tuition Fee Income", credit: r.amount - r.lateFine, memo },
...(r.lateFine > 0 ? [{ date: r.receivedOn, ref: r.number, account: "Late Fine Income", credit: r.lateFine, memo }] : []),
]
}
// Post them with your accounting software's API, keyed by r.number so a repeated event is skippedPitfalls
- Use one source per entry: either the daily pull or the webhook, not both, or every receipt is booked twice.
- Dates:
receivedOnis the school's date the receipt was made. Run the daily job after midnight in the school's time zone. - Refunds and cancellations change receipts after the day: re-read the last few days (or poll
GET /changesforreceipts) before a month is closed. - A read-only key with fees: read is all this needs.