Back to Help Center
Help Articles

Institution API and webhooks

Connect your SIS, warehouse or CRM to Folio: scoped read-only API keys, signed outbound webhooks, retries and the delivery log.

F

Folio Team

September 3, 2026 5 min read

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.

Building the receiving system? Use the Institution API v1 and webhook reference for endpoint schemas, pagination, errors, event payloads, signature verification, and retry behavior.

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.

ScopeGrants
roster.readEvery admitted student: id, name, email, status, track, directory-restriction flag
enrollments.readEvery course membership (students and teaching staff) with status and dates
grades.readAdds final_grade to enrollment rows
audit.readThe 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; pass next_since back 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.

EventFires when
enrollment.changedStaff change a course membership's status or a student leaves a course
grade.postedA final grade is posted on a course membership
certificate.issuedA course-completion certificate is issued to a student
request.submittedA student submits an institution request (waiver, appeal, form)
hold.placedThe 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.

CRM sync

Webhooks give you the raw event feed. If what you want instead is the same lifecycle as CRM contacts — one record per person, showing where they came from and how far they got — connect a CRM on the same Integrations page. HubSpot is supported today.

Create a private app in HubSpot with contact read and write scopes and paste its pat-… token. Folio verifies it before storing it, keeps it encrypted, and never shows it again; replace it any time without re-creating the connection.

Before you connect, create these contact properties in HubSpot:

PropertyHolds
folio_institutionThe institution's name
folio_course_codeThe course the last lifecycle event was about
folio_statuslead, enrolled, completed or dropped
folio_signup_sourceThe channel that first brought this person to Folio — the UTM source when the link was tagged, otherwise the referring site or landing page
folio_last_synced_atWhen Folio last wrote to the contact

Contacts are matched by email address; a person Folio holds no address for is skipped. Folio writes only the properties above and the contact's first and last name — never your own lifecycle stages, contact owners or deals.

Stages move like this:

StageSet when
leadSomeone joins the institution at sign-in — claiming an invitation, or admitted by your email domain
enrolledA course membership becomes active
completedA membership is marked completed, or a completion certificate is issued
droppedA membership is dropped or withdrawn

Pushes are queued and sent by a sweep that runs every 15 minutes, with the same retry ladder as webhooks; Sync now drains your queue immediately. A rejected token stops retrying at once rather than burning through the ladder, and shows on the connection so you can replace it. Connecting, pausing, re-keying and disconnecting are all audited under integration.crm_sync.

Was this helpful?

Discussion

No comments yet. Be the first to share your thoughts.