Guide
Idempotency
Networks fail half-way: the request reached School360 but the answer never came back. With an Idempotency-Key you can simply send it again.
How it works
Networks fail. Send an Idempotency-Key header (any unique value of 8–100 characters, e.g. a UUID) with a POST and you can safely send it again: the first answer is returned (with Idempotent-Replayed: true) instead of doing it twice. It is required on POST /payments. The same key with a different body is refused with 422. Keys are kept for 24 hours.
Example: a bank payment
Build the key from something that identifies the operation itself (the bank's transaction id), not from a random value made on each attempt, so that every retry of the same payment carries the same key.
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// txn: a confirmed payment from the bank or wallet
async function recordPayment(txn) {
const res = await fetch("https://api.school360.cloud/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHOOL360_KEY}`,
"Content-Type": "application/json",
// Derived from the bank's transaction id: a retry can never make a second receipt
"Idempotency-Key": `hbl-${txn.id}`,
},
body: JSON.stringify({ voucherNumber: txn.billRef, amount: txn.amount, method: "ibft", reference: txn.id, paidAt: txn.paidAt }),
})
const body = await res.json()
if (res.status === 201) return body // the receipt
if (res.status === 409 && body.error.code === "duplicate_reference") return null // recorded before
throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`)
}<?php
// $txn: a confirmed payment from the bank or wallet
function record_payment(array $txn): ?array
{
$ch = curl_init('https://api.school360.cloud/v1/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SCHOOL360_KEY'),
'Content-Type: application/json',
'Idempotency-Key: hbl-' . $txn['id'], // a retry can never make a second receipt
],
CURLOPT_POSTFIELDS => json_encode([
'voucherNumber' => $txn['billRef'],
'amount' => $txn['amount'],
'method' => 'ibft',
'reference' => $txn['id'],
'paidAt' => $txn['paidAt'],
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status === 201) return $body; // the receipt
if ($status === 409 && $body['error']['code'] === 'duplicate_reference') return null; // recorded before
throw new RuntimeException("$status {$body['error']['code']}: {$body['error']['message']}");
}import os
import requests
def record_payment(txn):
"""txn: a confirmed payment from the bank or wallet → the receipt (None if recorded before)."""
res = requests.post(
"https://api.school360.cloud/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['SCHOOL360_KEY']}",
# Derived from the bank's transaction id: a retry can never make a second receipt
"Idempotency-Key": f"hbl-{txn['id']}",
},
json={"voucherNumber": txn["bill_ref"], "amount": txn["amount"], "method": "ibft",
"reference": txn["id"], "paidAt": txn["paid_at"]},
timeout=30,
)
body = res.json()
if res.status_code == 201:
return body
if res.status_code == 409 and body["error"]["code"] == "duplicate_reference":
return None
raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}")Rules
- 8–100 characters; any unique value (a UUID, or
<bank>-<transaction id>). - Required on
POST /payments; accepted on every other POST. - Kept for 24 hours per key: a repeat within that time gets the first answer, with
Idempotent-Replayed: true. - The same key with a different body answers
422 idempotency_mismatch. - While the first request is still running, a repeat answers
409 idempotency_in_progress: wait a moment and send it again. - On top of this,
POST /paymentsrefuses a reference already recorded for the same method (409 duplicate_reference), even with a new key.