Octavia Guides

Knowledge Base

Browse step-by-step guides for every part of Octavia, with clear navigation and search to help you find what you need fast.

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:read
  • students:sync

Lesson Scheduling Export

Endpoint

``text GET https://<project-ref>.supabase.co/functions/v1/school-integration-lessons-v1 ``

Required scope:

  • lessons:read

Required Parameters

  • from
  • to

Both must use:

``text YYYY-MM-DD ``

Current request rules:

  • maximum range is 90 days
  • to must be on or after from

Optional parameters:

  • include_cancelled=true|false
  • updated_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:

  • cancelled
  • canceled

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:

  • data
  • meta

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_time and lesson.end_time are UTC timestamps
  • lesson.start_time_school_local and lesson.end_time_school_local are the same lesson times converted into the school's timezone
  • school.timezone provides the IANA timezone identifier used for that school, such as Australia/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

  • from and to are still required even when you use updated_since
  • updated_since is a delta filter inside the requested date window, not a replacement for the date window
  • use meta.max_updated_at from the response as the next cursor
  • use lesson_participant_id as 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=false may 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=true for 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 Cancelled through 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_since alone

For most schools, a practical and safe lesson-sync pattern is:

  1. Poll regularly for day-to-day changes using updated_since inside a rolling date window.
  2. Use meta.max_updated_at as the next cursor.
  3. Use lesson_participant_id as the unique key for lesson participant rows in your downstream system.
  4. 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_since so 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=true if 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_code
  • first_name
  • last_name
  • date_of_birth
  • gender
  • email

Current request rules:

  • external_student_code, first_name, and last_name are required for every student
  • date_of_birth, if included, must use YYYY-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=true for test runs and dry_run=false for 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_run or 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-v1
  • school-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_since for lesson export
  • request and response row counts where applicable
  • dry_run for student sync

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_key
  • invalid_api_key
  • school_api_integrations_not_enabled
  • insufficient_scope
  • missing_from
  • missing_to
  • invalid_updated_since
  • invalid_date_range
  • invalid_json
  • invalid_request_body
  • empty_students_batch
  • batch_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.