Developers
A REST API with 558 endpoints, district-scoped API keys, scopes mapped to permissions, and an OpenAPI 3.1 spec you can import into any client generator.
1. Sign in to your district admin account.
Navigate to /admin/api-keys — only roles with the api-key.manage permission see this page (district_admin has it by default).
2. Generate a key.
Pick the scopes your integration needs — every scope corresponds 1:1 with a platform permission code (see the table below). Optionally set a rate limit, IP allowlist, and expiry. The plaintext key is shown once — copy it somewhere safe.
3. Call the API.
curl
curl -H "Authorization: Bearer fsis_secret_<your-key>" \ https://omniforgesis.click/__api/v1/persons?personType=STUDENT&limit=10
JavaScript (Node 20+)
const res = await fetch(
'https://omniforgesis.click/__api/v1/persons?personType=STUDENT&limit=10',
{ headers: { Authorization: `Bearer ${process.env.FORGE_API_KEY}` } },
);
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const { data } = await res.json();
console.log(`fetched ${data.length} students`);4. Handle errors.
Every error response is a typed JSON envelope with a stable error.code field. See the table further down for the full list of API-key-specific codes.
Two equivalent ways to pass an API key on every request:
Bearer (recommended)
Authorization: Bearer fsis_secret_<64-hex>
Custom header
X-API-Key: fsis_secret_<64-hex>
Keys are scoped to a single district by issuance. They cannot cross tenants. We store SHA-256 of the key — the plaintext is unrecoverable if you lose it. Rotate by issuing a new key and revoking the old one.
A scope is the permission code the API checks on a route. Pick the narrowest set your integration needs. Custom permission codes (e.g. fees.assess) are accepted as scopes too if your route requires them.
| Scope | Grants |
|---|---|
| student.read | List + read student records |
| attendance.read | Daily + period attendance |
| attendance.write | Submit attendance marks |
| grade.read | Section grades + report cards |
| assignment.read | Assignment definitions + scores |
| guardian.read | Parent/guardian records |
| enrollment.read | Roster + active enrollments |
| fees.read | Student fees + payments |
| incident.read | Discipline + HIB incidents |
| health.read | Visit log, medications, screenings |
| transportation.read | Bus routes + assignments |
Each key has an optional rateLimit.requestsPerMinute ceiling set at issuance. Enforcement is per-key, fixed one-minute windows.
When you exceed the ceiling, you get:
HTTP/2 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"error": {
"code": "auth.api_key_rate_limited",
"message": "API key exceeded 60 requests/minute.",
"traceId": "cor_..."
}
}Honor the Retry-After header. There is also a baseline IP-level rate limit (600/min) applied across all unauthenticated traffic — keyed requests count against the per-key ceiling instead.
| Code | HTTP | When |
|---|---|---|
| auth.api_key_malformed | 401 | Key doesn't match the fsis_<type>_<hex> shape. |
| auth.api_key_unknown | 401 | Key not found in the platform registry. |
| auth.api_key_revoked | 401 | Key has been revoked from the district admin panel. |
| auth.api_key_expired | 401 | Key passed its expiresAt timestamp. |
| auth.api_key_ip_not_allowed | 403 | Caller IP not in the key’s ipAllowlist. |
| auth.api_key_rate_limited | 429 | Exceeded requestsPerMinute. Retry-After header set. |
| auth.forbidden | 403 | Key valid but its scopes don’t cover the route’s required permission. |
Subscribe a URL to one or more event types. We POST a signed envelope when those events fire — usually within a second of the underlying mutation.
1. Create the subscription
curl
curl -X POST https://omniforgesis.click/__api/v1/webhooks \
-H "Authorization: Bearer fsis_secret_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com/forge/webhook",
"events": ["IncidentReported", "AttendanceMarked"],
"description": "Reporting integration"
}'The signingSecret in the response is shown only once.
2. Receive a delivery
POST to your URL
POST /forge/webhook HTTP/1.1
Content-Type: application/json
User-Agent: forge-sis-webhooks/1
X-Forge-Event: IncidentReported
X-Forge-Event-Id: 47053415-9408-4fd8-9713-50972a5c95fb
X-Forge-District-Id: 0a6e77d3-d626-4b23-8b13-760eb1badec4
X-Forge-Signature: sha256=eeb89bfc7a106df5e71b4db75db12474f38791174cfd2edd1c833fbf5190381a
{
"eventId": "47053415-9408-4fd8-9713-50972a5c95fb",
"eventType": "IncidentReported",
"districtId": "0a6e77d3-d626-4b23-8b13-760eb1badec4",
"payload": { "incidentId": "...", "participants": [...] },
"sentAt": "2026-05-21T09:18:11.338Z"
}3. Verify the signature
Node 20+
import crypto from 'node:crypto';
export function verifyForgeSignature(rawBody, headerValue, signingSecret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', signingSecret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(headerValue ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Use crypto.timingSafeEqual (or equivalent) to avoid timing side channels.
Retry policy
5xx and network failures are retried with exponential backoff. 4xx responses are recorded but not retried — fix the receiver before re-enabling. Past attempts are visible at /admin/webhooks (per subscription).
Every event you can subscribe a webhook to, with the exact payload shape your receiver will get. Each event also flows through audit + the realtime hub — your webhook delivery wraps the same body in the standard envelope{ eventId, eventType, districtId, payload, sentAt }.
AssessmentResultsImported
A batch of state assessment results was imported into the district (NJSLA / SBAC / STAAR).
Example payload
{
"districtId": "00000000-0000-0000-0000-000000000000",
"importId": "00000000-0000-0000-0000-000000000000",
"adapter": "njsla",
"filename": "string",
"successRows": 0,
"errorRows": 0,
"importedByUserId": "string"
}AssignmentCreated
A teacher created a new assignment in the gradebook.
Example payload
{
"assignmentId": "00000000-0000-0000-0000-000000000000",
"sectionId": "00000000-0000-0000-0000-000000000000",
"name": "string",
"category": "string",
"pointsPossible": 0,
"dueAt": "2026-05-21T12:00:00.000Z"
}AssignmentScored
A student was assigned a score on an assignment.
Example payload
{
"assignmentId": "00000000-0000-0000-0000-000000000000",
"studentId": "00000000-0000-0000-0000-000000000000",
"points": 0,
"percent": 0,
"submittedAt": "2026-05-21T12:00:00.000Z"
}AttendanceMarked
Attendance for a student was recorded (daily or period).
Example payload
{
"studentId": "00000000-0000-0000-0000-000000000000",
"sectionId": "00000000-0000-0000-0000-000000000000",
"period": "string",
"date": "2026-05-21",
"status": "present",
"code": "string"
}CommunicationSent
A communication was delivered (or failed to deliver).
Example payload
{
"channel": "email",
"recipients": [
"string"
],
"templateId": "string",
"status": "queued"
}CounselorAssignment.Created
A student was assigned (or re-assigned) to a counselor. Reassignments end-date the prior row first.
Example payload
{
"assignmentId": "00000000-0000-0000-0000-000000000000",
"counselorPersonId": "00000000-0000-0000-0000-000000000000",
"studentPersonId": "00000000-0000-0000-0000-000000000000",
"schoolId": "00000000-0000-0000-0000-000000000000",
"isPrimary": false
}CounselorAssignment.Ended
A counselor assignment was end-dated.
Example payload
{
"assignmentId": "00000000-0000-0000-0000-000000000000",
"endDate": "2026-05-21"
}FeeAssessed
A fee was charged to a specific student (registrar assigning registration, teacher flagging field-trip opt-in, etc.).
Example payload
{
"studentFeeId": "00000000-0000-0000-0000-000000000000",
"studentPersonId": "00000000-0000-0000-0000-000000000000",
"feeCatalogId": "00000000-0000-0000-0000-000000000000",
"code": "string",
"description": "string",
"amountCents": 0,
"dueOn": "2026-05-21",
"schoolYearId": "00000000-0000-0000-0000-000000000000",
"assessedByUserId": "00000000-0000-0000-0000-000000000000"
}FeePaid
Money was applied to a student fee (online via Stripe, in-office check/cash, or an adjustment).
Example payload
{
"paymentId": "00000000-0000-0000-0000-000000000000",
"studentFeeId": "00000000-0000-0000-0000-000000000000",
"amountCents": 0,
"paymentMethod": "stripe",
"processor": "stripe",
"processorIntentId": "string",
"processorChargeId": "string",
"processorStatus": "succeeded",
"referenceId": "string",
"paidByUserId": "00000000-0000-0000-0000-000000000000",
"balanceAfterCents": 0
}IncidentActionAssigned
A consequence (ISS, OSS, detention, referral) was assigned for an incident.
Example payload
{
"incidentId": "00000000-0000-0000-0000-000000000000",
"studentId": "00000000-0000-0000-0000-000000000000",
"actionType": "string",
"durationDays": 0
}IncidentReported
A discipline incident was reported.
Example payload
{
"incidentId": "00000000-0000-0000-0000-000000000000",
"date": "2026-05-21T12:00:00.000Z",
"location": "string",
"offenseCode": "string",
"participants": [
{
"studentId": "00000000-0000-0000-0000-000000000000",
"role": "offender"
}
]
}M365SyncCompleted
A Microsoft 365 user or Teams sync completed for a district.
Example payload
{
"districtId": "00000000-0000-0000-0000-000000000000",
"created": 0,
"updated": 0,
"failed": 0,
"syncType": "users"
}MessageBounced
A message bounced or was rejected. Hard bounces suppress future sends.
Example payload
{
"communicationId": "00000000-0000-0000-0000-000000000000",
"districtId": "00000000-0000-0000-0000-000000000000",
"channel": "string",
"provider": "string",
"recipientAddress": "string",
"bounceType": "hard",
"reason": "string"
}MessageDelivered
A message was successfully delivered to a recipient by the provider.
Example payload
{
"communicationId": "00000000-0000-0000-0000-000000000000",
"districtId": "00000000-0000-0000-0000-000000000000",
"channel": "string",
"provider": "string",
"recipientAddress": "string",
"providerMessageId": "string",
"deliveredAt": "2026-05-21T12:00:00.000Z"
}OneRosterExportGenerated
A OneRoster 1.2 CSV ZIP export was generated for a district.
Example payload
{
"districtId": "00000000-0000-0000-0000-000000000000",
"asOf": "string",
"fileCount": 0,
"rowCount": 0
}ReportCardPublished
A report card was published for a student for a term.
Example payload
{
"studentId": "00000000-0000-0000-0000-000000000000",
"termId": "00000000-0000-0000-0000-000000000000",
"url": "https://example.com"
}Section504Plan.Created
A new Section 504 plan was drafted for a student following an eligibility determination.
Example payload
{
"planId": "00000000-0000-0000-0000-000000000000",
"studentId": "00000000-0000-0000-0000-000000000000",
"disability": "string",
"majorLifeActivity": "string",
"planStartDate": "2026-05-21",
"planEndDate": "2026-05-21"
}SectionGradePosted
A final section grade was posted for a student for a term.
Example payload
{
"sectionId": "00000000-0000-0000-0000-000000000000",
"studentId": "00000000-0000-0000-0000-000000000000",
"termId": "00000000-0000-0000-0000-000000000000",
"letterGrade": "string",
"percent": 0,
"gpaValue": 0
}StudentEnrolled
A student began an enrollment at an organization.
Example payload
{
"personId": "00000000-0000-0000-0000-000000000000",
"organizationId": "00000000-0000-0000-0000-000000000000",
"gradeLevel": "string",
"startDate": "2026-05-21"
}StudentWithdrawn
A student withdrew from an organization.
Example payload
{
"personId": "00000000-0000-0000-0000-000000000000",
"exitReason": "string",
"exitCode": "string",
"endDate": "2026-05-21"
}TranscriptGenerated
A transcript was generated for a student.
Example payload
{
"studentId": "00000000-0000-0000-0000-000000000000",
"generatedByUserId": "00000000-0000-0000-0000-000000000000",
"purpose": "string"
}Need an event we don't expose yet? Open a ticket — the underlying event log captures more than is currently subscribable.
Interactive Swagger UI
Try every endpoint in the browser. Authenticate with your district admin session or paste an API key.
OpenAPI 3.1 JSON
Raw spec — generate a TypeScript / Python / Go client, or wire it into your own gateway.
Postman collection
One-click import into Postman. Bearer auth pre-wired to a FORGE_API_KEY collection variable — paste your key and run any request.
Manage API keys
Sign in as a district admin to issue, rotate, and revoke keys for your district.
Manage webhooks
Subscribe URLs to event types and inspect delivery history per webhook.
Pricing & contact
API access is included for every district. Get in touch for a tailored quote.