School Integrations - School Integration API Technical Guide
This guide is for school IT teams, developers, and integration providers implementing the School Integration API. For the non technical overview, use: The current live scope includes: The current live scope does not incl
For the non-technical overview, use:
Current Live Scope
The current live scope includes:
- Lesson scheduling export
- Student master sync import
The current live scope does not include:
- webhook push delivery
- delete sync
- lesson write access
Integration Model
Version one uses a school-managed integration model.
That means the school-side integration is responsible for:
- polling lesson data from Octavia
- pushing student master updates into Octavia
- retries, scheduling, and recovery
- any broader reconciliation runs
Authentication
Authentication uses a bearer token:
``text Authorization: Bearer <API_KEY> ``
Current live scopes:
lessons:readstudents:sync
Lesson Scheduling Export
Endpoint
``text GET https://<project-ref>.supabase.co/functions/v1/school-integration-lessons-v1 ``
Required scope:
lessons:read
Required Parameters
fromto
Both must use:
``text YYYY-MM-DD ``
Current request rules:
- maximum range is 90 days
tomust be on or afterfrom
Optional parameters:
include_cancelled=true|falseupdated_since=<ISO 8601 timestamp with timezone>
updated_since is an inclusive cursor against each participant row's sync.updated_at value.
If include_cancelled=false, cancelled lessons are filtered out of the returned rows.
This means include_cancelled=false is not suitable as the only source of truth if your downstream system needs to reliably remove or mark cancelled lessons.
In practice, include_cancelled=false is most useful for consumers that only want currently active lessons. It is less suitable for authoritative downstream synchronisation.
For this endpoint, a lesson participant is currently treated as cancelled only when the participant's attendance outcome is marked as:
cancelledcanceled
Example Request
``bash curl -X GET "https://<project-ref>.supabase.co/functions/v1/school-integration-lessons-v1?from=2026-07-01&to=2026-07-31&include_cancelled=false&updated_since=2026-07-05T00:00:00Z" \ -H "Authorization: Bearer <API_KEY>" ``
Response Shape
Successful responses return:
datameta
Each item in data represents one lesson participant row.
That means:
- an individual lesson usually returns one row
- a group lesson returns one row per participant
- attendance is included on the same row as that participant
Example Success Response
``json { "data": [ { "lesson_id": "aa33bb1d-4159-4fe3-8ac9-9f923332e1f2", "lesson_participant_id": "0c0cbc1b-f0fd-437f-9586-9c17534fa713", "student": { "master_student_id": "388d2fe6-7b90-4f6d-8a80-8f48ffe28194", "external_student_code": null, "name": "Tarini Abeyanda" }, "teacher": { "id": "b111e690-716c-4dad-8dfa-baa6fa38554b", "name": "Belinda T Mackey" }, "lesson": { "start_time": "2026-06-30T23:05:00+00:00", "end_time": "2026-06-30T23:35:00+00:00", "start_time_school_local": "2026-07-01T09:05:00", "end_time_school_local": "2026-07-01T09:35:00", "duration_minutes": 30, "status": "scheduled", "is_makeup": false, "group_id": null, "lesson_kind": "individual", "lesson_context": "30 minute individual lesson with Belinda T Mackey", "location": { "source": "room_booking_inferred", "room_id": "3d61232a-6244-4880-a4d0-574bcb4a118e", "name": "Classroom 2" } }, "attendance": { "is_marked": false, "attendance_status_id": null, "outcome": null, "label": null, "comment": null, "marked_at": null }, "school": { "timezone": "Australia/Melbourne" }, "sync": { "updated_at": "2026-07-05T04:12:55.102Z" } } ], "meta": { "from": "2026-07-01", "to": "2026-07-31", "updated_since": "2026-07-05T00:00:00.000Z", "max_updated_at": "2026-07-05T04:12:55.102Z", "count": 1, "include_cancelled": false } } ``
Field Rules Worth Knowing
Student identity
Use student.master_student_id as the stable participant identity.
If your school uses external student codes, student.external_student_code is also included where available.
Teacher identity
The exported teacher is the actual teacher of that lesson occurrence:
- if there is no substitute teacher, the normal lesson teacher is returned
- if there is a substitute teacher, the substitute is the teacher returned in the API
Attendance
Attendance is included on the same participant row.
If attendance has not yet been marked, attendance status fields return null and attendance.is_marked returns false.
Attendance changes also update the row's sync freshness. That means a poll that includes the lesson row in its requested date window can pick up attendance changes through updated_since.
Time fields
The lesson payload now includes both UTC and school-local lesson times:
lesson.start_timeandlesson.end_timeare UTC timestampslesson.start_time_school_localandlesson.end_time_school_localare the same lesson times converted into the school's timezoneschool.timezoneprovides the IANA timezone identifier used for that school, such asAustralia/Melbourne
The UTC fields remain the canonical machine-safe timestamps. The school-local fields are convenience fields for downstream systems that want a ready-to-use school-local display time.
Location
Lesson location is inferred from room bookings only when Octavia can determine one safe matching room booking for that lesson time and teacher.
Operational Notes
fromandtoare still required even when you useupdated_sinceupdated_sinceis a delta filter inside the requested date window, not a replacement for the date window- use
meta.max_updated_atfrom the response as the next cursor - use
lesson_participant_idas the unique key for each lesson participant row in your downstream system - run a broader rolling-window reconciliation periodically, because lessons that move outside your requested date window do not produce tombstones
- a normal schedule poll can also pick up attendance changes, as long as the affected lesson still falls inside the requested date window
- if a lesson was previously imported and is later cancelled in Octavia, a sync that uses only
include_cancelled=falsemay no longer return that row - if a teacher deletes a future lesson instance instead of marking it cancelled, that removed lesson also does not produce a tombstone row
- to handle both cancelled and deleted lesson instances safely, either:
- use
include_cancelled=truefor sync runs and handle cancelled rows explicitly, or - treat a refreshed reconciliation window from Octavia as the current source of truth for that same window in your downstream schedule
Operational cancellation guidance:
- for a same-day cancellation, the safest approach is for the teacher to mark the lesson as
Cancelledthrough the attendance workflow rather than hard-deleting the lesson - this keeps the lesson row available for polling and allows attendance-sensitive sync runs to pick up the cancellation quickly
- for a future lesson removal, deleting the lesson instance is acceptable, but downstream systems will only recognise that removal through reconciliation-style logic, not through
updated_sincealone
Recommended School Sync Strategy
For most schools, a practical and safe lesson-sync pattern is:
- Poll regularly for day-to-day changes using
updated_sinceinside a rolling date window. - Use
meta.max_updated_atas the next cursor. - Use
lesson_participant_idas the unique key for lesson participant rows in your downstream system. - Run a broader reconciliation on a regular schedule so your downstream system can remove lesson instances that were cancelled or deleted in Octavia.
If your school needs attendance updates closer to real time, use a split polling model rather than making every sync run high frequency.
Recommended pattern:
- run an attendance-sensitive poll every 5 minutes when needed
- keep that fast poll limited to a small same-day window, such as the current day or a short rolling window around the current time
- always pair that fast poll with
updated_sinceso you only receive rows changed since the last successful run - keep your broader schedule polling on a slower cadence, such as every 15 to 30 minutes
- continue to run a broader reconciliation, such as nightly, over a wider rolling window such as the current teaching term
This allows a school to pick up attendance changes quickly, without polling large future schedule windows every 5 minutes.
One practical example is:
- poll every 5 minutes for a same-day attendance-sensitive window, using
updated_since - poll every 15 minutes for a broader schedule window such as the next 30 days
- use
include_cancelled=trueif your downstream system needs explicit cancellation rows - run a broader reconciliation, such as nightly, over a wider rolling window such as the current teaching term
- during that reconciliation, treat the refreshed window from Octavia as the current source of truth for that same window in your downstream schedule
Operational guidance for higher-frequency polling:
- do not run broad future-window polls every few minutes unless your school has a specific operational need
- keep fast polling focused on the time period where attendance changes are operationally important
- stagger scheduled polling times where possible rather than concentrating all schools or jobs on the same minute boundary
- the broader schedule poll can also pick up attendance changes for lessons inside its date window, but the attendance-sensitive poll is the faster operational path for same-day attendance and same-day cancellations
Student Master Sync
Endpoint
``text POST https://<project-ref>.supabase.co/functions/v1/school-integration-students-sync-v1 ``
Required scope:
students:sync
Optional query parameter:
dry_run=true|false
Request Body
The request body must contain a students array.
Version one currently supports:
external_student_codefirst_namelast_namedate_of_birthgenderemail
Current request rules:
external_student_code,first_name, andlast_nameare required for every studentdate_of_birth, if included, must useYYYY-MM-DD- maximum batch size is 500 students per request
- matching is always scoped to the current school
- the sync creates or updates student master records only
- version one does not delete records
Example Request
``bash curl -X POST "https://<project-ref>.supabase.co/functions/v1/school-integration-students-sync-v1?dry_run=true" \ -H "Authorization: Bearer <API_KEY>" \ -H "Content-Type: application/json" \ -d '{ "students": [ { "external_student_code": "S12345", "first_name": "Jane", "last_name": "Smith", "date_of_birth": "2011-04-12", "gender": "female", "email": "jane.smith@example.edu.au" } ] }' ``
Example Success Response
``json { "data": { "created": 1, "updated": 0, "unchanged": 0, "failed": 0, "processed": 1, "dry_run": true }, "errors": [] } ``
Partial Success Behaviour
If the request body is structurally valid but some rows fail validation, Octavia returns a partial-success response with per-row errors in errors.
Operational Pattern
This is a school-managed push into Octavia.
A practical pattern is:
- send changes when your school system is ready to push them
- run a broader reconciliation batch on a regular schedule, such as nightly, if your school wants to catch missed updates
- use
dry_run=truefor test runs anddry_run=falsefor live runs
Logging And Monitoring
For a production-style integration, both sides should keep operational logs.
School-side logs
The school or integration provider should usually log:
- when each sync run started and finished
- whether the run was
dry_runor live - how many rows were sent
- the HTTP response status
- a short response summary
- any retry or failure details
This helps the school-side integration answer:
- what did we try to send?
- when did we send it?
- did it succeed?
- what should we retry?
Octavia-side logs
Octavia already keeps inbound API request logs for the School Integration API.
These logs are stored in:
public.school_integration_api_request_logs
They are written via:
public.school_integration_log_api_request_v1(...)
The current implementation records request details from both live endpoints:
school-integration-lessons-v1school-integration-students-sync-v1
Current logged fields include:
- endpoint
- request method
- status code
- error code
- request start time
- duration
- date range fields for lesson export
updated_sincefor lesson export- request and response row counts where applicable
dry_runfor student sync
Recommended split
Use the two logging layers together:
- school-side logs to understand what the school integration attempted
- Octavia-side logs to understand what Octavia received and how it responded
When logging on either side, avoid storing more personal data than you need. Counts, identifiers, status values, and short summaries are usually safer than storing full payloads.
Error Responses
Error responses use this shape:
``json { "error": { "code": "error_code", "message": "Human-readable message" } } ``
Common current error codes include:
missing_api_keyinvalid_api_keyschool_api_integrations_not_enabledinsufficient_scopemissing_frommissing_toinvalid_updated_sinceinvalid_date_rangeinvalid_jsoninvalid_request_bodyempty_students_batchbatch_too_large
Current Limitations
The current pilot version has these limitations:
- no webhook push delivery yet
- maximum 90-day request window for lesson export
- maximum 500-student sync batch size
- no delete sync in version one
If your school needs near-real-time inbound updates rather than polling, that is intended as a future upgrade rather than part of the current pilot.
Support
If your school is joining the pilot and needs help with setup, key management, or interpreting the response, contact Octavia support.
