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).

JSON
{ "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
}