Folio
FeaturesFor teachersFor institutionsPricing
Resources
About
Back to Help Center
Help Articlesv1

Institution API v1 and webhook reference

Endpoint, scope, pagination, error, payload, signature and delivery contracts for institution integrations.

F

Folio Team

September 4, 2026 9 min read

On this page

  • Base URL and authentication
  • Version lifecycle and compatibility
  • Scopes
  • Pagination
  • Get the roster
  • Get enrollments
  • Get the audit trail
  • Errors and rate limits
  • Webhook events
  • Verify a webhook signature
  • Delivery and retry semantics

This is the developer contract for Folio's read-only institution API and outbound webhooks. For key and endpoint setup in the administrator console, see Institution API and webhooks.

Base URL and authentication

The API version is part of the path:

https://www.usefolio.co/api/v1/institutions/{institution_id}

Send an institution API key in the Authorization header on every request. Keys begin with fk_inst_, belong to one institution, and are shown only when created or rotated.

curl --fail-with-body \
  -H "Authorization: Bearer $FOLIO_API_KEY" \
  "$FOLIO_ORIGIN/api/v1/institutions/$FOLIO_INSTITUTION_ID/roster?limit=2"

The institution id in the path must be the institution that issued the key. A key for a different institution receives 401, even if it has the required scope.

Version lifecycle and compatibility

Institution API v1 is generally available, supported, and not currently deprecated. Every success and error response carries Folio-API-Version: 1. While v1 remains supported, responses do not carry Deprecation or Sunset headers.

Within v1, Folio can add endpoints, optional query parameters, and optional or nullable response fields without advance notice. Clients must ignore response fields they do not recognize. Folio can also fix defects or security issues while preserving the documented request, response, authorization, and tenant-isolation contract, and can raise a documented limit.

A change is breaking when an existing conforming client must change to preserve the same documented behavior. Examples include removing or renaming an endpoint, parameter, or response field; requiring an existing optional input; changing a field's type or documented meaning; changing an enum that is not explicitly open-ended; narrowing a scope's documented access; lowering a documented limit; or changing pagination, authentication, or error semantics. Breaking changes require a new API version and owner approval.

If Folio deprecates v1, it will publish the deprecation date and provide at least 365 days before the earliest sunset date. For that entire notice period, Folio will keep v1 operational with security and correctness fixes, keep a generally available replacement available, and publish a migration guide and lifecycle dates in this reference and the changelog. Every v1 success and error response will then carry:

HeaderValue during the deprecation window
Deprecation@<unix_seconds> for the published deprecation date
Sunset<HTTP-date> for the earliest retirement date
Link<...>; rel="deprecation"; type="text/html" linking to the migration guidance

Folio can retire v1 only after the full notice window has elapsed, the generally available replacement and migration guide have remained available throughout it, and the owner has approved retirement. The response headers and published reference are the canonical client notifications; an email to an administrator is only an additional courtesy. Reaching the announced sunset date permits retirement but does not require Folio to switch v1 off that day.

Scope checks and institution isolation are security boundaries, not compatibility promises. Folio can tighten enforcement immediately when needed to restore the documented boundary; it will not broaden a scope or weaken tenant isolation as a v1-compatible change.

Scopes

ScopeRequired forData granted
roster.readGET /rosterAdmitted-student identity, status, program and directory-restriction state
enrollments.readGET /enrollmentsCourse memberships for students and teaching staff
grades.readEnrollment response fieldAdds final_grade to each enrollment row; it does not grant /enrollments by itself
audit.readGET /auditInstitution audit events for a SIEM or data warehouse
scim.writeSCIM 2.0 provisioningCreates, updates and deactivates institution users through the separate SCIM API; it grants no /api/v1 access

Grant only the scopes a connector needs. grades.read is deliberately additive: a key needs both enrollments.read and grades.read to receive grades. Keep scim.write on a separate key from read-only integrations so each credential has one job.

Pagination

roster and enrollments use offset pagination. Pass the returned next_offset as the next request's offset; stop when it is null.

audit uses an exclusive time cursor. Pass the returned next_since as the next request's since; stop when it is null. Rows with created_at equal to the cursor are not returned again.

All three endpoints default to 200 rows and cap limit at 500. Responses are JSON and have a top-level data array.

