Guides
Integration Best Practices
Keys, idempotency, backoff, webhooks, secrets and monitoring: how to build an integration against the Grout Institution API that keeps working.
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, SSO launch and gradebook write-back each get their own key with only the scopes that job needs.
x:writeimpliesx: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/meon 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.
sis_student_id → grout_user_id, login_email
sis_teacher_id → grout_user_id, login_email
sis_section_id → grout_group_idWrites 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.
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-Signatureist=<unix>,v1=<hex>wherev1is HMAC-SHA256 over"<t>.<raw body>"with your endpoint secret. Reject iftis more than 300 seconds old. During rotation twov1=values are sent; accept either. See Verify Signatures. - Ack first, work later. Respond
2xxwithin 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-Idinstead. - Do not expose a student's personal email, license key or proctoring recordings to anyone whose key does not have
submissions:write. - Store
login_emailvalues, which are generated addresses, not personal emails. Send students their login email from your own system. - Keep the desktop launch flow server-side: the LMS button calls your service, which holds the key. Never mint tokens 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.
Monitoring#
- Surface
X-Request-Idin 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-Remainingis regularly near zero. - Check
GET /licenses/summaryafter each roster run andGET /students?status=pendingweekly 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 is the record.
Working with an AI agent#
If a coding agent builds or runs part of the integration, point it at AGENTS.md and 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.