Skip to content

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: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=<unix>,v1=<hex> where v1 is HMAC-SHA256 over "<t>.<raw body>" 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.
  • 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 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-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 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.