Integrations

CSV roster sync: getting a nightly export from any SIS into Kastr

This is the universal route. Whatever your student information system is — including the one nobody writes documentation for — if it can produce a CSV on a schedule, it can feed Kastr. No OneRoster required, no vendor relationship required, and no credential of yours held by us.

Last reviewed 2026-08-04 ยท Kastr is pre-launch; we publish dated status rather than logos.

The five ways a roster CSV goes wrong, what you see, and the fix
FailureWhat actually happenedWhat the run reportsFix
Truncated exportThe SIS job died halfway; the file is valid CSV with 40% of your studentsaborted_guardrail, nothing writtenRe-run the export; add a row-count sanity check to the export job
UTF-8 byte-order markExcel or a Windows export prepended a BOM, so the first header reads student_idFirst column unrecognised; every row rejectedWrite UTF-8 without BOM, or strip the first three bytes in the pipeline
Excel-mangled identifiersSomeone opened the file; 0012345 became 12345Mass adds plus mass withdrawals, usually an abortNever round-trip through a spreadsheet; quote the column or export as text
Phone numbers without country code(555) 010-1234 with no +1Normalised to E.164 on a US assumption, flagged in validationStateExport E.164 if the SIS can; check the flagged list on the first run
Duplicate rowsA join fan-out produced the same student-guardian pair three timesDe-duplicated, counted once, reported in the ledgerFix the join; a fan-out usually means a field you did not intend is in the key

Four of these five are silent failures in most systems. The Excel one is the most expensive, because a file that has been opened and saved looks completely normal to a human and completely different to a matcher. If your process involves anyone opening the export to check it, that person needs a copy, not the original.

The cron recipe

Your SIS export almost certainly already exists and already drops a file somewhere. What you add is one line.

Assume the export lands at /srv/sis/exports/roster-YYYYMMDD.csv at about 01:30. The added job, at 03:00:

  • Check the file exists and is newer than the last successful run. If not, exit non-zero so your monitoring notices.
  • Call the Kastr CLI with the file path and the organisation slug, reading the API key from your secret store rather than from a config file.
  • Log the CLI's ledger output to the same place your other overnight jobs log.

The CLI is MIT-licensed and open source. Sixteen commands, plus an MCP server exposing nineteen tools if you want an agent able to inspect a roster diff. It runs on your host. You can read what it sends before you trust it with anything, which is a materially different proposition from a vendor-hosted connector.

If you would rather not install anything, the same thing is a curl POST to /api/v1/roster/sync with a bearer token. The CLI exists for convenience and ledger formatting, not as a gate.

Run it with --dry-run for the first week. Nothing is written, the full ledger prints, and you can compare counts against the SIS at leisure. There is no cost to running a dry run every night for a month.

Reading a dry run

A dry run against a 12,410-record file prints a ledger you should read line by line the first few times. What each section means:

  • Records read. Rows parsed successfully. If this is materially below your file's line count, rows are being rejected and the reasons are listed underneath.
  • Adds. Records with an external identifier not currently in the roster. On day one this is everything. On day two it should be single digits, plus whatever genuinely enrolled.
  • Changes. The identifier matched but the SHA-256 hash of the record payload differs. The ledger names the fields that moved, which is how you discover that one school changed its homeroom naming convention overnight.
  • Unchanged. Identical hash. No write happens at all, which is why a stable district produces a fast sync.
  • Withdrawals. Present yesterday, absent today, or carrying an end date in the past. This is the number to read hardest.
  • Rejected rows, with a reason per row: missing identifier, unparseable date, no contact points, malformed email.

If withdrawals exceed 50% of active records, the run stops and reports aborted_guardrail whether or not it was a dry run. Nothing is written and the previous roster stands. That single behaviour is the difference between a bad export and a bad week.

Headers, and why you should not rewrite your export

Kastr accepts a wide set of header spellings, because the alternative — making a district rewrite a working export to satisfy a vendor's preference — is a poor trade.

For a student identifier, all of these are recognised: student_id, studentId, student_number, studentnumber, sis_id, sourcedId, local_id, state_id, perm_id, id. Matching is case-insensitive and ignores underscores, spaces and hyphens, so Student Number and student-number both land. The same tolerance applies to guardian phone, guardian email, relationship, enrolment status and school identifier.

What matters far more than the spelling is that the identifier is stable between runs. Every alias in the world does not help if Monday's file uses the local number and Tuesday's uses the state number.

Minimum viable columns: a student identifier, a student name, a school identifier, an enrolment status, a guardian name, a guardian phone or email, and the relationship. One row per student-guardian pair. Everything else is optional and improves things at the margin.

Where the API key lives

Kastr API keys are SHA-256 hashed at rest and shown exactly once, at creation. There is no recovery path: if it is not in your secret store, you rotate it. That is deliberate, and it means a support ticket can never contain your key because we do not have it either.

Connector configs reject literal credentials. Paste a string beginning sk_, whsec_, AKIA, AIza, ghp_, xoxb- or -----BEGIN into a roster connector configuration and the save is refused. You store a reference to a secret in your own store instead. A rejected paste is the feature working: the alternative is a district's SFTP password sitting in a vendor's database, and then in a screenshot, and then in a ticket.

For the cron job itself, read the key from an environment variable populated by your secret manager, or from a file with restrictive permissions owned by the job's user. Do not put it in the crontab, where it appears in process listings. Rotate on staff changes; key creation, use and revocation are all recorded in the append-only audit log.

To answer the most common question directly: Kastr does not host an SFTP server for you to drop files onto. The file stays on infrastructure you control and your job pushes it. That means one fewer credential in our hands and one fewer place your student data sits at rest waiting to be processed.

Questions people actually ask

Does Kastr host an SFTP server I can drop files onto?

No. The file stays on infrastructure your district controls, and a job you own POSTs it to the roster endpoint over HTTPS. That is one fewer credential for us to hold and one fewer place your student data sits at rest. If your workflow genuinely requires a drop target, host it yourself and point the POST job at it.

Can I test a sync without changing any live records?

Yes. Run with the dry-run flag and the full ledger prints — records read, adds, changes, unchanged, withdrawals, and every rejected row with its reason — while nothing is written. Run it nightly for a week if you want. There is no charge and no side effect.

What happens if my export is truncated and only half the students are in the file?

The run computes that accepting it would withdraw more than 50% of active records, refuses, records aborted_guardrail and writes nothing. Yesterday's roster stands. This is the failure that ends badly elsewhere precisely because a truncated CSV is still valid CSV and every individual step reports success.

Does the CSV have to be in OneRoster format?

No. A flat file with one row per student-guardian pair is fine. You need a stable student identifier, a name, a school identifier, an enrolment status, a guardian name, a phone or email, and the relationship. Header spellings are matched loosely and case-insensitively, so you rarely need to change an export that already works.

How do I stop an API key from ending up in a config file in version control?

Read it from an environment variable populated by your secret manager, or from a permission-restricted file owned by the job's user. Never put it in the crontab, where it shows up in process listings. Roster connector configs also hard-reject credential-shaped strings outright, so the obvious wrong move fails loudly instead of quietly.

One price. Every feature. Locked for three years.

$3.50 per student per year under 5,000 students. No tiers, no add-on modules, no per-message fees. Published on the site because you should not have to book a call to learn a price.