Integrations

The Kastr roster sync API: authentication, rate limits, idempotency and webhooks

No named competitor in K-12 communications publishes API documentation on the open web. ParentSquare's knowledge base is login-gated and Cloudflare-blocked to crawlers. This page is the part of ours that matters for rostering: how you authenticate, what the limits are, what a webhook signature looks like, and what happens when you POST the same file twice.

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

Rate limit tiers, and what a nightly roster run actually consumes
TierLimitApplies toNightly sync impact
Per API key120 requests / minuteEvery authenticated call with that keyNone — a roster POST is one request carrying 15,000 records
Per SCIM key600 requests / minuteIdentity-provider provisioning trafficNone — separate key class
Per organisation600 requests / minuteAll keys in one district, combinedRelevant only if several jobs run concurrently
Per IP, unauthenticated60 requests / minuteCalls with no valid keyNone, unless your key is wrong

Caveat, stated because you would find it anyway: rate limiting is currently in-memory and per Node process. It resets on restart and does not coordinate across instances. It is a courtesy limiter and abuse damper, not a distributed quota system, and we would rather write that here than let a security reviewer discover it and wonder what else was overstated. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After on a 429.

Authentication and keys

Bearer token in the Authorization header. Keys are generated in the district console, SHA-256 hashed at rest, and displayed exactly once at creation. There is no route that returns a key after the fact — not for you, not for support, not for us. If it is not in your secret store, rotate it.

Every key is scoped to one organisation, and the scoping is enforced in the database rather than in application code. Each request runs inside a transaction that sets a non-owner, DML-only Postgres role with row-level security policies attached. A query that forgets its filter returns nothing rather than everything, and with no organisation context set, policies evaluate against NULL and return zero rows. It fails closed. The cross-tenant leakage suite runs in continuous integration against a real Postgres 16 instance, including a regression test for the owner-role bypass that is the usual quiet failure of this control.

One limitation you should know before your security review finds it. API keys carry declared scopes, but only the send and scim scopes are actually enforced today. A key marked read-only is not restricted to reads. Treat every non-revoked key as capable of reading all of your organisation's data, issue them narrowly, and rotate them on staff changes. We are not going to describe scope-based access control as a security feature while that is true.

Key creation, use and revocation are all recorded in the append-only audit log, which is hash-chained per organisation with SHA-256 and carries neither UPDATE nor DELETE permission for the application role.

POST /api/v1/roster/sync

One request, one file, one ledger back. The body carries your roster records; the response carries the classification.

What happens server-side, in order:

  1. Each record's payload is canonicalised and hashed with SHA-256.
  2. Each hash is compared against the stored hash for that external identifier. The result is add, change, unchanged, or — for identifiers present before and absent now — withdraw.
  3. If withdrawals exceed 50% of active records, the run aborts with aborted_guardrail, nothing is written, and the previous roster stands.
  4. Otherwise writes are applied, and the run is recorded in the audit log with its counts.

Re-POSTing an identical file is safe and cheap. Every record hashes to the same value, everything classifies as unchanged, and no write occurs. That is what makes retry logic simple: if your cron job is uncertain whether last night's run completed, run it again. There is no half-applied state to reconcile, because the classification is computed from content rather than from a sequence of events.

Message delivery downstream has its own idempotency: a unique key per broadcast, person and channel, so a retried dispatch cannot double-send. Workers claim delivery rows with SELECT ... FOR UPDATE SKIP LOCKED under a five-minute claim TTL, which means a worker that dies mid-batch releases its rows automatically rather than stranding them, and network I/O happens outside the transaction so a slow vendor call never holds a database lock.

Webhooks: verifying that a call came from us

Kastr can call your endpoint when a sync completes, aborts, or crosses a threshold you configure. The signature scheme is the Stripe-style one, because it is well understood and easy to verify correctly.

The header is X-Kastr-Signature: sha256=<hmac>. The HMAC is computed over the string formed by the timestamp, a full stop, and the raw request body — before any JSON parsing. To verify:

  1. Read the raw body as bytes. Do not parse it first; re-serialising JSON changes whitespace and key order, and the signature will not match.
  2. Read the timestamp from the signed payload and reject anything older than your tolerance, five minutes being a sensible default. This is what makes a captured request useless later.
  3. Compute HMAC-SHA256(secret, timestamp + "." + rawBody).
  4. Compare with a constant-time comparison, never with string equality.

Delivery retries on a fixed schedule: 1, 2, 5, 15, 60 and 360 minutes. An endpoint that fails ten times consecutively is disabled automatically and must be re-enabled deliberately in the console, which stops a decommissioned endpoint from being retried indefinitely and stops a broken receiver from filling your logs for a week.

The SSRF guard, and why your endpoint may be refused

Before any webhook is delivered, the destination hostname is resolved and the resulting address is checked. Loopback, RFC1918 private ranges and link-local addresses are rejected.

The check happens at DNS resolution rather than on the literal string, which is the part that matters: a hostname that looks entirely public can resolve to 10.0.0.5, and a naive string check passes it happily. Resolving first is what turns a server-side request forgery vector into a rejection.

What this means for you: you cannot point a Kastr webhook at an internal-only endpoint, even one reachable from our infrastructure by some accident of networking. The endpoint is refused at configuration time with a clear error rather than failing mysteriously later. If you want sync events inside your network, terminate the webhook on a public endpoint you control and forward from there, or poll the API on your own schedule.

The CLI and MCP server are MIT-licensed and open source — sixteen commands and nineteen MCP tools. If you want to know exactly what a request looks like before you trust it with roster data, the source is the documentation, and it runs on your machine rather than ours.

Questions people actually ask

What happens if I POST the same roster file twice?

Nothing changes. Every record hashes to the value already stored, everything classifies as unchanged, and no write occurs. That is what makes retries safe: if your cron job cannot tell whether last night's run completed, just run it again. There is no half-applied state to reconcile.

How do I verify that a webhook actually came from Kastr?

Compute HMAC-SHA256 over the timestamp, a full stop, and the raw request body, using your endpoint secret, and compare it in constant time with the X-Kastr-Signature header. Read the raw bytes before parsing — re-serialised JSON will not match. Reject timestamps older than about five minutes so a captured request cannot be replayed.

Why was my webhook endpoint rejected before it was ever called?

Because it resolved to a loopback, RFC1918 or link-local address. The guard resolves the hostname rather than inspecting the string, so a public-looking name pointing at a private address is still refused. If you need events inside your network, terminate on a public endpoint you control and forward, or poll the API instead.

What are the rate limits for a nightly 15,000-record sync?

Irrelevant, in practice. A roster sync is a single request carrying all the records, against a 120-per-minute per-key limit. Limits matter for read-heavy integrations, not for rostering. Note that the limiter is currently in-memory and per process, so it resets on restart and does not coordinate across instances — treat it as an abuse damper rather than a quota.

Can I recover an API key I did not copy when it was created?

No. Keys are SHA-256 hashed at rest and shown exactly once. There is no route that returns one afterwards, for you or for us, which also means a support ticket can never contain your key. Generate a new one, update your secret store, and revoke the old one; all three events are recorded in the append-only audit log.

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.