The SIS sync endpoint
SIS sync is how a student information system pushes a cohort's roster into First Six. It is the production path for getting students in, and it is what SSO links against at login time, so it is usually the first integration you build.
Your institution admin generates the feed secret themselves in the console
(Settings → Integrations → Connect your student system); it is shown once
and stored only as a hash, so it never travels by email. Start every
integration with a dry_run push (below): it validates your whole roster
against our side and writes nothing. The console CSV import
(Cohorts & Student Import → Import) remains the right tool for one-off and
small-cohort loads.
Degrees, campuses, the teaching calendar and units first (one call to
/api/sis/enrolments), then the roster, then enrolment.
Each step depends on the one before it. A student's program_code and
campus_name are matched against rows that already exist, and a value we
don't recognise imports with that field left unset rather than failing, so
a roster sent before the taxonomy lands with every student's degree and campus
blank and nothing to tell you. Enrolment needs both the students and the units
to exist.
The part that catches everyone: a dry_run writes nothing, terms
included. Send the calendar with dry_run: true on a tenant whose calendar
is still empty and the enrolments section comes back with
term "T1-2026" not found in this institution, which reads like a broken
endpoint and is not. Push that first call for real once, then dry-run
enrolment as much as you like.
How your systems and ours find each other
The first question every integration asks, and it has a short answer: there is no discovery step, no registration handshake and nothing to install.
-
Your student system finds us by URL plus secret. The host is the same for every university (
https://console.firstsix.com.au), not a per-tenant subdomain. Which university you are is carried in the request body, never in the host:"institution": "<your-slug>"for/api/sis/enrolments, and acohort_idfor/api/sis/sync, from which we derive the institution. -
We check it is really you with the bearer secret. Once your institution has been issued its own secret, it is verified against that institution's own hash, so naming a different university in the body does not help: someone else's slug with your secret is a
401. The slug identifies, the secret authenticates, and neither does the other's job. Revoking your secret closes the feed outright rather than falling back to anything.Before your own secret is issuedA brand-new institution that has not yet been issued a secret is authenticated against a platform bootstrap secret instead, so that a first sync can be proved before the credential exchange happens. While that is the case, the per-institution guarantee above does not yet apply to you: the bootstrap credential is not specific to your institution. Ask us to issue your own secret (Settings → Integrations in the console) before you send real roster data, and the guarantee applies from that moment on.
-
We find your events calendar the other way round. That integration is a pull: an admin pastes a public
.icsURL into Settings → Integrations and we fetch it on a schedule. No secret, no push from your side, but it must be reachable from the public internet over https. -
Your people find their console through SSO, a third path that shares nothing with the two above. Sign-in matches on the identity provider's subject or the email address. A feed secret never signs anyone in, and a person's login never writes a roster.
So the only two things your integration team needs are the URL and the secret. Everything else (which university, which cohort, which term) travels in the payload.
The endpoint
POST /api/sis/sync
Authorization: Bearer <your-sync-secret>
Content-Type: application/jsonAuthentication is a shared secret in the Authorization header, not a user
session. We issue the secret per institution, it is machine-to-machine, and
it only authenticates pushes into your own cohorts. Rotation is coordinated
with your First Six contact (90-day cadence). If no secret is configured on our
side the endpoint returns 503, and a missing or wrong secret returns 401.
The payload
{
"cohort_id": "0e0f...uuid",
"dry_run": false,
"students": [
{
"first_name": "Alex",
"last_name": "Connor",
"email": "alex.connor@student.example.edu.au",
"student_id_ext": "U2025001",
"program_code": "BUSI",
"campus_name": "Riverside",
"first_in_family": false,
"start_date": "2026-03-02"
}
]
}cohort_id and a students array are required. For each student, email and
student_id_ext are required; the rest are optional. Matching is by email
first, then by student_id_ext within your institution, and both are stored,
so a student whose email changes upstream still updates in place on the next
push. Field notes:
program_codematches a program by its code;campus_namematches a campus by its name as shown in the console (campus_codeis accepted as an alias for the same value). Unrecognised values still import with that field left unset; check the response and fix on a follow-up push. Better still, send your degrees and campuses from the SIS too, in theprogramsandcampusessections of/api/sis/enrolments, so there is nothing to mismatch against. See Programs and campuses below.first_in_familyaccepts a boolean or the strings"true"/"false". Omit the field entirely to leave an existing value untouched.start_date(yyyy-mm-dd) anchors a student's own six weeks on individual-clock cohorts; invalid dates are ignored, never fatal.
A batch is capped at 5000 students. Larger batches return 413; page them.
Dry run: validate before you write
Send the same payload with "dry_run": true and nothing is written. The
response reports would_create / would_update, every program_code and
campus value we don't recognise, duplicate emails inside your batch, bad
dates, and per-row issues. Run it before your first real push, and any time
the SIS export changes shape; it is the roster half of the pre-go-live checks.
Your institution admin can run the same validation without curl. Settings → Integrations → Connect your student system → Check the feed has two buttons: Run readiness check reads the prerequisites out of their own data (is a secret configured, do students carry student numbers, does the calendar exist) and finishes by running one real enrolment row through this same validator; Check a sample payload takes the JSON you would POST and validates it against their real students, programs, campuses and terms. Neither writes anything. Useful when you want to confirm your extract's shape before booking time with their IT team.
One thing neither can check is the bearer token itself: First Six stores only
a sha256 of the feed secret, so nobody can test a secret without the
plaintext. A wrong secret always comes back as a plain 401.
A working example
curl -X POST https://console.firstsix.com.au/api/sis/sync \
-H "Authorization: Bearer $FIRST_SIX_SYNC_SECRET" \
-H "Content-Type: application/json" \
-d '{
"cohort_id": "0e0f...uuid",
"students": [
{ "email": "alex.connor@student.example.edu.au", "student_id_ext": "U2025001" }
]
}'The response
{
"received": 41,
"created": 20,
"updated": 20,
"imported": 40,
"errors": [{ "row": 5, "message": "email required" }]
}Always read errors. A 200 does not mean every row landed; it means the batch
was accepted and each row was processed individually. The status codes you should
handle:
| Code | Meaning | What to do |
|---|---|---|
200 | Batch processed | Check errors for per-row failures |
401 | Missing/wrong secret | Fix the Authorization header |
413 | Batch over 5000 | Split into pages |
429 | Rate limited | Back off, honour Retry-After |
503 | Secret not configured our side | Contact us |
Idempotency and partial data
The sync is idempotent. Re-sending the same students updates the existing rows in place rather than duplicating them, so a nightly full-roster push is safe to run as often as you like.
Bad rows do not sink the batch. A row missing a required field is rejected and
reported in errors, while every valid row still imports. A push of 100 with 10
bad rows imports 90 and returns 10 errors.
The endpoint is synchronous and does not queue or retry. On a 5xx or a 429,
your SIS should retry with backoff. The endpoint is rate limited to 20 requests
per minute per IP and returns Retry-After on a 429.
Programs and campuses
Your degrees and your campus list are the last two structural things an admin
would otherwise type into the console by hand, and they are the two the roster
silently depends on. Send them from the SIS as well, in the programs and
campuses sections of /api/sis/enrolments, ahead of the roster:
{
"institution": "your-slug",
"programs": [
{ "program_code": "BBUS", "name": "Bachelor of Business",
"coordinator_name": "Dr Alice Nguyen", "coordinator_title": "Course Director" }
],
"campuses": [
{ "name": "Riverside" }
]
}Notes worth knowing before you build:
- Programs are keyed on
code, case-insensitively.nameand the two coordinator fields are optional; omittingnameon a re-push leaves the stored one alone rather than overwriting it with the code, so a curated name survives a minimal nightly export. - Campuses are keyed on
name, because a campus has no code on our side. Matching is case-insensitive, soRiversideandriversideare one campus.campus_nameis accepted as an alias forname. Renaming a campus upstream creates a second campus here rather than renaming the first, and the two have to be merged by hand. Programs do not have this problem: rename them freely, the code is the key. - Both mint an audience tag automatically, so a fed taxonomy is immediately targetable when your team writes content for "Nursing students" or "everyone at Riverside".
- Caps: 1000 programs, 200 campuses per request. Larger returns
413.
A typical integration
- Send your degrees, campuses, calendar and units first
One POST to
/api/sis/enrolments, for real (notdry_run, which writes nothing). This is what the roster'sprogram_codeandcampus_namewill match against. - Dry-run your first export
Push the real roster with
"dry_run": trueand fix what it reports (unknown program/campus values, duplicates) before writing anything. - Pull the current roster from your SIS
Take the full current state of the cohort, not a delta. Because the sync is idempotent, you don't need to compute what changed.
- Page it into batches of up to 5000
Split large cohorts so no request exceeds the cap.
- POST each page and inspect errors
Treat the
errorsarray as the real result. Log or alert on rows that fail validation so a bad export doesn't silently drop students. - Schedule it nightly
A nightly full push keeps the roster current and means SSO always has a record to link against.
Troubleshooting
401 unauthorized on every request
The feed secret. It is stored only as a hash and shown once at generation, so nobody can read it back to you: if it has been lost or rotated, your institution admin generates a fresh one in the console (Settings, Integrations, Connect your student system) and you deploy it to your job. Also check the obvious pair: the header carrying the secret, and whether your scheduler is still running an old deployment.
413 batch_too_large
The cap is 5,000 students per request. Page the roster and send sequential batches; the sync is idempotent, so pages can overlap or be retried without double-writing anyone.
404 cohort_not_found
The cohort_id is not one of your institution's cohorts. Copy it from
the console rather than carrying it across environments: a cohort id
from a demo or a previous term does not exist in the tenant you are
pushing to.
Students imported, but their degree and campus are blank
The roster arrived before the taxonomy. program_code and
campus_name match against rows that must already exist, and an
unrecognised value imports with the field left unset rather than
failing. Send degrees, campuses, calendar and units first (one call to
the enrolments endpoint), then re-send the same roster: the sync is
idempotent and fills the blanks in place.
The dry run said success but nothing is in the console
That is the dry run working: "dry_run": true validates everything and
writes nothing, terms included. Re-send the identical payload with
dry_run false (or absent) to actually write.
Common questions
Do we need to delete students who've left?
Send the current roster; the sync reconciles additions and updates. Talk to us about how departures should be handled for your institution before relying on a specific behaviour.
What if program_code or campus_name isn't recognised?
The row still imports, with that field left unset. Check the response summary and fix codes on a follow-up push; the sync is idempotent, so re-sending corrected rows updates them in place. The durable fix is to send your degrees and campuses from the SIS too, ahead of the roster, so there is nothing left to mismatch against.
We renamed a campus. What happens?
A new campus is created rather than the existing one being renamed, because a campus is identified by its name and has no separate code. Students pushed after the rename attach to the new one, students pushed before stay on the old, and nothing is lost. Tell us and we'll merge them. Renaming a program is safe: it is keyed on its code.
Can we run it more than once a day?
Yes. It's idempotent and rate limited to 20 requests per minute per IP, so
frequent full pushes are fine as long as you back off on 429.
Do you have a connector for our SIS?
You don't need one. Any SIS, commercial or home-grown, integrates if it can
POST the JSON payload above; there is no vendor-specific connector to
install or wait for. Use a "dry_run": true push as your pre-go-live
roster validation: it checks the whole export against our side without
writing anything.
Next steps
Related
The fastest answer is usually one question away.