Skip to content
Open the app

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.

Get your secret, then dry run

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.

Send the sections in this order the first time

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 a cohort_id for /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 issued

    A 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 .ics URL 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/json

Authentication 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_code matches a program by its code; campus_name matches a campus by its name as shown in the console (campus_code is 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 the programs and campuses sections of /api/sis/enrolments, so there is nothing to mismatch against. See Programs and campuses below.
  • first_in_family accepts 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:

CodeMeaningWhat to do
200Batch processedCheck errors for per-row failures
401Missing/wrong secretFix the Authorization header
413Batch over 5000Split into pages
429Rate limitedBack off, honour Retry-After
503Secret not configured our sideContact 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.

Retries are your job

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. name and the two coordinator fields are optional; omitting name on 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, so Riverside and riverside are one campus. campus_name is accepted as an alias for name. 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

  1. Send your degrees, campuses, calendar and units first

    One POST to /api/sis/enrolments, for real (not dry_run, which writes nothing). This is what the roster's program_code and campus_name will match against.

  2. Dry-run your first export

    Push the real roster with "dry_run": true and fix what it reports (unknown program/campus values, duplicates) before writing anything.

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

  4. Page it into batches of up to 5000

    Split large cohorts so no request exceeds the cap.

  5. POST each page and inspect errors

    Treat the errors array as the real result. Log or alert on rows that fail validation so a bad export doesn't silently drop students.

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

Was this helpful?
Need more help?

The fastest answer is usually one question away.

Contact us
Edit this page on GitHub