Get the roster

GET /api/v1/institutions/{institution_id}/roster

Requires roster.read. Rows are ordered by admitted_at, oldest first. Institution connectors receive directory-restricted students because the institution controls its own roster; downstream public directories must honor directory_restricted.

Query parameterMeaning
limitPage size from 1 to 500; default 200
offsetZero-based row offset; default 0
curl --fail-with-body \
  -H "Authorization: Bearer $FOLIO_API_KEY" \
  "$FOLIO_ORIGIN/api/v1/institutions/$FOLIO_INSTITUTION_ID/roster?limit=2&offset=0"
{
  "data": [
    {
      "user_id": "7fd34ff7-7fb1-440f-a456-13bc29901133",
      "student_id": "20260041",
      "name": "Leila Haddad",
      "email": "leila.haddad@example.edu",
      "status": "active",
      "program": "Computer Science",
      "directory_restricted": false,
      "admitted_at": "2026-08-18T09:30:00.000Z"
    }
  ],
  "next_offset": null
}

student_id, name, email, status, and program can be null. directory_restricted is always a boolean.

Get enrollments

GET /api/v1/institutions/{institution_id}/enrollments

Requires enrollments.read. Rows are ordered by joined_at, oldest first. Template courses are excluded.

Query parameterMeaning
termExact course-term value; omit it to include every term
limitPage size from 1 to 500; default 200
offsetZero-based row offset; default 0
curl --fail-with-body \
  -H "Authorization: Bearer $FOLIO_API_KEY" \
  "$FOLIO_ORIGIN/api/v1/institutions/$FOLIO_INSTITUTION_ID/enrollments?term=Fall%202026&limit=2&offset=0"

With enrollments.read and grades.read:

{
  "data": [
    {
      "id": "26f7bf06-73e4-4a20-90f4-6613a02692e2",
      "course_id": "1d7f20f9-e198-49b1-b554-f2970474028d",
      "course_code": "CS-204",
      "course_title": "Data Structures",
      "term": "Fall 2026",
      "course_status": "active",
      "user_id": "7fd34ff7-7fb1-440f-a456-13bc29901133",
      "role": "student",
      "status": "active",
      "joined_at": "2026-08-20T11:00:00.000Z",
      "ended_at": null,
      "final_grade": "A-"
    }
  ],
  "next_offset": null
}

Without grades.read, the final_grade property is omitted rather than returned as null. course_code, course_title, term, course_status, ended_at, and—when present—final_grade can be null.

Get the audit trail

GET /api/v1/institutions/{institution_id}/audit

Requires audit.read. Rows are ordered by created_at, oldest first, which makes the endpoint suitable for incremental ingestion.

Query parameterMeaning
sinceValid ISO-8601 timestamp; only rows strictly newer than it are returned
limitPage size from 1 to 500; default 200
curl --fail-with-body \
  -H "Authorization: Bearer $FOLIO_API_KEY" \
  "$FOLIO_ORIGIN/api/v1/institutions/$FOLIO_INSTITUTION_ID/audit?since=2026-09-01T00%3A00%3A00.000Z&limit=2"
{
  "data": [
    {
      "id": "0a4f6778-187c-4da4-b20f-b7082cd9d848",
      "action": "integration.key_rotated",
      "actor_user_id": "61ca5287-eafa-478f-a03b-452682b41921",
      "target_type": "api_key",
      "target_id": "608a1bbe-45a9-4ebd-b980-80229a6d52ed",
      "summary": "Rotated API key \"Warehouse sync\"",
      "created_at": "2026-09-03T08:12:00.000Z"
    }
  ],
  "next_since": null
}

target_type, target_id, and summary can be null. An absent or invalid since value currently starts from the oldest available audit event; validate stored cursors before sending them.

Errors and rate limits

JSON errors use the shape { "error": "Human-readable message" }.

StatusMeaning
401Missing or malformed bearer key, unknown key, revoked key, or key issued by a different institution
403The key is valid for this institution but lacks the endpoint's required scope
429This key has used its 1,000-request hourly budget
500Folio could not read the requested institution data

The limit is 1,000 requests per hour per API key, shared by all three endpoints. A 429 response does not currently include a Retry-After header. Back off until the next hourly window rather than retrying immediately.

