# Grout Developer Docs — full text Generated 2026-09-13T16:28:35.903Z. Source: https://grout.app/developer/documentation/ --- # Documentation Source: https://grout.app/developer/documentation/ ## How it fits together Grout is three apps and one portal. **GroutApp** is the offline-first AI learning studio, **GroutCode** is the coding studio with proctored coding exams, and the **Guardians portal** at audit.grout.app lets parents follow progress. Institutions administer all of it from **portal.grout.app**. The Institution API sits underneath the portal. Everything an admin can do by hand, your integration can do with an API key: create people, hand out licenses, sign students into the apps, run exams, credit AI merits, and get told when something happens. :::cards - [Institution Account](/developer/documentation/setting-up/institution-account/) — Register, get approved, and find the Developers tab in the portal. - [API Keys & Scopes](/developer/documentation/setting-up/api-keys/) — Least-privilege keys, rotation, rate limits. - [Webhooks](/developer/documentation/webhooks/overview/) — Signed, retried events for grades, licenses and more. ::: ## Who this is for - **LMS and SIS teams** syncing rosters and grades with Moodle, Canvas or an in-house system. - **Institution developers** invited by an admin from the portal. - **AI agents** — every page is available as markdown; start at [/developer/llms.txt](/developer/llms.txt) and read [AGENTS.md](/developer/AGENTS.md). ## Conventions used in these docs `$API` means `https://serverless.grout.app/v1` and `$KEY` is an institution API key. Request bodies are JSON unless a page says multipart. Timestamps are ISO-8601 UTC. Path parameters written `{id}` accept the internal id **or**, for people, the generated login email. --- # Why Grout? Source: https://grout.app/developer/documentation/why-grout/ ## One key, the whole suite A single institution-scoped key covers GroutApp, GroutCode and the Guardians portal. There is no per-app credential, no per-user OAuth dance, and no way for a key to reach another institution's data. ## Built for the school year | Moment | What the API does | |---|---| | Admissions | `POST /students` creates the login email and license in one call. Bulk endpoint for 200 at a time. | | Timetabling | Groups mirror your sections; exams are assigned to groups. | | Assessment | Coding exams with encrypted hidden tests, proctoring events, grading and CSV export. | | Results | `exam.graded` webhooks push scores to your gradebook within seconds. | | Renewal | `license.expired` events and `POST /licenses/{id}/extend`. | ## Privacy by default The API never returns a student's personal (guardian-facing) email, license keys, encrypted profile blobs or proctoring recordings unless the key explicitly holds `submissions:write`. Webhook payloads carry ids and outcomes, not documents. ## Reliability you can verify - Every response has an `X-Request-Id`. - Rate limits are per key and visible in headers. - Webhooks are signed, retried for up to 12 hours, and replayable from the portal. - Anything you can fetch by webhook you can also poll from `GET /events`. :::tip Next Head to the [Quick Start](/developer/documentation/quickstart/) — the first call takes under five minutes. ::: --- # Quick Start Source: https://grout.app/developer/documentation/quickstart/ :::steps ### Get access in the portal Sign in to [portal.grout.app](https://portal.grout.app) as an **Institution Admin** and open **Developers** in the sidebar. Either create an API key directly on the **API keys** tab, or **invite a developer** who will get their own dashboard. No portal account yet? Start with [Institution Account](/developer/documentation/setting-up/institution-account/). ### Create a key Give it a name and pick scopes. For this walkthrough choose `students:read` and `institution:read`. The full key (`grt_live_…`) is shown **once** — copy it into a secret manager. ### Call `/v1/me` :::codegroup ```bash title="cURL" curl https://serverless.grout.app/v1/me \ -H "Authorization: Bearer $GROUT_API_KEY" ``` ```js title="Node" const res = await fetch('https://serverless.grout.app/v1/me', { headers: { Authorization: `Bearer ${process.env.GROUT_API_KEY}` }, }); const { data } = await res.json(); console.log(data.institution.name, data.scopes); ``` ```python title="Python" import os, requests r = requests.get("https://serverless.grout.app/v1/me", headers={"Authorization": f"Bearer {os.environ['GROUT_API_KEY']}"}) print(r.json()["data"]["institution"]["name"]) ``` ::: You should see your institution, the key's scopes and its rate limit: ```json { "success": true, "data": { "key_id": "key_…", "name": "Quick start", "environment": "live", "institution": { "id": "…", "name": "Example College" }, "scopes": ["students:read", "institution:read"], "rate_limit_per_min": 600, "request_id": "req_…" } } ``` ### List your students ```bash curl "https://serverless.grout.app/v1/students?limit=5" \ -H "Authorization: Bearer $GROUT_API_KEY" ``` ### Pick a path :::cards - [Roster Sync](/developer/documentation/guides/roster-sync/) — Provision students and faculty from your SIS nightly. - [Gradebook Sync](/developer/documentation/guides/gradebook/) — Push exam results back with `exam.graded`. - [Webhooks](/developer/documentation/webhooks/overview/) — Register an endpoint and verify signatures. ::: ::: :::note Keys are live from the moment they are created. There is no sandbox environment yet; use a dedicated test institution or narrow scopes while you build. Sandbox keys (`grt_sandbox_…`) are on the roadmap. ::: --- # Institution Account Source: https://grout.app/developer/documentation/setting-up/institution-account/ The Institution API is only available to **approved institutions**. Everything starts on the portal at [portal.grout.app](https://portal.grout.app). :::steps ### Register the institution Go to [portal.grout.app/institution/register](https://portal.grout.app/institution/register) and fill in the institution details, the admin contact and how many student, faculty and lab-machine licenses you need. You will receive an email confirming the request. ### Wait for approval A Grout admin reviews the request. Once approved you get an email, licenses are generated in the background, and the admin account can sign in. Typical turnaround is one business day. Contact [hello@grout.app](mailto:hello@grout.app) to speed things up. ### Sign in as Institution Admin Open [portal.grout.app/login](https://portal.grout.app/login), choose **Institution Admin**, enter the admin email and the one-time code you receive. There are no passwords. ### Open Developers In the left sidebar choose **Developers**. The page has four tabs: | Tab | What it does | |---|---| | **Developers** | Invite developers by email, resend or revoke invitations, suspend accounts. | | **API keys** | Create keys with scopes and a rate limit; rename, re-scope, revoke; see usage. | | **Webhooks** | Register HTTPS endpoints, choose events, send a test ping, rotate secrets. | | **Deliveries** | Every delivery with status, attempts, response code; open the payload; replay. | Direct link: [portal.grout.app/institution/developers](https://portal.grout.app/institution/developers). ::: ## Admin vs developer | | Institution Admin | Developer | |---|---|---| | Sign in | Institution Admin tile on `/login` | Developer tile on `/login` (after accepting an invite) | | Home | `/institution` | `/developer` | | Invite developers | Yes | No | | Create / revoke API keys | Yes | Yes | | Webhooks & deliveries | Yes | Yes | | See licenses, batches, institution settings | Yes | No | Developers only see the developer platform; they cannot change licenses or institution settings. Suspending a developer ends their sessions immediately and, by default, revokes every key they created. ## What the API can see Everything is scoped to the institution the key belongs to. The API reads and writes the same tables the portal uses, so a student created by `POST /students` appears on the portal's license batches, and a group created in the portal is visible to `GET /groups`. :::warning Institution status If the institution is later suspended, every key stops working with `401 invalid_api_key` and webhooks pause. Re-approval restores them without re-issuing keys. ::: --- # Invite Developers Source: https://grout.app/developer/documentation/setting-up/developers/ Institution admins can hand integration work to a developer. The developer gets a dashboard with API keys, webhooks, delivery logs and these docs — nothing else. :::steps ### Send the invitation In the portal open **Developers → Developers** and click **Invite developer**. Enter the person's email (and optionally their name). They receive an email with a link that is valid for **7 days**. ### The developer accepts The link opens [portal.grout.app/accept-invitation](https://portal.grout.app/accept-invitation), where the developer confirms their name. That creates their account. ### The developer signs in They are sent to `/login` with the **Developer** tile pre-selected. Enter the invited email, receive a one-time code, and land on [portal.grout.app/developer](https://portal.grout.app/developer). ::: ## Managing developers - **Resend** an invitation that expired or was lost — this issues a new link and invalidates the old one. - **Revoke** a pending invitation. - **Suspend** a developer: their sessions end immediately and every key they created is revoked (you can keep the keys by unticking that option in the confirmation). **Reactivate** later without a new invitation. ## What a developer can do | Area | Developer | Admin | |---|---|---| | Create, edit, revoke API keys | ✓ | ✓ | | See key usage | ✓ | ✓ | | Webhook endpoints, test ping, rotate secret | ✓ | ✓ | | Delivery log and replay | ✓ | ✓ | | Invite or suspend developers | — | ✓ | | Licenses, batches, institution settings | — | ✓ | Keys created by a developer are tagged with their account, so an admin can see who issued what on the **API keys** tab. :::tip Keep one key per integration (for example "Moodle sync", "Gradebook", "Guardian onboarding") rather than one key per person. Keys survive people leaving; scopes stay tight. ::: --- # API Keys & Scopes Source: https://grout.app/developer/documentation/setting-up/api-keys/ ## Creating a key From **Developers → API keys** in the portal (admin) or **API keys** on the developer dashboard, click **Create API key**: 1. **Name** — describe the integration, not the person. 2. **Scopes** — tick only what the integration needs. `x:write` implies `x:read`. 3. **Rate limit** — requests per minute, 10 to 6 000. Default 600. The plaintext key (`grt_live_` + 32 characters) is shown **once**. The portal then only shows the prefix and the last four characters. Up to 25 active keys per institution. ## Scopes | Scope | Grants | |---|---| | `students:read` / `students:write` | List and read students · provision, update, suspend, map personal emails | | `faculty:read` / `faculty:write` | List and read faculty · provision, update | | `groups:read` / `groups:write` | Groups and membership | | `licenses:read` / `licenses:write` | Licenses and device seats · extend, revoke | | `exams:read` / `exams:write` | Exams, assignments · create, publish, files, hidden-test key | | `submissions:read` / `submissions:write` | Submissions, scores, proctoring events · grade, void (write also unlocks recordings) | | `coupons:read` / `coupons:write` | Coupon batches, redemptions · request batches | | `merits:read` / `merits:write` | Wallets and ledger · credit merits | | `guardians:read` / `guardians:write` | Guardian links and reports · invite, remove | | `alpha:read` / `alpha:write` | Alpha Learning data · enroll, award points | | `institution:read` / `institution:write` | Profile · integration config | | `webhooks:read` / `webhooks:write` | Endpoints, deliveries, events · create, rotate, replay | `GET /v1/scopes` returns the same list with descriptions. A request without the right scope gets `403 insufficient_scope` with `required` and `missing` arrays in the body. ## Recommended scope sets :::tabs ### Roster sync `students:write`, `faculty:write`, `groups:write`, `licenses:read` ### Gradebook `submissions:read`, `exams:read`, `webhooks:write` ### Exam authoring `exams:write`, `groups:read`, `faculty:read` ### Read-only reporting `students:read`, `licenses:read`, `submissions:read`, `merits:read`, `alpha:read` ::: ## Rotating a key There is no in-place rotation; create a new key, switch the integration, then revoke the old one. Revocation is immediate. Usage per key is visible under **Usage** on the keys table (requests, errors and rate-limited calls per day, top routes). ## Storing keys - Keep keys in a secret manager or environment variable. Never commit them, never ship them to a browser or mobile app. - The API accepts requests without an `Origin` header, so server-to-server is the intended mode. - If a key leaks, revoke it in the portal; the audit log records who created and revoked it. :::danger A key is as powerful as its scopes across your **entire** institution. Prefer several narrow keys over one broad key. ::: --- # Authentication Source: https://grout.app/developer/documentation/setting-up/authentication/ Send the key as a bearer token on every request: ```http GET /v1/students HTTP/1.1 Host: serverless.grout.app Authorization: Bearer grt_live_k3JdP8x… ``` There is no session, cookie or refresh token. The key identifies the institution; nothing else in the request does. ## Response envelope ```json { "success": true, "data": { … } } ``` ```json { "success": false, "error": "insufficient_scope", "message": "This endpoint requires scope(s): students:write", "required": ["students:write"], "missing": ["students:write"] } ``` Validation failures use `422 validation_error` with an `issues` array of `{ path, message }`. ## Errors | Status | `error` | Meaning | |---|---|---| | 401 | `unauthorized` | No bearer token, or the token is not a `grt_` key | | 401 | `invalid_api_key` | Unknown, revoked, expired, or institution not approved — deliberately indistinguishable | | 403 | `insufficient_scope` | See `required` / `missing` | | 404 | `not_found` | Resource does not exist **in your institution** | | 409 | `conflict` and friends | State conflict (already added, already verified, insufficient allowance…) | | 422 | `validation_error` and friends | Body or query invalid | | 429 | `rate_limited` | Honour `Retry-After` | | 500 | `internal_error` | Quote `X-Request-Id` to support | | 503 | `rate_limiter_unavailable` | Transient; retry after 5 s | ## Request ids Every response carries `X-Request-Id: req_…`. Log it next to your own correlation id. Support can trace a request end to end from it. ## CORS Browser calls from `*.grout.app` origins are allowed, but a key must never be embedded in client-side code. Call the API from your server and expose only what your users need. ## Checking a key `GET /v1/me` returns the key's name, scopes, rate limit and institution. Use it as a health check in your deploy pipeline: ```bash curl -fsS https://serverless.grout.app/v1/me -H "Authorization: Bearer $GROUT_API_KEY" > /dev/null \ && echo "key ok" ``` --- # Rate Limits & Errors Source: https://grout.app/developer/documentation/setting-up/rate-limits/ ## Limits Each key has its own limit, default **600 requests per minute**, configurable from 10 to 6 000 when the key is created or edited. The window is a fixed 60-second bucket. | Header | Meaning | |---|---| | `X-RateLimit-Limit` | Requests allowed per minute for this key | | `X-RateLimit-Remaining` | Requests left in the current window | | `X-RateLimit-Reset` | Unix time (seconds) when the window resets | | `Retry-After` | Only on 429/503: seconds to wait | When the limit is exceeded you get `429 rate_limited`. The limiter fails **closed**: if its backing store is unreachable you get `503 rate_limiter_unavailable` with `Retry-After: 5` rather than an unlimited pass. ## Backing off ```js async function call(url, init, attempt = 0) { const res = await fetch(url, init); if ((res.status === 429 || res.status === 503) && attempt < 5) { const wait = Number(res.headers.get('Retry-After') || 2 ** attempt); await new Promise((r) => setTimeout(r, wait * 1000)); return call(url, init, attempt + 1); } return res; } ``` ## Bulk work - Use `POST /students/bulk` (≤ 200 per call) instead of looping `POST /students`. - Page through lists with `limit=200`. - Prefer webhooks to polling; if you must poll, use `GET /events?since=` once a minute rather than re-listing resources. ## Idempotency Writes that carry a `ref` are idempotent on it: merit credits, Alpha point awards, webhook event replays. Provisioning calls are not idempotent — store the returned `login_email` and check before re-creating. A dedicated `Idempotency-Key` header is on the roadmap. ## Error reference | Status | Codes | |---|---| | 400 | `institution_required` (platform admins only) | | 401 | `unauthorized`, `invalid_api_key` | | 403 | `insufficient_scope`, `forbidden`, `institution_not_approved` | | 404 | `not_found`, `user_not_found`, `no_secret` | | 409 | `conflict`, `already_added`, `already_verified`, `user_inactive`, `insufficient_allowance`, `endpoint_inactive`, `void_failed` | | 410 | `event_pruned` | | 422 | `validation_error`, `invalid_faculty`, `invalid_scopes`, `no_owner`, `no_valid_students`, `https_required`, `private_host`, `events_required`, `endpoint_limit`, `key_limit`, `file_too_large` | | 429 | `rate_limited` | | 500 | `internal_error` | | 501 | `not_implemented` | | 503 | `rate_limiter_unavailable` | --- # Agent-Readable Surfaces Source: https://grout.app/developer/documentation/setting-up/agent-surfaces/ These docs are written for people and for coding agents. Every page has a markdown twin, and there is an index an agent can fetch first. | URL | What it is | |---|---| | [/developer/llms.txt](/developer/llms.txt) | Index of every page with a one-line summary, plus API reference and agent files | | [/developer/llms-full.txt](/developer/llms-full.txt) | Every documentation page concatenated into one file | | `/developer/documentation/.md` | The raw markdown of any page — add `.md` to the path | | [/developer/AGENTS.md](/developer/AGENTS.md) | How an agent should work with the Grout API and this docs site | | [/developer/SKILLS.md](/developer/SKILLS.md) | Task recipes with request templates: roster sync, gradebook, webhooks, exams, exams | | [/developer/openapi-v1.json](/developer/openapi-v1.json) | Vendored OpenAPI 3.1 document (live copy at `serverless.grout.app/v1/openapi.json`) | | [/developer/CHANGELOG.md](/developer/CHANGELOG.md) | Dated changes | ## Suggested agent workflow 1. Fetch `/developer/llms.txt` and pick the pages relevant to the task. 2. Read `/developer/SKILLS.md` for a matching recipe; each recipe lists scopes, endpoints and a request template. 3. Fetch `/developer/openapi-v1.json` when you need exact parameter names. 4. Call `GET /v1/me` first to confirm the key's scopes before doing any writes. 5. Emit `X-Request-Id` values in your logs and error messages. ## Using the API from an agent ```bash # Discover curl -s https://grout.app/developer/llms.txt # Confirm access curl -s https://serverless.grout.app/v1/me -H "Authorization: Bearer $GROUT_API_KEY" # Do the task (example: provision one student) curl -s -X POST https://serverless.grout.app/v1/students \ -H "Authorization: Bearer $GROUT_API_KEY" -H 'Content-Type: application/json' \ -d '{"full_name":"Asha Rao","student_id":"S-0142","personal_email":"asha@example.com"}' ``` :::note Keys are institution-wide. An agent operating with a broad key should confirm destructive actions (suspend, revoke, void, delete) with a human first. `SKILLS.md` marks those recipes accordingly. ::: --- # Students Source: https://grout.app/developer/documentation/people/students/ ## How student accounts work Every institution student signs in with a **generated login email** of the form `studentN.@grout.user`. One-time codes are delivered to the student's **personal email**, which is mapped on first login (or ahead of time by you). The personal email is never returned by the API. Provisioning creates three things atomically: the `users` row, the institution email record, and a **student license** (365 days by default, 2 devices). ## Create a student ```bash curl -X POST $API/students -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "full_name": "Asha Rao", "student_id": "S-2026-0142", "personal_email": "asha@example.com", "license_days": 365 }' ``` ```json { "success": true, "data": { "id": "u_…", "email": "student143.4f9a2b1c@grout.user", "login_email": "student143.4f9a2b1c@grout.user", "user_type": "institution_student", "full_name": "Asha Rao", "student_id": "S-2026-0142", "status": "pending", "personal_email_mapped": true, "license": { "id": "lic_…", "status": "active", "license_type": "student", "expires_at": "2027-09-14T…", "is_lifetime": false } } } ``` | Field | Notes | |---|---| | `full_name` | Optional; shown in the apps. | | `student_id` | Your SIS identifier; searchable via `?student_id=`. | | `personal_email` | Optional. When set, first login goes straight to the OTP screen. | | `license_days` | 1–3650, default 365. | `status` is `pending` until the first successful sign-in, then `active`. :::tip Store the login email The response's `login_email` is what the student types on `/login`. Save it (and `id`) against the SIS record so you never provision twice. ::: ## Bulk create `POST /students/bulk` takes `{ "students": [ …up to 200… ] }` and returns per-item results: ```json { "created": 198, "failed": 2, "results": [ { "index": 0, "id": "u_…", "login_email": "…" }, { "index": 7, "error": "…" } ] } ``` ## List and find ```bash curl "$API/students?status=active&limit=200&page=1" -H "Authorization: Bearer $KEY" curl "$API/students?student_id=S-2026-0142" -H "Authorization: Bearer $KEY" curl "$API/students?group_id=grp_…" -H "Authorization: Bearer $KEY" ``` `GET /students/{id}` accepts the internal id **or** the login email. ## Update, suspend, reactivate ```bash curl -X PATCH $API/students/u_… -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "full_name": "Asha K. Rao", "personal_email": "asha.new@example.com" }' curl -X POST $API/students/u_…/suspend -H "Authorization: Bearer $KEY" curl -X POST $API/students/u_…/reactivate -H "Authorization: Bearer $KEY" ``` Suspending ends the student's sessions immediately; their license stays and resumes on reactivation. ## Related reads - `GET /students/{id}/exams` — assigned exams with submission and attempt state. - `GET /students/{id}/guardians` — linked guardians. See [Guardians](/developer/documentation/people/guardians/). ## Events `student.created`, `student.email_mapped`, `student.updated`, `license.generated`. See [Event Catalogue](/developer/documentation/webhooks/events/). --- # Faculty Source: https://grout.app/developer/documentation/people/faculty/ Faculty accounts mirror students: a generated login email (`facultyN.@grout.user`), a **faculty license** (3 devices), and a one-time-code sign-in. On first login the teacher completes a short profile (employee id, department, position); until then `profile_completed` is `false`. ## Create ```bash curl -X POST $API/faculty -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "full_name": "Dr. Menon", "faculty_id": "F-031", "license_days": 365 }' ``` The response includes `login_email`. Faculty OTPs are routed to the institution admin's mailbox for the first login, then to the teacher's own address once their profile is complete. ## Why faculty matter to the API - Every **exam** has a faculty owner (`faculty_id`), required on `POST /exams`. - Every **group** has an owner; pass `faculty_id` or it defaults to the institution admin. - Grading via the API is recorded against the exam owner unless you pass `graded_by`. ## List, read, update ```bash curl "$API/faculty?status=active" -H "Authorization: Bearer $KEY" curl $API/faculty/faculty3.4f9a2b1c@grout.user -H "Authorization: Bearer $KEY" curl -X PATCH $API/faculty/u_… -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "status": "suspended" }' curl "$API/faculty/u_…/exams?page=1" -H "Authorization: Bearer $KEY" ``` ## Events `faculty.created`, `faculty.profile_completed`, `license.generated`. --- # Groups Source: https://grout.app/developer/documentation/people/groups/ A group is a named set of students with a faculty owner. Assign an exam to a group and every member sees it in the app; add a student to the group later and they see it too. ## Create ```bash curl -X POST $API/groups -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "name": "CS101 – Section A", "description": "Autumn 2026", "faculty_id": "faculty3.4f9a2b1c@grout.user", "student_ids": ["u_1", "u_2", "u_3"] }' ``` `faculty_id` may be an id or login email. Omit it and the institution admin becomes the owner. Unknown or foreign `student_ids` are skipped silently; the response's `member_count` tells you how many landed. ## Membership ```bash curl -X POST $API/groups/grp_…/members -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "student_ids": ["u_4"] }' curl -X DELETE $API/groups/grp_…/members/u_4 -H "Authorization: Bearer $KEY" ``` `PATCH /groups/{id}` with `student_ids` **replaces** the whole membership — handy for nightly section sync. ## Read ```bash curl "$API/groups?q=CS101" -H "Authorization: Bearer $KEY" curl $API/groups/grp_… -H "Authorization: Bearer $KEY" # members + assigned exams ``` ## Mapping LMS sections Keep a table `lms_section_id → grout_group_id`. On each sync: create missing groups, `PATCH` membership for existing ones, and delete groups whose section was archived. Because exam assignments hang off the group, deleting a group also unassigns its exams — archive instead if results still matter. ## Events `exam.assigned` fires per group when an exam is assigned. --- # Licenses & Devices Source: https://grout.app/developer/documentation/people/licenses/ Each institution user holds one license (`student`, `faculty` or `institution` for lab machines). A license has an expiry, a maximum number of activations and a status: `active`, `expired`, `suspended` or `revoked`. ## Read ```bash curl "$API/licenses?status=active&expires_before=2026-12-31T00:00:00Z" -H "Authorization: Bearer $KEY" curl $API/licenses/summary -H "Authorization: Bearer $KEY" # counts by type/status + expiring in 30 days curl $API/licenses/lic_… -H "Authorization: Bearer $KEY" ``` ## Extend ```bash curl -X POST $API/licenses/lic_…/extend -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "days": 180 }' curl -X POST $API/licenses/lic_…/extend -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "expires_at": "2027-06-30T00:00:00Z" }' ``` Extending an `expired` license reactivates it. ## Revoke ```bash curl -X POST $API/licenses/lic_…/revoke -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "reason": "left institution" }' ``` The app signs out on its next heartbeat (≤ 5 minutes). ## Devices Each app (`groutapp`, `groutcode`, `groutfilm`) allows **2 active device seats** per user. The oldest seat is signed out when a third device logs in. ```bash curl $API/licenses/lic_…/devices -H "Authorization: Bearer $KEY" curl -X POST $API/licenses/lic_…/devices//revoke -H "Authorization: Bearer $KEY" ``` The response lists `seats` (per app, with device name, platform, app version, last seen) and legacy `activations`. ## Expiry automation - `license.expired` fires when a license passes its expiry (daily sweep, or lazily when the app checks in). - Subscribe to it and call `POST /licenses/{id}/extend` from your billing system, or let it lapse. - `GET /licenses/summary` exposes `expiring_within_30_days` for dashboards. ## Events `license.generated`, `license.expired`, `license.device_registered`. --- # Guardians Source: https://grout.app/developer/documentation/people/guardians/ A guardian is linked to a student, accepts an email invitation, then signs in to [audit.grout.app](https://audit.grout.app) with a one-time code. They see exams, grades, submissions and weekly Alpha reports — never proctoring recordings. ## Invite ```bash curl -X POST $API/guardians -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "student_id": "u_…", "guardian_email": "parent@example.com", "guardian_type": "mother", "guardian_name": "R. Rao", "is_primary": true }' ``` `guardian_type` is `father`, `mother` or `guardian`. A student can have several guardians; `409 already_added` if the pair exists. ## Manage ```bash curl "$API/guardians?student_id=u_…" -H "Authorization: Bearer $KEY" curl "$API/guardians?verified=false" -H "Authorization: Bearer $KEY" # still pending curl -X POST $API/guardians/g_…/resend -H "Authorization: Bearer $KEY" curl -X DELETE $API/guardians/g_… -H "Authorization: Bearer $KEY" ``` `GET /students/{id}/guardians` returns the same rows for one student. ## Reports Weekly Alpha Learning reports are generated for each verified guardian: ```bash curl "$API/guardians/reports?since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY" ``` ## Events `guardian.invited` when a link is created; `guardian.verified` when the guardian first signs in. Neither carries the guardian's email. --- # Exams Overview Source: https://grout.app/developer/documentation/exams/overview/ An exam belongs to a **faculty owner** and an **app**: `grout` (document and office exams in GroutApp) or `groutcode` (coding exams). Students see an exam once it is **published** and **assigned** to a group they belong to. ## Create ```bash curl -X POST $API/exams -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "faculty_id": "faculty3.4f9a2b1c@grout.user", "title": "Data Structures Midterm", "exam_type": "exam", "max_score": 100, "due_date": "2026-10-01T09:00:00Z", "time_limit_minutes": 90, "allow_late_submission": false, "proctoring": { "camera": true, "screen": true }, "ai_policy": { "allowed": true, "model_tier": "local", "max_interactions": 20 }, "publish": true, "group_ids": ["grp_…"] }' ``` | Field | Notes | |---|---| | `faculty_id` | Required. Id or login email of the owning teacher. | | `exam_type` | `exam`, `homework`, `assignment`, `quiz`, `project` | | `time_limit_minutes` | Starts a countdown when the student opens the exam. | | `proctoring` | What the student must enable before the exam opens. See [Proctoring & AI Policy](/developer/documentation/exams/proctoring/). | | `ai_policy` | Rules for the in-app assistant. | | `code_spec` | Present ⇒ GroutCode coding exam. See [GroutCode Coding Exams](/developer/documentation/exams/groutcode/). | | `publish` | Default `true`. Pass `false` to keep a draft. | | `group_ids` | Assign in the same call; students and verified guardians are emailed. | ## Lifecycle ```bash curl -X POST $API/exams/ex_…/publish -H "Authorization: Bearer $KEY" curl -X POST $API/exams/ex_…/close -H "Authorization: Bearer $KEY" curl -X PATCH $API/exams/ex_… -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "due_date": "2026-10-02T09:00:00Z" }' curl -X DELETE $API/exams/ex_… -H "Authorization: Bearer $KEY" ``` ## Assignment ```bash curl -X POST $API/exams/ex_…/assignments -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "group_ids": ["grp_a", "grp_b"] }' curl $API/exams/ex_…/assignments -H "Authorization: Bearer $KEY" curl -X DELETE $API/exams/ex_…/assignments/grp_b -H "Authorization: Bearer $KEY" ``` ## Read ```bash curl "$API/exams?app=groutcode&status=published&updated_since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY" curl $API/exams/ex_… -H "Authorization: Bearer $KEY" # + groups, stats, code_secret_set curl $API/exams/ex_…/attempts -H "Authorization: Bearer $KEY" # who started, when, hidden-test release curl $API/exams/ex_…/scores -H "Authorization: Bearer $KEY" curl $API/exams/ex_…/analytics -H "Authorization: Bearer $KEY" curl $API/exams/ex_…/export.csv -H "Authorization: Bearer $KEY" -o results.csv ``` ## Events `exam.published`, `exam.assigned`, `exam.attempt_started`, `exam.submitted`, `exam.graded`. --- # GroutCode Coding Exams Source: https://grout.app/developer/documentation/exams/groutcode/ A coding exam is an exam with a `code_spec`. GroutCode installs the toolchain locally, opens the brief, runs sample tests on demand and, when the student asks, fetches the hidden-test key **once per attempt** so hidden tests can run offline without ever shipping the key inside the exam. ## `code_spec` ```json { "v": 1, "brief_md": "# Implement a stack\n…", "toolchain": "python", "starter": { "kind": "zip", "file": { "id": "file_…" } }, "sample_tests": { "kind": "inline", "files": [{ "path": "tests/sample/test_basic.py", "content": "…" }] }, "hidden_tests": { "kind": "zip-encrypted", "file": { "id": "file_…" }, "iv_b64": "…", "sha256": "…" }, "commands": { "setup": "", "run": "python main.py", "test_sample": "pytest {{workspace}}/tests/sample", "test_hidden": "pytest {{hidden}}", "timeout_ms": 60000 }, "ai_policy": { "allowed": false, "model_tier": "any" }, "rubric_md": "…", "exclude_globs": ["node_modules/**", ".venv/**"] } ``` | Field | Notes | |---|---| | `toolchain` | `c`, `cpp`, `java`, `python`, `node`, `sqlite`. Missing toolchains are installed on the student's machine. | | `starter` | `{kind:"zip", file}` uploaded via `POST /exams/{id}/files`, or `{kind:"empty"}`. | | `sample_tests` | Inline files (relative paths, no `..`) or a zip. Visible to the student. | | `hidden_tests` | AES-256-GCM-encrypted zip. `iv_b64` and the ciphertext `sha256` live in the spec; the **key does not**. | | `commands` | `{{workspace}}`, `{{hidden}}`, `{{results}}` are substituted. `timeout_ms` 1 000–600 000. | | `ai_policy` | Enforced locally; usage is reported at submit in `ai_usage_data`. | ## Hidden-test workflow :::steps ### Encrypt and upload Zip the hidden tests, encrypt with AES-256-GCM using a random 12-byte IV, and upload the ciphertext: ```bash curl -X POST $API/exams/ex_…/files -H "Authorization: Bearer $KEY" -F "file=@hidden-tests.zip.enc" ``` Put the returned `ref` in `code_spec.hidden_tests.file`, plus `iv_b64` and the ciphertext `sha256`. ### Store the key ```bash curl -X PUT $API/exams/ex_…/code-secret -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "hidden_key_b64": "" }' ``` The key is write-only for students; GroutCode obtains it through a release call that stamps the attempt. ### Watch releases ```bash curl $API/exams/ex_…/code-secret/status -H "Authorization: Bearer $KEY" curl $API/exams/ex_…/attempts -H "Authorization: Bearer $KEY" # hidden_released_at per student ``` ### Re-grade offline (optional) `POST /exams/{id}/code-secret/release` returns the key to an institution integration so you can decrypt and re-run hidden tests yourself. ::: ## Encrypting in practice ```js import { randomBytes, createCipheriv, createHash } from 'node:crypto'; import { readFileSync, writeFileSync } from 'node:fs'; const key = randomBytes(32), iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', key, iv); const ct = Buffer.concat([cipher.update(readFileSync('hidden-tests.zip')), cipher.final(), cipher.getAuthTag()]); writeFileSync('hidden-tests.zip.enc', ct); console.log({ hidden_key_b64: key.toString('base64'), iv_b64: iv.toString('base64'), sha256: createHash('sha256').update(ct).digest('hex') }); ``` ## What comes back at submit GroutCode uploads `workspace.zip`, `transcript.json` (AI transcript) and `test-results.json`. They appear as signed attachment URLs on `GET /submissions/{id}`, and `ai_usage_data` carries the local test results, timing, focus-loss and models used: ```json { "kind": "groutcode", "v": 1, "tests": { "sample": { "passed": 8, "failed": 0 }, "hidden": { "passed": 11, "failed": 1, "hidden_zip_sha256": "…", "workspace_sha256": "…" } }, "timing": { "started_at": "…", "hidden_released_at": "…", "submitted_at": "…" }, "focus": { "lost_count": 2, "lost_seconds": 41 }, "toolchain": "python", "app": { "name": "groutcode", "version": "2.4.0", "platform": "win32" }, "total_interactions": 0, "models_used": [] } ``` --- # Submissions & Grading Source: https://grout.app/developer/documentation/exams/submissions/ ## Status flow `submitted` → `graded` | `resubmission_required` | `voided`. A student resubmitting creates a new version (`version` increments); previous versions stay readable. ## List and read ```bash curl "$API/submissions?exam_id=ex_…&status=submitted" -H "Authorization: Bearer $KEY" curl "$API/submissions?student_id=student143.4f9a2b1c@grout.user&since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY" curl $API/submissions/sub_… -H "Authorization: Bearer $KEY" ``` `GET /submissions/{id}` returns the text, `attachments` and `feedback_attachments` as **signed URLs valid one hour**, `ai_usage_data`, `score_detail`, and a `proctoring` summary. Recording URLs are included only when the key holds `submissions:write`. ## Grade ```bash curl -X POST $API/submissions/sub_…/grade -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "score": 87, "feedback": "Good work", "graded_by": "faculty3.4f9a2b1c@grout.user" }' ``` - `score` must not exceed the exam's `max_score`. - `score: -1` requests a **resubmission**; students and verified guardians are emailed. - `graded_by` is optional; defaults to the exam owner. Late submissions have the exam's `late_penalty_percentage` applied automatically; `score_detail.percentage` reflects the final value. ## Void ```bash curl -X POST $API/submissions/sub_…/void -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "reason": "duplicate" }' ``` ## Export `GET /exams/{id}/export.csv` streams `email,student_id,status,score,max_score,percentage,is_late,submitted_at,graded_at`. ## Events `exam.submitted` on every version; `exam.graded` on grade **and** on resubmission request (`resubmission_required: true`). Pair with [Gradebook Sync](/developer/documentation/guides/gradebook/). --- # Proctoring & AI Policy Source: https://grout.app/developer/documentation/exams/proctoring/ ## Proctoring requirements `proctoring: { camera, microphone, screen }` on an exam lists what the student must grant before the exam opens. The app records in segments and uploads them with the submission. Recordings never leave the institution: guardians cannot see them, and the API only returns their URLs to keys with `submissions:write`. ## Event feed Every attempt keeps a rolling log of events the app observed. Read it per submission: ```bash curl $API/submissions/sub_…/events -H "Authorization: Bearer $KEY" ``` ```json { "attempt": { "id": "att_…", "started_at": "…", "expires_at": "…", "hidden_released_at": "…" }, "counts": { "focus_lost": 3, "sample_tests_run": 5, "submitted": 1 }, "events": [ { "type": "focus_lost", "at": "2026-09-14T10:03:12Z" }, { "type": "focus_gained", "at": "…", "detail": { "away_seconds": 14 } } ] } ``` | Type | Meaning | |---|---| | `attempt_started`, `attempt_resumed`, `attempt_abandoned` | Attempt lifecycle | | `focus_lost`, `focus_gained{away_seconds}` | Window lost/regained focus | | `screen_locked`, `screen_unlocked`, `system_suspend`, `system_resume` | OS-level interruptions | | `recording_started`, `recording_stopped`, `recorder_error`, `segment_uploaded`, `segment_upload_failed` | Recorder health | | `sample_tests_run{passed,failed}`, `hidden_tests_released` | GroutCode test activity | | `submitted{submission_id}` | Final submission | Use `counts` for quick flags (many `focus_lost`, a `recorder_error`) and the raw list for review. ## AI policy `ai_policy: { allowed, model_tier, max_interactions }` governs the in-app assistant for the exam: - `allowed: false` disables it entirely. - `model_tier`: `any`, `cloud` (hosted models only) or `local` (on-device models only). - `max_interactions`: budget per attempt. The policy is enforced by the app; what the student actually used is reported at submit in `ai_usage_data` (`total_interactions`, `models_used`, and for GroutCode the transcript attachment). Institution-wide defaults can be stored in `PUT /institution/config` (`default_ai_policy`, `default_proctoring`, `allowed_toolchains`); new exams created in the portal inherit them in a future app release. --- # Alpha Learning Source: https://grout.app/developer/documentation/learning/alpha/ Alpha Learning is Grout's two-hour mastery loop: a curriculum of concepts, daily plans, sessions and spaced review, with points, streaks and badges on top. The API exposes enrollment and progress so an LMS can show it or drive it. ## Curricula ```bash curl $API/alpha/curricula -H "Authorization: Bearer $KEY" # published; institution-owned + shared ``` ## Enroll ```bash curl -X POST $API/alpha/enrollments -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "curriculum_id": "cur_…", "student_ids": ["u_1", "u_2"], "daily_target_minutes": 120 }' curl -X PATCH $API/alpha/enrollments/enr_… -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "status": "paused" }' curl "$API/alpha/enrollments?student_id=u_1" -H "Authorization: Bearer $KEY" ``` ## Progress ```bash curl $API/alpha/students/u_1 -H "Authorization: Bearer $KEY" # drill-down curl $API/alpha/students/u_1/mastery -H "Authorization: Bearer $KEY" # per-concept score/status curl $API/alpha/students/u_1/points -H "Authorization: Bearer $KEY" # stats + ledger curl "$API/alpha/leaderboard?limit=20" -H "Authorization: Bearer $KEY" ``` ## Award points ```bash curl -X POST $API/alpha/students/u_1/points -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "points": 25, "note": "Science fair", "ref": "fair-2026" }' ``` ±100 per call, idempotent on `ref`. ## Events `alpha.points_awarded`, `alpha.badge_granted`. Guardians receive weekly reports; read them via `GET /guardians/reports`. --- # Merits & Coupons Source: https://grout.app/developer/documentation/learning/merits/ **Merits** are AI credits: 1 merit = ₹1 (100 cents) of model spend inside the apps. Each user has a wallet. Institutions receive a **merit allowance** they can distribute through the API. ## Allowance ```bash curl $API/merits/allowance -H "Authorization: Bearer $KEY" ``` Contact Grout to top the allowance up. Credits are refused with `409 insufficient_allowance` when it runs out. ## Credit a wallet ```bash curl -X POST $API/merits/credits -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "email": "student143.4f9a2b1c@grout.user", "amount_cents": 500, "reason": "semester_grant", "ref": "2026-S2:S-2026-0142" }' ``` Idempotent on `ref`: repeating the call returns `applied: false` and does not charge the allowance twice. The apps pick up the new limit on their next sync. ## Read ```bash curl $API/merits/wallets/student143.4f9a2b1c@grout.user -H "Authorization: Bearer $KEY" # balance + last 50 moves curl "$API/merits/ledger?since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY" # institution-wide ``` ## Coupons Coupons are redeemable codes that credit merits. The API files a **request**; a Grout admin approves it, after which codes appear on the batch. ```bash curl -X POST $API/coupons/batches -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{ "name": "Hackathon 2026", "total_coupons": 100, "value_cents": 500, "expiry_date": "2026-12-31T00:00:00Z" }' curl $API/coupons/batches -H "Authorization: Bearer $KEY" curl $API/coupons/batches/cb_… -H "Authorization: Bearer $KEY" # codes once approved curl "$API/coupons/redemptions?since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY" ``` ## Events `merits.credited` (any source), `coupon.batch_requested`, `coupon.batch_approved`, `coupon.redeemed`. --- # Webhooks Overview Source: https://grout.app/developer/documentation/webhooks/overview/ Grout POSTs a signed JSON envelope to your endpoint whenever a subscribed event happens. Respond `2xx` within 10 seconds and process asynchronously. ## Register an endpoint From the portal (**Developers → Webhooks → Add endpoint**) or the API: ```bash curl -X POST $API/webhooks/endpoints -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "url": "https://lms.example.edu/hooks/grout", "description": "Gradebook sync", "events": ["exam.*", "student.created", "license.expired"] }' ``` The response includes `secret` (`whsec_…`) **once**. Subscriptions accept `"*"`, a prefix such as `"exam.*"`, or exact types. Endpoints must be public HTTPS; private hosts and credentials in the URL are rejected. Up to 20 endpoints per institution. ## The envelope ```http POST /hooks/grout HTTP/1.1 Content-Type: application/json User-Agent: Grout-Webhooks/1.0 X-Grout-Event: exam.graded X-Grout-Delivery-Id: whd_… X-Grout-Signature: t=1789300000,v1=5f1c… { "id": "evt_…", "type": "exam.graded", "created_at": "2026-09-14T10:00:00.000Z", "institution_id": "…", "data": { "submission_id": "sub_…", "exam_id": "ex_…", "student_id": "u_…", "score": 87, "max_score": 100, "resubmission_required": false } } ``` Store `id` and ignore duplicates — deliveries are at-least-once. ## Test it **Send test ping** in the portal (or `POST /webhooks/endpoints/{id}/test`) delivers a `ping` event synchronously and shows you the response code and latency. ## Manage ```bash curl $API/webhooks/endpoints -H "Authorization: Bearer $KEY" curl -X PATCH $API/webhooks/endpoints/we_… -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "status": "paused" }' curl -X POST $API/webhooks/endpoints/we_…/rotate-secret -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{ "grace_hours": 24 }' curl -X DELETE $API/webhooks/endpoints/we_… -H "Authorization: Bearer $KEY" ``` :::cards - [Verify Signatures](/developer/documentation/webhooks/verify/) — Constant-time HMAC check in Node, Python and PHP. - [Event Catalogue](/developer/documentation/webhooks/events/) — Every type with a sample payload. - [Retries & Replay](/developer/documentation/webhooks/retries/) — Schedule, auto-disable, replay from the portal. ::: --- # Verify Signatures Source: https://grout.app/developer/documentation/webhooks/verify/ `v1 = HMAC_SHA256(secret, ".")` in hex, where `t` is the Unix timestamp in the header. Compare in constant time and reject if `|now − t| > 300 s`. During a secret rotation the header carries **two** `v1=` values — accept either. :::codegroup ```js title="Node" import crypto from 'node:crypto'; export function verifyGrout(rawBody, header, secret, toleranceSec = 300) { const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('='))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); return header.split(',').filter((kv) => kv.startsWith('v1=')).map((kv) => kv.slice(3)) .some((sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))); } // Express: keep the raw body app.post('/hooks/grout', express.raw({ type: 'application/json' }), (req, res) => { if (!verifyGrout(req.body.toString(), req.get('X-Grout-Signature') || '', process.env.GROUT_WEBHOOK_SECRET)) return res.status(400).end(); const event = JSON.parse(req.body); queue.push(event); // process later res.status(202).end(); }); ``` ```python title="Python" import hmac, hashlib, time def verify_grout(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool: parts = dict(kv.split('=', 1) for kv in header.split(',')) t = int(parts.get('t', 0)) if not t or abs(time.time() - t) > tolerance: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(kv[3:], expected) for kv in header.split(',') if kv.startswith('v1=')) # Flask @app.post('/hooks/grout') def hook(): if not verify_grout(request.get_data(), request.headers.get('X-Grout-Signature', ''), os.environ['GROUT_WEBHOOK_SECRET']): abort(400) process_later(request.get_json()) return '', 202 ``` ```php title="PHP" function verifyGrout(string $rawBody, string $header, string $secret, int $tolerance = 300): bool { parse_str(str_replace(',', '&', $header), $p); $t = (int)($p['t'] ?? 0); if (!$t || abs(time() - $t) > $tolerance) return false; $expected = hash_hmac('sha256', "$t.$rawBody", $secret); foreach (explode(',', $header) as $kv) { if (str_starts_with($kv, 'v1=') && hash_equals($expected, substr($kv, 3))) return true; } return false; } ``` ::: ## Common mistakes - **Re-serialising the body.** Sign the bytes you received, not `JSON.stringify(JSON.parse(body))`. - **Clock drift.** Keep your server on NTP; the 5-minute window is generous but finite. - **Rotations.** After `rotate-secret`, update your secret within the grace window (default 24 h). Both `v1=` values validate during that time. - **Slow handlers.** Do the work after responding; the delivery times out at 10 s and will be retried. --- # Event Catalogue Source: https://grout.app/developer/documentation/webhooks/events/ Every envelope is `{ id, type, created_at, institution_id, data }`. `GET /v1/events/types` returns this catalogue with a sample payload per type; the portal shows the same under **Developers → Deliveries**. ## People | Type | When | `data` | |---|---|---| | `student.created` | Student provisioned (API, portal or batch) | `user_id, email, user_type, student_id, source` | | `student.email_mapped` | Personal email linked | `user_id, generated_email, student_id, full_name` | | `student.updated` | Status or profile changed via API | `user_id, changes[], status?` | | `faculty.created` | Faculty provisioned | `user_id, email, user_type, faculty_id` | | `faculty.profile_completed` | Teacher finished first-login profile | `user_id, email, employee_id, department, position` | | `guardian.invited` | Guardian link created | `student_id, guardian_type, guardian_name` | | `guardian.verified` | Guardian signed in for the first time | `student_id, guardian_type` | ## Licenses | Type | When | `data` | |---|---|---| | `license.generated` | License issued | `license_id, license_type, user_id, email, expires_at, is_lifetime, max_activations` | | `license.expired` | Passed expiry | `license_id, user_id, expires_at` | | `license.device_registered` | New device seat | `user_id, app_id, device_name, platform, app_version, kicked` | ## Exams | Type | When | `data` | |---|---|---| | `exam.published` | Exam visible to students | `exam_id, title, exam_type, app, faculty_id, due_date` | | `exam.assigned` | Exam assigned to a group | `exam_id, group_id, assigned_by` | | `exam.attempt_started` | Student opened the exam | `attempt_id, exam_id, student_id, started_at, expires_at` | | `exam.submitted` | Submission (each version) | `submission_id, exam_id, student_id, is_late, submitted_at, version, app` | | `exam.graded` | Graded or resubmission requested | `submission_id, exam_id, student_id, score, max_score, percentage, resubmission_required, graded_by` | ## Credits | Type | When | `data` | |---|---|---| | `merits.credited` | Wallet credit from any source | `email, user_id, amount_cents, reason, balance_after_cents` | | `coupon.batch_requested` | Batch filed for approval | `batch_id, count, value_cents, requested_by` | | `coupon.batch_approved` | Grout approved it | `batch_id, approved_by` | | `coupon.redeemed` | Code redeemed by your user | `coupon_id, user_email, value_cents` | ## Alpha Learning | Type | When | `data` | |---|---|---| | `alpha.points_awarded` | Points ledger entry | `student_id, reason, points, ref_type, ref_id, note` | | `alpha.badge_granted` | Badge earned | `student_id, badge, title` | ## System | Type | When | |---|---| | `ping` | Test event from the portal or `POST /webhooks/endpoints/{id}/test` | ## Wildcards Subscriptions accept `*`, `.*` (e.g. `exam.*`) or exact types. Events are deduplicated server-side on `(type, ref)`, so re-running an import does not re-fire `student.created` for the same email. --- # Retries & Replay Source: https://grout.app/developer/documentation/webhooks/retries/ ## Delivery rules - Timeout **10 seconds**. Success is any `2xx`. Redirects are not followed. - Failures retry at **+1 m, +5 m, +30 m, +2 h, +12 h** — six attempts in total — then the delivery is marked `dead`. - A `410 Gone` disables the endpoint immediately. **30 consecutive failures** also disable it (`disabled_reason: too_many_failures`). - Re-enable a disabled endpoint with `PATCH { "status": "active" }`; the failure counter resets. - Deliveries and events are retained **30 days**. ## Delivery log Portal: **Developers → Deliveries** (admin) or the endpoint page on the developer dashboard. API: ```bash curl "$API/webhooks/deliveries?status=dead" -H "Authorization: Bearer $KEY" curl "$API/webhooks/deliveries?endpoint_id=we_…&event_type=exam.graded" -H "Authorization: Bearer $KEY" curl $API/webhooks/deliveries/whd_… -H "Authorization: Bearer $KEY" # payload + your response body ``` Each row shows status (`pending`, `delivering`, `succeeded`, `failed`, `dead`), attempt count, next attempt time, HTTP status, response time and the first 2 KB of your response. ## Replay ```bash curl -X POST $API/webhooks/deliveries/whd_…/replay -H "Authorization: Bearer $KEY" ``` Replay creates a **new** delivery (`replay_of` points at the original) with a fresh signature timestamp. Use it after fixing a bug on your side. You can also replay an event to a different endpoint from the portal's event browser. ## Polling fallback If you cannot receive webhooks at all: ```bash curl "$API/events?since=2026-09-14T09:00:00Z&type=exam.*&limit=200" -H "Authorization: Bearer $KEY" ``` Persist the newest `created_at` you processed and pass it as `since` next time. ## Operational tips - Respond `202` immediately and process from a queue. - Alert when `GET /webhooks/endpoints` shows `status: disabled` or `consecutive_failures > 5`. - Keep the endpoint idempotent on `id`; a replay or a retry after a timeout can deliver the same event twice. --- # LMS Integration Source: https://grout.app/developer/documentation/guides/lms-integration/ Grout integrates with a learning management system through two jobs. Each one is a small piece of code on your side, uses its own API key, and can be adopted independently. | Job | What it does | Scopes | Guide | |---|---|---|---| | **Roster sync** | Mirrors students, faculty and sections from your SIS or LMS into Grout | `students:write`, `faculty:write`, `groups:write` | [Roster Sync](/developer/documentation/guides/roster-sync/) | | **Gradebook write-back** | `exam.graded` webhooks push scores into the LMS gradebook | `submissions:read`, `webhooks:write` | [Gradebook Sync](/developer/documentation/guides/gradebook/) | There is no LMS plugin to install. Everything runs over plain HTTPS and JSON against `https://serverless.grout.app/v1`, so the same code works for Moodle, Canvas, Blackboard, a homegrown portal or an LTI tool. ## How the pieces fit ```text SIS / LMS ──(nightly)──► POST /students, /faculty, /groups ──► Grout institution Grout grades exam ──(event)──► exam.graded webhook ──► your handler ──► LMS gradebook ``` 1. **Provision people once.** Your sync creates each student with `student_id` set to the SIS id, stores the returned `id` and `login_email`, and keeps one Grout group per section. Membership is replaced on every run, so groups never drift. Students sign in to the apps themselves with the login email and a one-time code; pre-map `personal_email` so the code reaches them on the first try. 2. **Get results back.** Exams created through the API are graded in Grout. Subscribe an endpoint to `exam.graded`, verify the signature, fetch the submission for the authoritative score, and write it to the gradebook item mapped to `exam_id`. ## Platform guides :::cards - [Moodle](/developer/documentation/guides/moodle/) — Web-services roster sync and `core_grades_update_grades` for scores. - [Canvas](/developer/documentation/guides/canvas/) — Sections API and `posted_grade` on assignments. - [Roster Sync](/developer/documentation/guides/roster-sync/) — The LMS-agnostic algorithm, mappings and safety rules. - [Gradebook Sync](/developer/documentation/guides/gradebook/) — Handler sketch, exam-to-item mapping, backfill. ::: ## Keys and scopes Create one key per job in the portal (**Institution → Developers → API keys**). A leaked roster key cannot then read submissions, and a gradebook key cannot create people. ```text lms-roster students:write faculty:write groups:write 600/min lms-gradebook submissions:read webhooks:write 100/min ``` Every key is bound to your institution. No request accepts an `institution_id`, so a key can never touch another school's data. See [API Keys & Scopes](/developer/documentation/setting-up/api-keys/). ## Sizing A 5 000-student institution syncs within the default 600 requests per minute: `POST /students/bulk` takes 200 students per call, and `PATCH /groups/{id}` is one call per section. Webhook deliveries are pushed to you, so gradebook write-back costs nothing against the limit beyond one `GET /submissions/{id}` per graded attempt. ## Checklist before go-live - `GET /v1/me` with each key shows exactly the scopes above. - Roster sync stores `id` and `login_email` from the API response, never derives them. - Webhook handler verifies `X-Grout-Signature`, acks within 10 seconds, and dedupes on the event `id`. - A test exam assigned to a test group produces a grade in the LMS end to end. - Suspend, don't delete, students who leave. :::tip Automating the integration with an agent Every page here is readable as markdown by appending `.md` to its path, and [llms.txt](/developer/llms.txt), [AGENTS.md](/developer/AGENTS.md) and [SKILLS.md](/developer/SKILLS.md) give a coding agent the endpoints, scopes and request templates for each job. ::: Read [Integration Best Practices](/developer/documentation/guides/integration-best-practices/) next: idempotency, backoff, secret handling and monitoring for anything that talks to this API. --- # Integration Best Practices Source: https://grout.app/developer/documentation/guides/integration-best-practices/ These are the rules a production integration should follow. Most of them come from mistakes integrators have already made against similar APIs; following them costs an afternoon and saves a term. ## Keys - **One key per job.** Roster sync and gradebook write-back each get their own key with only the scopes that job needs. `x:write` implies `x:read`, so never add both. - **Store keys as secrets**, not in config files, repositories, LMS plugin settings pages or logs. The full key (`grt_live_…`) is shown once at creation. - **Rotate by overlap.** Create the new key, deploy it, confirm traffic with `GET /v1/me`, then revoke the old one. - **Call `GET /v1/me` on startup** and fail fast if a required scope is missing. Better a clear error at boot than a 403 in the middle of a nightly run. ## Idempotency Provisioning (`POST /students`, `/faculty`, `/groups`) is **not** idempotent: calling it twice creates two people. Keep a mapping table on your side and look before you create. ```text sis_student_id → grout_user_id, login_email sis_teacher_id → grout_user_id, login_email sis_section_id → grout_group_id ``` Writes that carry a `ref` **are** idempotent on it: merit credits, Alpha point awards and webhook replays. Generate `ref` from your own record id, never from a timestamp, so a retry reuses the same value. `PATCH /groups/{id}` with `student_ids` is a full replace. Compute the list from your source of truth on every run rather than merging. ## Rate limits and backoff Each key has its own limit, default 600 requests per minute in a fixed 60-second window. Read `X-RateLimit-Remaining` and `X-RateLimit-Reset`; on `429` or `503` wait `Retry-After` seconds and retry with a cap of five attempts. ```js async function call(url, init, attempt = 0) { const res = await fetch(url, init); if ((res.status === 429 || res.status === 503) && attempt < 5) { const wait = Number(res.headers.get('Retry-After') || 2 ** attempt); await new Promise((r) => setTimeout(r, wait * 1000)); return call(url, init, attempt + 1); } return res; } ``` Prefer bulk and paged endpoints: `POST /students/bulk` (≤ 200 per call), `limit=200` on lists. Retry only on `429`, `503` and network errors; a `4xx` other than those will fail the same way again. ## Webhooks - **Verify every delivery.** `X-Grout-Signature` is `t=,v1=` where `v1` is HMAC-SHA256 over `"."` with your endpoint secret. Reject if `t` is more than 300 seconds old. During rotation two `v1=` values are sent; accept either. See [Verify Signatures](/developer/documentation/webhooks/verify/). - **Ack first, work later.** Respond `2xx` within 10 seconds, then process on a queue. Slow handlers get retried and eventually auto-disabled. - **Dedupe on the envelope `id`.** Deliveries are at-least-once. Store processed ids for at least the retry window. - **Fetch the authoritative record.** Treat the event as a notification; call `GET /submissions/{id}` (or the relevant resource) before writing to your system. - **Backfill on a schedule.** `GET /events?since=` once a day catches anything a dead endpoint missed. Replay individual deliveries from the portal. ## Secrets and data - Never log or store the plaintext API key, webhook secret or a GroutCode hidden-test key. Log the key id and `X-Request-Id` instead. - Do not expose a student's personal email, license key or proctoring recordings to anyone whose key does not have `submissions:write`. - Store `login_email` values, which are generated addresses, not personal emails. Send students their login email from your own system. - Keep every call server-side: your service holds the key. Never call the API from browser code. ## Environments There is no sandbox yet. Use a separate test institution with its own keys, or narrow scopes on a test key so a bug cannot write to real groups. Sandbox keys (`grt_sandbox_…`) are on the roadmap; watch the [Changelog](/developer/changelog/). ## Monitoring - Surface `X-Request-Id` in every error you report to a human; support can trace a request from it. - Alert on three things: a sync run that creates more than an expected number of people, a webhook endpoint that has been auto-disabled, and a key whose `X-RateLimit-Remaining` is regularly near zero. - Check `GET /licenses/summary` after each roster run and `GET /students?status=pending` weekly for students who have never signed in. ## Versioning The API is versioned in the path (`/v1`). Additive changes (new fields, new event types, new endpoints) ship without a version bump, so parse responses leniently and ignore fields you do not know. Breaking changes get a new path version with an overlap period. The [Changelog](/developer/changelog/) is the record. ## Working with an AI agent If a coding agent builds or runs part of the integration, point it at [AGENTS.md](/developer/AGENTS.md) and [SKILLS.md](/developer/SKILLS.md) first. They carry the guardrails above as rules, including which actions need a human to confirm: suspending people, revoking keys, voiding submissions, deleting groups or endpoints, and crediting merits above a stated budget. --- # Moodle Source: https://grout.app/developer/documentation/guides/moodle/ This guide assumes a small local plugin or an external service with access to Moodle's web services. It uses two Grout keys, one per job, as recommended in [API Keys & Scopes](/developer/documentation/setting-up/api-keys/). ## 1. Nightly roster sync Scopes: `students:write`, `groups:write`, `faculty:read`. :::steps ### Pull enrolments Use `core_enrol_get_enrolled_users` per course to get users and roles. ### Provision missing students For each learner without a stored Grout id call `POST /students` with `student_id` = Moodle user id and `personal_email` = their Moodle email. Store the returned `id` and `login_email` in a custom profile field or your plugin table. ### Mirror course groups `POST /groups` once per Moodle course (name = course full name, `faculty_id` = the teacher's Grout login email), then `PATCH /groups/{id}` with the full `student_ids` list on every run. ### Suspend leavers `POST /students/{id}/suspend` for users whose enrolment ended. ::: ## 2. Gradebook write-back Scopes: `submissions:read`, `webhooks:write`. Register an endpoint subscribed to `exam.graded`. On delivery: 1. Verify the signature ([Verify Signatures](/developer/documentation/webhooks/verify/)). 2. `GET /submissions/{submission_id}` for `score`, `max_score`, `feedback`. 3. Map `exam_id` → Moodle grade item, then call `core_grades_update_grades` (or your grade item's API) for the student. 4. If `resubmission_required` is `true`, set the grade to "needs revision" instead. ## Cron sizing A 5 000-student institution syncs comfortably within the default 600 requests/minute using `POST /students/bulk` and `PATCH /groups/{id}` — roughly 30 bulk calls plus one call per course. --- # Canvas Source: https://grout.app/developer/documentation/guides/canvas/ ## Roster sync Scopes: `students:write`, `groups:write`. 1. `GET /api/v1/courses/:id/sections?include[]=students` on Canvas. 2. For each student without a Grout id, `POST /students` with `student_id` = Canvas `sis_user_id` and `personal_email` = Canvas login email; store the returned `id` and `login_email` as a custom user data entry. 3. One Grout group per Canvas section: `POST /groups` then `PATCH /groups/{id}` with the full `student_ids` each run. ## Scores to Canvas Scopes: `submissions:read`, `webhooks:write`. Subscribe to `exam.graded`. Map each Grout `exam_id` to a Canvas assignment id, then: ```http PUT /api/v1/courses/:course_id/assignments/:assignment_id/submissions/:user_id Content-Type: application/json { "submission": { "posted_grade": "87" } } ``` Use `resubmission_required` to leave a comment instead of a grade. Canvas rate limits apply; batch updates with `POST …/submissions/update_grades` for large classes. ## Guardians If parents use Canvas observer accounts, mirror them: `POST /guardians` per observer with `guardian_type: "guardian"` so they can also follow Alpha Learning reports at audit.grout.app. --- # Roster Sync Source: https://grout.app/developer/documentation/guides/roster-sync/ ## Data model Keep three mappings on your side: | Your record | Grout id | Where it comes from | |---|---|---| | Student | `users.id` + `login_email` | `POST /students` response | | Teacher | `users.id` + `login_email` | `POST /faculty` response | | Section | `groups.id` | `POST /groups` response | Never derive the login email; always store what the API returned. ## Algorithm ```text for each teacher in SIS: ensure faculty (POST /faculty if missing) for each student in SIS: ensure student (POST /students or /students/bulk if missing) PATCH /students/{id} if name/personal_email changed for each section in SIS: ensure group (POST /groups if missing) PATCH /groups/{id} { student_ids: [...] } # replaces membership for each student no longer enrolled anywhere: POST /students/{id}/suspend ``` ## Idempotency and safety - Provisioning is **not** idempotent; check your mapping before creating. - `PATCH /groups/{id}` with `student_ids` is a full replace — compute the list from the SIS, do not merge. - Suspend, don't delete. Suspended students keep their license and history. - Run under a key with only `students:write`, `faculty:write`, `groups:write`. ## Verifying a run ```bash curl "$API/students?status=pending&limit=200" -H "Authorization: Bearer $KEY" # never signed in curl $API/licenses/summary -H "Authorization: Bearer $KEY" curl "$API/events?type=student.*&since=" -H "Authorization: Bearer $KEY" ``` ## First-run tips - Pre-map `personal_email` so students skip the mapping screen on first login. - Send students their `login_email` from your own system; the API does not email students on creation. - Faculty first logins route the OTP to the institution admin's mailbox until the teacher completes their profile. --- # Gradebook Sync Source: https://grout.app/developer/documentation/guides/gradebook/ ## Flow 1. Register a webhook endpoint subscribed to `exam.graded` (and optionally `exam.submitted` for "turned in" states). 2. On `exam.graded`, fetch the submission for the authoritative numbers. 3. Write the score to the gradebook item mapped to `exam_id`. ## Handler sketch ```js app.post('/hooks/grout', express.raw({ type: 'application/json' }), async (req, res) => { if (!verifyGrout(req.body.toString(), req.get('X-Grout-Signature'), SECRET)) return res.status(400).end(); const evt = JSON.parse(req.body); res.status(202).end(); // ack first if (await seen(evt.id)) return; // dedupe on event id if (evt.type !== 'exam.graded') return; const { data } = await grout(`/submissions/${evt.data.submission_id}`); const item = await gradeItemFor(evt.data.exam_id); if (evt.data.resubmission_required) await markNeedsRevision(item, data.student_id, data.feedback); else await postGrade(item, data.student_id, data.score, data.max_score, data.feedback); }); ``` ## Mapping exams to items Create the gradebook item when you see `exam.published` (title, `max_score`, `due_date`), store `exam_id → item_id`, and assign the exam to the section's group. That keeps authoring in Grout and reporting in the LMS. ## Backfill Missed events? Pull them: ```bash curl "$API/events?type=exam.graded&since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY" curl "$API/submissions?exam_id=ex_…&status=graded" -H "Authorization: Bearer $KEY" ``` Or export a whole exam with `GET /exams/{id}/export.csv`. --- # FAQ Source: https://grout.app/developer/documentation/guides/faq/ ## Access **Is there a sandbox?** Not yet. Use a test institution or narrow scopes. Sandbox keys (`grt_sandbox_…`) are planned. **Can a developer see student data in the portal?** No. The developer dashboard shows keys, webhooks and deliveries only. Data access goes through the API with whatever scopes the key holds. **How do I get more than 600 requests per minute?** Edit the key's rate limit (up to 6 000) in the portal. Bulk endpoints reduce the need. ## People **Why is a student `pending`?** They have not signed in yet. It flips to `active` on first successful login. **Can I set a student's password?** There are no passwords. Students receive a one-time code at their personal email. Pre-map it with `personal_email` on `POST /students`. **Can I change a login email?** No; it is generated and permanent. Suspend the account and provision a new one if needed. ## Exams **Do I have to use groups?** Yes. Assignment is by group; a one-student group is fine. **Where are the proctoring videos?** In the submission's `proctoring.recordings` as signed URLs, only for keys with `submissions:write`. **Can students see hidden tests?** The encrypted zip is downloadable but useless without the key, which is released once per attempt and stamped on the attempt record. ## Webhooks **How many times can I receive the same event?** At least once. Dedupe on `id`. **My endpoint was disabled.** 30 consecutive failures or a `410`. Fix the receiver, then re-enable with `PATCH { "status": "active" }` and replay the dead deliveries. **Do you support IP allow-lists?** Deliveries come from Cloudflare's network; use the signature, not the source IP, to authenticate. ## Support Email [hello@grout.app](mailto:hello@grout.app) with the `X-Request-Id` of a failing call. Institution admins can also use the in-portal help widget. --- # API: Meta - GET /me — Describe the calling API key - GET /scopes — List available scopes --- # API: Institution - GET /institution — Institution profile, user and license counts (scope institution:read) - GET /institution/config — Read integration config (scope institution:read) - PUT /institution/config — Replace integration config (default AI policy, proctoring, toolchains, LMS) (scope institution:write) --- # API: Students - GET /students — List students (scope students:read) - POST /students — Provision a student (login email + license) (scope students:write) - POST /students/bulk — Provision up to 200 students (scope students:write) - GET /students/{id} — Get a student (id or login email) (scope students:read) - PATCH /students/{id} — Update a student (scope students:write) - POST /students/{id}/suspend — Suspend a student (scope students:write) - POST /students/{id}/reactivate — Reactivate a student (scope students:write) - GET /students/{id}/guardians — Guardians linked to a student (scope guardians:read) - GET /students/{id}/exams — Exams assigned to a student with submission state (scope exams:read) --- # API: Faculty - GET /faculty — List faculty (scope faculty:read) - POST /faculty — Provision a faculty member (scope faculty:write) - GET /faculty/{id} — Get a faculty member (scope faculty:read) - PATCH /faculty/{id} — Update a faculty member (scope faculty:write) - GET /faculty/{id}/exams — Exams owned by a faculty member (scope exams:read) --- # API: Groups - GET /groups — List groups (scope groups:read) - POST /groups — Create a group (scope groups:write) - GET /groups/{id} — Get a group with members and exams (scope groups:read) - PATCH /groups/{id} — Update a group (student_ids replaces membership) (scope groups:write) - DELETE /groups/{id} — Delete a group (scope groups:write) - POST /groups/{id}/members — Add students to a group (scope groups:write) - DELETE /groups/{id}/members/{studentId} — Remove a student from a group (scope groups:write) --- # API: Licenses - GET /licenses — List licenses (scope licenses:read) - GET /licenses/summary — License counts by type/status (scope licenses:read) - GET /licenses/{id} — Get a license (scope licenses:read) - POST /licenses/{id}/revoke — Revoke a license (scope licenses:write) - POST /licenses/{id}/extend — Extend a license (scope licenses:write) - GET /licenses/{id}/devices — Device seats and activations (scope licenses:read) - POST /licenses/{id}/devices/{seatId}/revoke — Revoke a device seat (scope licenses:write) --- # API: Exams - GET /exams — List exams (scope exams:read) - POST /exams — Create an exam (optionally publish + assign) (scope exams:write) - GET /exams/{id} — Get an exam with groups and stats (scope exams:read) - PATCH /exams/{id} — Update an exam (scope exams:write) - DELETE /exams/{id} — Delete an exam (scope exams:write) - POST /exams/{id}/publish — Publish an exam (scope exams:write) - POST /exams/{id}/close — Close an exam (scope exams:write) - GET /exams/{id}/assignments — Groups the exam is assigned to (scope exams:read) - POST /exams/{id}/assignments — Assign exam to groups (notifies students + guardians) (scope exams:write) - DELETE /exams/{id}/assignments/{groupId} — Unassign a group (scope exams:write) - GET /exams/{id}/attempts — Attempts (started/expiry/hidden-test release) (scope submissions:read) - GET /exams/{id}/analytics — Score analytics (scope submissions:read) - GET /exams/{id}/scores — All scores for an exam (scope submissions:read) - GET /exams/{id}/export.csv — CSV export of submissions (scope submissions:read) --- # API: Exams (GroutCode) - POST /exams/{id}/files — Upload an exam file (starter.zip, sample-tests.zip, hidden-tests.zip.enc) (scope exams:write) - PUT /exams/{id}/code-secret — Store the hidden-tests AES key (write-only custody) (scope exams:write) - GET /exams/{id}/code-secret/status — Whether a key is stored and how many students received it (scope exams:read) - POST /exams/{id}/code-secret/release — Read back the hidden-tests key (institution role) (scope exams:write) --- # API: Submissions - GET /submissions — List submissions (scope submissions:read) - GET /submissions/{id} — Get a submission (signed attachment URLs, AI usage, proctoring summary) (scope submissions:read) - GET /submissions/{id}/events — Proctoring event feed for the attempt (scope submissions:read) - POST /submissions/{id}/grade — Grade (score −1 requests resubmission) (scope submissions:write) - POST /submissions/{id}/void — Void a submission (scope submissions:write) --- # API: Coupons - GET /coupons/batches — List coupon batches requested by the institution (scope coupons:read) - POST /coupons/batches — Request a coupon batch (Grout approves) (scope coupons:write) - GET /coupons/batches/{id} — Batch detail (codes once approved) (scope coupons:read) - GET /coupons/redemptions — Redemptions by institution users (scope coupons:read) --- # API: Merits - GET /merits/allowance — Remaining institution merit allowance (scope merits:read) - GET /merits/wallets/{email} — Wallet balance and ledger (scope merits:read) - POST /merits/credits — Credit merits (idempotent by ref, draws from allowance) (scope merits:write) - GET /merits/ledger — Institution-wide ledger (scope merits:read) --- # API: Guardians - GET /guardians — List guardian links (scope guardians:read) - POST /guardians — Invite a guardian for a student (scope guardians:write) - POST /guardians/{id}/resend — Resend the invitation (scope guardians:write) - DELETE /guardians/{id} — Remove a guardian link (scope guardians:write) - GET /guardians/reports — Weekly guardian reports (Alpha) (scope guardians:read) --- # API: Alpha Learning - GET /alpha/curricula — Published curricula (institution + shared) (scope alpha:read) - GET /alpha/enrollments — List enrollments (scope alpha:read) - POST /alpha/enrollments — Enroll students in a curriculum (scope alpha:write) - PATCH /alpha/enrollments/{id} — Update an enrollment (scope alpha:write) - GET /alpha/students/{id} — Student drill-down (scope alpha:read) - GET /alpha/students/{id}/mastery — Concept mastery (scope alpha:read) - GET /alpha/students/{id}/points — Points, streaks, badges and ledger (scope alpha:read) - POST /alpha/students/{id}/points — Award bonus points (±100, idempotent by ref) (scope alpha:write) - GET /alpha/leaderboard — Institution leaderboard (scope alpha:read) --- # API: Webhooks - GET /webhooks/endpoints — List webhook endpoints (scope webhooks:read) - POST /webhooks/endpoints — Create an endpoint (secret returned once) (scope webhooks:write) - GET /webhooks/endpoints/{id} — Get an endpoint (scope webhooks:read) - PATCH /webhooks/endpoints/{id} — Update / pause / resume an endpoint (scope webhooks:write) - DELETE /webhooks/endpoints/{id} — Delete an endpoint (scope webhooks:write) - POST /webhooks/endpoints/{id}/rotate-secret — Rotate the signing secret (24h dual-secret grace) (scope webhooks:write) - POST /webhooks/endpoints/{id}/test — Send a synchronous ping (scope webhooks:write) - GET /webhooks/deliveries — Delivery log (scope webhooks:read) - GET /webhooks/deliveries/{id} — Delivery detail with payload and response (scope webhooks:read) - POST /webhooks/deliveries/{id}/replay — Replay a delivery (scope webhooks:write) --- # API: Events - GET /events — Polling fallback over the event log (scope webhooks:read) - GET /events/types — Event catalogue with sample payloads