# Integration Best Practices

> Keys, idempotency, backoff, webhooks, secrets and monitoring: how to build an integration against the Grout Institution API that keeps working.

Canonical: 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=<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](/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.
