Recipes

Bank, 1Link and JazzCash payment sync

Turn confirmed bank, 1Link bill-payment, JazzCash and Easypaisa payments into School360 receipts automatically, safely retried and reconciled every day.

Goal

When the bank or wallet confirms a payment (a 1Link bill-payment callback, a JazzCash IPN, a line in the day's statement), your connector posts it to POST /v1/payments. School360 makes a normal receipt from the school's own series, pays the family's vouchers (late fines first, then the oldest) and sends the fee.payment_received webhook.

Matching the payment to a family

Tell School360 who paid, in one of three ways:

  • voucherNumber: the voucher's number (e.g. FV-2026-10-00123). Best for 1Link and bank bill payment, where the parent enters the number printed on the voucher as the consumer or bill reference.
  • familyCode: the family's code (e.g. FAM-00233), printed on every voucher. Pays the family's open vouchers, oldest first: good for a fixed reference per family on JazzCash or Easypaisa.
  • familyId: the family's id from the API, when your system already keeps it.

method is one of the school's payment methods (Finance › Fields & Options › Payment methods: ibft, bank-deposit, jazzcash, easypaisa…) and reference is the bank's or wallet's transaction id.

Recording the payment

Send the bank's transaction id as reference and build the Idempotency-Key from it. A timed-out request can then be sent again as many times as needed, and the payment is still recorded once.

curl -X POST https://api.school360.cloud/v1/payments \
  -H "Authorization: Bearer $SCHOOL360_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hbl-FT26100812345" \
  -d '{ "voucherNumber": "FV-2026-10-00123", "amount": 8500, "method": "ibft",
        "reference": "FT26100812345", "paidAt": "2026-10-08T10:15:00+05:00" }'
# → 201 { "id": 981, "number": "RCT-2026-00981", "amount": 8500, "lines": [...] }
# The same request again → the same 201 answer, with Idempotent-Replayed: true

Daily reconciliation

Once a day, compare the bank's statement with GET /v1/receipts for that day and method: anything on the statement without a receipt is sent again (the same Idempotency-Key makes this safe), and anything School360 has that the bank doesn't is checked by the accounts office.

// reconcile.mjs: does every bank credit of the day have a School360 receipt? Run once a day.
// bankTxns: the day's statement (from the bank's portal or API), [{ id, amount, billRef }]
const headers = { Authorization: `Bearer ${process.env.SCHOOL360_KEY}` }

async function receiptsOn(day, method) {
  const out = []
  let cursor = null
  do {
    const q = new URLSearchParams({ from: day, to: day, method, 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}`)
    out.push(...page.data)
    cursor = page.hasMore ? page.nextCursor : null
  } while (cursor)
  return out
}

export async function reconcile(day, bankTxns) {
  const receipts = (await receiptsOn(day, "ibft")).filter((r) => !r.cancelled)
  const byRef = new Map(receipts.map((r) => [r.reference, r]))
  const missing = bankTxns.filter((t) => !byRef.has(t.id)) // send these again (same Idempotency-Key)
  const wrong = bankTxns.filter((t) => byRef.has(t.id) && byRef.get(t.id).amount !== t.amount)
  const unknown = receipts.filter((r) => !bankTxns.some((t) => t.id === r.reference)) // not on the statement
  return { missing, wrong, unknown }
}

Pitfalls

  • Amounts are numbers in PKR (8500, not "8,500").
  • 409 duplicate_reference means this method and reference were already recorded: treat it as done, not as a failure.
  • 422 means the amount is more than the family owes, or the voucher is already paid or cancelled: park the payment for a person to look at; don't retry it.
  • The receipt is dated the day it's recorded; paidAt (when the parent paid) is kept on the receipt's note. Reconcile by reference, not by date.
  • Give the connector's key fees: write and families: read only, limited to the connector's IP address.
Next recipeAccounting export: QuickBooks, Xero, Tally