Institution administrators manage integrations from the console under Security & IT โ Integrations. Everything on that page is audited under integration.* and reaches the institution's security alert subscribers.
API keys
A key is scoped to the institution it was created in and to the scopes you pick. It is shown once; Folio stores only a hash. Rotate a key to mint a replacement with the same scopes and revoke the old one in the same step.
| Scope | Grants |
|---|---|
roster.read | Every admitted student: id, name, email, status, track, directory-restriction flag |
enrollments.read | Every course membership (students and teaching staff) with status and dates |
grades.read | Adds final_grade to enrollment rows |
audit.read | The institution audit trail, for a SIEM or warehouse |
Send the key as a bearer token:
curl -H "Authorization: Bearer fk_inst_..." \
"https://www.usefolio.co/api/v1/institutions/<institution-id>/roster?limit=200"
Endpoints (all GET, JSON, limit up to 500):
/api/v1/institutions/{id}/roster?limit=&offset=โ{ data: [...], next_offset }/api/v1/institutions/{id}/enrollments?term=&limit=&offset=โ{ data: [...], next_offset }/api/v1/institutions/{id}/audit?since=<ISO>&limit=โ{ data: [...], next_since }(oldest first; passnext_sinceback to continue)
Each key is limited to 1,000 requests per hour. A 401 means the key is unknown or revoked; a 403 means it lacks the scope; a 429 means the hour's budget is spent.
Webhooks
Register an https URL on a public host and pick the events you want. Folio tries each delivery once immediately, then retries failures after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After the sixth failure the delivery is marked dead; you can retry it by hand from the delivery log.
| Event | Fires when |
|---|---|
enrollment.changed | Staff change a course membership's status or a student leaves a course |
grade.posted | A final grade is posted on a course membership |
request.submitted | A student submits an institution request (waiver, appeal, form) |
hold.placed | The records office places a hold on a student |
A delivery is a POST with a JSON body:
{
"id": "<delivery id>",
"event": "grade.posted",
"created_at": "2026-09-03T08:12:00.000Z",
"institution_id": "<institution id>",
"data": { "course_id": "โฆ", "user_id": "โฆ", "final_grade": "A-" }
}
Headers: Folio-Event, Folio-Delivery-Id, and Folio-Signature: t=<unix seconds>,v1=<hex>.
Verifying the signature
The signature is HMAC-SHA256 over "<t>.<raw body>" with the webhook's secret. Reject anything older than five minutes and compare in constant time:
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map(p => p.trim().split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
Respond with any 2xx within 8 seconds. Deliveries are at-least-once: use Folio-Delivery-Id to ignore a repeat.
The secret is shown once when the webhook is created and again only if you rotate it. Send test posts a signed ping delivery so you can confirm your receiver before real events flow.