Documentation

The API

Version 0.1 ·Updated 2026-08-29

Next reviewed by 2027-02-28.

Written for integrators building against Kestrel without asking permission first.

Read this first

Every endpoint that serves school data is authenticated by a browser session, and every one of them checks that session on the server.

The reference is published in full, because deciding whether to integrate with a vendor is something you should be able to do before signing anything. A closed API is disqualifying in this sector, and gating the reference behind a partner conversation is the same thing with better manners.

The reference is generated

The machine-readable reference is an OpenAPI document the server produces from its own route table. It is not maintained by hand, so it cannot drift from the routes it describes.

curl https://app.kestrel.seraco.io/_openapi.json

Point your generator at it. One caveat worth knowing: it also lists the framework's own internal routes, the ones under /__nuxt and /api/_nuxt_icon. They are not part of the API and are not supported. Everything else in the document is.

Two calls that work right now

Neither needs an account, which is what makes them useful for checking connectivity.

curl https://app.kestrel.seraco.io/health
{ "status": "healthy", "uptime": 1284, "version": "2026-08-29T04:11:00Z" }

version is the build timestamp rather than the source revision: enough to tell one deployment from the next without naming the code it was cut from. The route answers while database migrations are still running, so a health check can distinguish "starting" from "broken".

curl https://app.kestrel.seraco.io/api/hello

Authentication

Sign-in is passwordless: a passkey, or a one-time code sent by email. It produces a session cookie, and that cookie is what every guarded endpoint checks. There is no password to send, and therefore no basic auth.

Every route that serves student data checks the session itself. The browser-side route guard exists to make navigation behave, and the access control is on the server.

Errors

Every failure is JSON with the same shape. statusMessage is written for a person; data is for your code.

{
  "statusCode": 422,
  "statusMessage": "This palette was refused",
  "data": { "failures": ["The accent on the page in light mode is 2.4:1, and needs 3:1."] }
}
400
The request was malformed, or a required field was missing. The message names the field.
401
No session, or an expired one. Sign in again; do not retry the same request.
403
Signed in, but not permitted. Retrying will not help.
404
No such record, or no such route. It does not distinguish between them for records another school owns.
409
Someone else changed the record since you read it. Re-read it, reapply your change, and send it again with the new version.
422
The request was understood and the content was refused. `data` holds the specific reasons, one per failure, not just the first.
500
Our fault. Safe to retry once, with a delay.

The distinction between 409 and 422 is the one worth internalising. A 409 means the record moved on and your change is probably still valid: re-read, reapply, resend. A 422 means the content itself was refused and resending it unchanged will be refused again.

Versioning and deprecation

The API is at 0.x. An endpoint can change shape between releases.

Once it reaches 1.0, three commitments hold:

  • Adding a field to a response is not a breaking change. Write your client to ignore fields it does not know.
  • Removing or renaming a field, or changing its type, means a new major version. Both versions run side by side for at least twelve months.
  • A deprecation is announced on this page and in a Deprecation response header before it takes effect, with the removal date stated in the announcement.

Getting in touch

api@seraco.io. If you are building something and an endpoint you need is missing, that is worth telling us before you work around it.