{
  "error": "This key lacks the audit.read scope."
}

Webhook events

Folio sends one delivery for each enabled endpoint subscribed to the event.

Eventdata payload
enrollment.changedcourse_id, course_code, course_title, user_id, status, self_service, actor_user_id
grade.postedcourse_id, course_code, course_title, user_id, final_grade, actor_user_id
request.submittedrequest_id, request_type_id, request_type_key, request_type_name, student_user_id
hold.placedhold_id, user_id, type, blocks, reason, actor_user_id
pingmessage, sent_by; emitted only by Send test

Every delivery is an HTTP POST with Content-Type: application/json and User-Agent: Folio-Webhooks/1:

{
  "id": "739eec89-48a8-489a-ae73-2ef0a82486a7",
  "event": "grade.posted",
  "created_at": "2026-09-03T08:12:00.000Z",
  "institution_id": "32d1297a-fb8e-4558-aace-cf7feb8495b5",
  "data": {
    "course_id": "1d7f20f9-e198-49b1-b554-f2970474028d",
    "course_code": "CS-204",
    "course_title": "Data Structures",
    "user_id": "7fd34ff7-7fb1-440f-a456-13bc29901133",
    "final_grade": "A-",
    "actor_user_id": "61ca5287-eafa-478f-a03b-452682b41921"
  }
}

The request also carries:

HeaderValue
Folio-EventEvent name, such as grade.posted
Folio-Delivery-IdStable delivery UUID; use it as the idempotency key
Folio-Signaturet=<unix_seconds>,v1=<lowercase_hex_hmac>

Verify a webhook signature

Compute HMAC-SHA256 over the UTF-8 bytes of "<timestamp>.<raw request body>". Verify the signature before parsing or re-serializing JSON, reject timestamps more than five minutes away from your clock, and compare digests in constant time.

import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyFolioWebhook(secret, signatureHeader, rawBody, now = Date.now()) {
  if (!signatureHeader) return false
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(part => part.trim().split('='))
  )
  const timestamp = Number(parts.t)
  if (!Number.isFinite(timestamp) || !parts.v1) return false
  if (Math.abs(now - timestamp * 1000) > 5 * 60 * 1000) return false

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest()
  const received = Buffer.from(parts.v1, 'hex')
  return received.length === expected.length && timingSafeEqual(received, expected)
}

Keep the raw body available in your framework. Calling a JSON body parser first and then signing JSON.stringify(parsedBody) can change whitespace or key order and reject a genuine delivery.

Delivery and retry semantics

Return any 2xx status within 8 seconds. Redirects and every non-2xx response count as failures. Folio records at most the first 300 characters of the response body in the institution's delivery log.

AttemptWhen it runs
1Immediately when the event is emitted
21 minute after the first failure
35 minutes after the second failure
430 minutes after the third failure
52 hours after the fourth failure
612 hours after the fifth failure

After the sixth failure the delivery is marked dead. An administrator can manually retry it from the delivery log. Pausing a webhook leaves pending deliveries recorded but prevents scheduled attempts until the endpoint is enabled again.

Delivery is at least once. Store Folio-Delivery-Id before applying the event and return success for a duplicate id. Do not use the timestamp or event name as a deduplication key.

Webhook endpoints must use https, resolve to a public hostname, contain no URL credentials, and respond directly; Folio does not follow redirects.

Was this helpful?

Discussion

Sign in to join the discussion →

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

Folio

The integrated research workspace: discover, read, write, cite, and prove your work.

Built for academic integrityGDPR compliant

Product

  • Features
  • For PhD students
  • For teachers
  • For institutions
  • Literature reviews
  • Discovery
  • Research Radar
  • Integrity
  • Surveyor
  • Classroom
  • Browser extension
  • Pricing

Resources

  • Free tools
  • Folio Studio
  • Citation generator
  • Reference checker
  • Word counter
  • How to cite
  • Validated scales
  • Guides
  • Templates
  • Compare
  • Blog
  • Changelog
  • Help center
  • All resources

Company

  • About
  • Careers
  • Sign up
  • Log in
  • Contact

Legal

  • Terms
  • Privacy
  • Trust & security
  • GDPR & data rights
  • Refund policy
  • Academic integrity

© 2026 Folio. All rights reserved.

Made for researchers.