Guide
Errors
When something goes wrong the API answers with an HTTP status and a JSON body that says why, in a form your code can act on.
The error body
Errors have a stable code to act on, a message for people and, when one field is at fault, its name in field (e.g. amount, or punches.3.at).
{ "error": { "code": "forbidden", "message": "This key needs fees:write.", "field": null } }Every answer also carries an X-Request-Id header: log it, and quote it when you ask School360 for help.
Error codes
New codes may be added; treat an unknown 4xx code as a request to fix, and an unknown 5xx as one to retry later.
- unauthorized
- 401
- Send your API key as Authorization: Bearer <key>.
- invalid_key
- 401
- This API key isn't valid.
- key_revoked
- 401
- This API key was revoked.
- key_expired
- 401
- This API key has expired.
- test_key_not_allowed
- 401
- Test keys only work on the Demo School.
- plan_required
- 403
- The API is part of the Enterprise package. Ask School360 to switch it on for your school.
- school_suspended
- 403
- This school is suspended.
- school_closed
- 403
- This school isn't open.
- ip_not_allowed
- 403
- Requests from this IP address aren't allowed for this key.
- forbidden
- 403
- This key doesn't have permission for this.
- workspace_off
- 403
- This workspace is switched off for the school.
- outside_scope
- 403
- This record is outside the key's campus.
- not_found
- 404
- Not found.
- method_not_allowed
- 405
- This method isn't allowed here.
- conflict
- 409
- This conflicts with the record as it is now.
- duplicate_reference
- 409
- A payment with this reference was already recorded.
- idempotency_in_progress
- 409
- A request with this Idempotency-Key is still running. Try again shortly.
- cursor_expired
- 409
- This cursor is older than the change journal keeps (30 days): read everything again, then poll from a new cursor.
- sheet_locked
- 409
- This mark sheet is submitted (locked), or the exam's results are published.
- results_not_published
- 404
- This exam's results aren't published.
- payload_too_large
- 413
- The request body is too large.
- unsupported_media_type
- 415
- Send JSON (Content-Type: application/json).
- invalid_json
- 400
- The request body isn't valid JSON.
- invalid_request
- 400
- The request isn't valid.
- idempotency_key_required
- 400
- Send an Idempotency-Key header with this request.
- idempotency_mismatch
- 422
- This Idempotency-Key was used with a different request.
- unprocessable
- 422
- This can't be done.
- rate_limited
- 429
- Too many requests. Slow down and try again.
- server_error
- 500
- Something went wrong on our side.
Handling errors
Retry 429 and 5xx answers after a pause (honour Retry-After), sending POSTs again with the same Idempotency-Key. Don't retry other 4xx answers unchanged: fix the request first.
// One helper for every call: retries what is worth retrying, explains the rest
async function school360(path, init = {}, tries = 4) {
const res = await fetch(`https://api.school360.cloud/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.SCHOOL360_KEY}`, "Content-Type": "application/json", ...init.headers },
})
if (res.status === 429 || res.status >= 500) {
if (tries <= 1) throw new Error(`School360 ${res.status} after retries`)
const wait = Number(res.headers.get("Retry-After")) || 2 ** (5 - tries)
await new Promise((r) => setTimeout(r, wait * 1000))
return school360(path, init, tries - 1) // POSTs: send the same Idempotency-Key again
}
const body = res.status === 204 ? null : await res.json()
if (!res.ok) {
const e = new Error(`${body.error.code}: ${body.error.message}`)
Object.assign(e, { status: res.status, code: body.error.code, field: body.error.field, requestId: res.headers.get("X-Request-Id") })
throw e
}
return body
}import os, time
import requests
api = requests.Session()
api.headers["Authorization"] = f"Bearer {os.environ['SCHOOL360_KEY']}"
def school360(method, path, tries=4, **kwargs):
"""One helper for every call: retries what is worth retrying, explains the rest."""
for attempt in range(tries):
res = api.request(method, f"https://api.school360.cloud/v1{path}", timeout=30, **kwargs)
if res.status_code == 429 or res.status_code >= 500:
time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))
continue # POSTs: the same Idempotency-Key header is sent again
body = res.json() if res.content else None
if not res.ok:
err = body["error"]
raise RuntimeError(f"{res.status_code} {err['code']} ({err.get('field')}): {err['message']} "
f"[request {res.headers.get('X-Request-Id')}]")
return body
raise RuntimeError(f"School360 {res.status_code} after {tries} tries")