The LMS API: Scoped Keys, Least Privilege & Integrations You Can Audit

There are two ways a learning platform can stop being an island. The first is to speak when something happens — a learner enrolls, a grade posts, a certificate is issued — and push that event to whoever is listening. That's webhooks, and we wrote about them in Your LMS Shouldn't Be an Island. The second is to answer when it's asked: who is on this roster right now? what did this cohort score? enroll these forty people I just onboarded. That's the API, and it's the half this post is about.
Push and pull
The distinction matters more than it sounds, because the two solve genuinely different problems. A webhook is the only sane way to react in real time to something you didn't schedule. An API call is the only way to ask a question nobody anticipated, backfill something you missed, or make a change from outside the dashboard.
| Webhooks | The REST API | |
|---|---|---|
| Direction | EduGears calls your endpoint | Your system calls EduGears |
| Trigger | Something happened in the LMS | Your code decided to ask or act |
| Good at | Reacting the moment an event fires; keeping a downstream system continuously in step | Backfills, reconciliation, on-demand lookups, and writes — enrolling, inviting, issuing |
| Weak at | Answering "what is true right now?" and doing anything on demand | Knowing something happened without asking (that's polling, and it's wasteful) |
| Auth | A per-subscription HMAC-SHA256 signing secret you verify on receipt | A scoped eg_live_ bearer key you send on every request |
Most real integrations use both. The webhook wakes your job up; the API call fetches the detail the job needs and writes the result back. Neither one replaces the other, and if you find yourself polling a list endpoint every sixty seconds waiting for something to change, that's the signal you wanted a webhook.
One host, one header
There is no separate API host to remember. The API lives under your own academy's address — the same one your staff sign in at. If your academy's address is https://youracademy.example, every endpoint is relative to that host.
Authentication is one header. Send your key as a bearer token on every request:
curl -H "Authorization: Bearer eg_live_…" https://youracademy.example/api/courses
That's the whole handshake. No OAuth dance, no token exchange, no session to keep alive. Requests and responses are JSON, apart from the downloads that are deliberately not JSON — the gradebook CSV, the transcript PDF, the QTI export.
Two response codes carry the meaning you'll care about while wiring things up. A key that is unknown, malformed, or revoked gets a 401 with one deliberately vague message — the platform won't tell a stranger whether a key exists, so nobody can probe for valid keys. A perfectly good key that lacks the scope for the endpoint it called gets a 403 naming the scope it needed. In practice: 401 means fix the key, 403 means fix the scopes.
Your integrations call the same endpoints the dashboard calls. There is no second-class API tier that lags behind the product — the difference is the credential, not the surface.
Nine scopes, and why read and write are separate
A key is not a password to your academy. It is a named grant, and at the moment you create it you tick exactly which capabilities it carries. Nine scopes are defined today, deliberately split so that reading and writing are never the same permission:
| Scope | What a key holding it can do |
|---|---|
courses:read | List courses, read course detail including sections and activities, export a quiz or question-bank questions as QTI |
enrollments:read | List classrooms, read a classroom and its roster, search the people still eligible to be added |
enrollments:write | Enroll someone into a classroom, and remove an enrollment again |
grades:read | Read a course gradebook, a learner's report card, the gradebook CSV export, and a learner's transcript as JSON or PDF |
users:read | List the people in your organization and read user counts per role |
users:write | Invite one person or a bulk list, change a role, disable or re-enable an account, trigger a password reset |
certificates:read | List certificates, read one, download its verifiable credential, and check whether an enrollment is eligible |
certificates:write | Issue the certificate for an enrollment |
webhooks:read | Read your webhook endpoints and their delivery log |
* | Every scope above at once. Convenient for a trusted internal tool — but if a narrower set would do the job, mint the narrower key |
The read/write split is the part worth dwelling on. A nightly job that pulls grades into a warehouse holds grades:read and nothing else. If a bug in that job — or someone who found the key in a log file — tries to enroll a student or invite a user, the call is refused with a 403. Least privilege stops being a policy someone has to remember and becomes a property of the credential.
The useful test when you mint a key: if this exact string were pasted into a public repository tomorrow, what could someone do with it? If the honest answer is "read one course's gradebook," the scoping is right. If it's "anything," mint a narrower one.
The scope list is served live from the backend to the key-minting screen, so when a new scope is added it appears in the form without waiting for a release.
What the API actually exposes
Rather than a wall of endpoints, here is the shape of the surface — the things teams typically want, and what each one costs in scopes:
| If you want to… | Call | Scope |
|---|---|---|
| Sync your course catalog | GET /api/courses | courses:read |
| Read a classroom and its roster | GET /api/classrooms/{id} | enrollments:read |
| Find who can still be added to a classroom | GET /api/classrooms/{id}/eligible-members | enrollments:read |
| Enroll (or unenroll) a learner | POST / DELETE /api/classrooms/{id}/enroll… | enrollments:write |
| Pull a course gradebook | GET /api/grades/course/{id} | grades:read |
| Pull the same gradebook as CSV | GET /api/grades/export/{id} | grades:read |
| Fetch a learner's full transcript | GET /api/users/{id}/transcript (or .pdf) | grades:read |
| Invite a person, or a whole list | POST /api/users/invite and the bulk preview/commit pair | users:write |
| Check eligibility, then issue a certificate | POST /api/certificates/check/{enrollment_id}, then /issue/{enrollment_id} | certificates:read, then certificates:write |
| Download a credential for an external registry | GET /api/certificates/{id}/wallet.json | certificates:read |
| Audit webhook deliveries from your monitoring | GET /api/integrations/webhooks/{id}/deliveries | webhooks:read |
Note the bulk-invite pair: preview is a genuine dry run. You post the list, get back who would be created and who already exists, and nothing is sent until you call commit. Onboarding scripts get to be careful without you writing the safety net yourself.
Four integrations teams actually build
- Provision people from HR. A nightly job reads new joiners out of the HR system, dry-runs the list against
invite/bulk/preview, commits the ones that are genuinely new, and enrolls each of them into the classroom their role requires. Two scopes:users:writeandenrollments:write. Leavers flow the other way —DELETE /api/users/{id}disables the account rather than erasing the record, so their grades and certificates survive. - Pull grades into BI. A warehouse job walks the courses it cares about and pulls each gradebook, or takes the CSV export straight into a staging table. One scope, read-only:
grades:read. This is the key that most deserves to be narrow, because it is the one that ends up pasted into a scheduler somewhere. - Issue and file certificates. A compliance workflow checks eligibility, issues the certificate, then downloads the verifiable credential and files it in an external registry. Pair it with the
certificate.issuedwebhook and the registry never drifts from the source of truth — the webhook says something was issued, the API call fetches exactly what. - Two-way roster sync with an SIS. Read the catalog with
courses:read, look up who is eligible withenrollments:read, write the enrollment withenrollments:write, and read the results back withgrades:read. Those four scopes are precisely the set a student information system or HR platform needs to close the loop, which is why they exist as separate grants rather than one "integration" toggle.
Have a stack the LMS needs to talk to — an SIS, an HR platform, a warehouse? Tell us what you run and we'll map the scopes and endpoints with you.
See the integrationsMinting, rotating, revoking
A credential's life is where most integration incidents are actually won or lost, so it's worth being explicit about all four stages.
- Mint. Keys are created under Settings → API keys, and only by the organization owner — admins and instructors see a notice instead of the list, and the backend enforces the same rule rather than trusting the screen. Give the key a name that says where it will run (
Warehouse nightly pull, notkey 2); that name is the only clue you get about it later. - Store. The full secret is shown exactly once, at creation. Only a SHA-256 hash and the first twelve characters are kept, so there is no "show key" button anywhere and no support ticket that can recover it. Copy it into your secret store on the spot.
- Watch. The list shows each key's prefix, its scopes, and when it was last used. The prefix lets you match a line in your logs to a row here without exposing the secret; Last used is the fastest way to find keys nobody needs any more.
- Rotate. Rotation is a deliberate act, and the order matters: mint the replacement, deploy it, confirm the new key's Last used is ticking, then revoke the old one. Two keys can be live at once, so a rotation needs no downtime window.
- Revoke. Revocation takes effect on the next call — there is no cache to wait out. And revoking is not deleting: the row stays, stamped with when it was killed and by whom, because after an incident the question is always "which key was live, and when did we stop it?"
One behavior deserves calling out because it surprises people. A key acts as the account that created it, and it cannot outlive that account: disable the person who minted a key and every key they minted stops working immediately. That's the right default — offboarding shouldn't leave live credentials behind — but it means production integrations should be minted from an account that belongs to the organization, not from whichever owner happened to be at the keyboard that afternoon.
Honest limits
Four things the API doesn't do today, so nobody discovers them at integration time:
- No expiry dates or automatic rotation. Keys live until someone revokes them. Put the rotation in your own calendar — and use Last used as the quarterly cleanup list.
- No separate test key. Every key is an
eg_live_key against your real data. While you're developing, mint a read-only key with the narrowest scope that lets you make progress, and revoke it when you're done. - No client libraries. There is no SDK to install and none is needed — it's HTTPS and JSON, so whatever HTTP client your stack already has is the integration layer. The curl one-liner above is a complete working example.
- No webhook-write scope.
webhooks:readis the only webhook scope — it lets an integration audit your endpoints and their delivery log. Creating and editing webhook subscriptions is a dashboard task for the organization owner.
And the standing advice that outlives any feature list: treat a key like a password, because that's what it is. Grant the minimum scopes, keep it out of source control and log lines, give each integration its own key so you can revoke one without breaking the others, and check the Last used column often enough that a key nobody remembers never sits there quietly working.
Want to see the key-minting screen, the scope list, and a live call against a real academy? We'll walk it with you.
Talk to usFAQ
What's the difference between the API and webhooks?
Direction. Webhooks push: EduGears calls your endpoint the moment an event happens, with a signed payload, retries, and a replayable delivery log. The API pulls and acts: your system calls EduGears to ask a question or make a change, using a scoped bearer key. Most production integrations use both — the webhook wakes the job up, the API call does the work.
Who can create an API key, and where?
Only the organization owner, under Settings → API keys. Admins and instructors get a notice instead of the list, and the backend enforces the same restriction independently — minting a long-lived bearer secret is treated as a billing-grade action rather than an everyday admin one.
What happens if a key leaks?
Revoke it, then mint a replacement. Revocation takes effect on the very next call, and the revoked row is kept — with when it was revoked and by whom — so you have an audit trail afterwards. The key prefix shown in the list lets you match the leaked value to the right row, and Last used tells you whether anyone was still calling with it.
Can I recover a key I lost?
No. Only a SHA-256 hash of the key is stored, plus the first twelve characters for display, so the plaintext genuinely doesn't exist anywhere after the creation screen closes. Revoke the lost key and mint a new one — it's a two-minute job and leaves a cleaner audit trail than sharing a recovered secret would.
Is there a sandbox or test key?
Not today — every key is a live key against your real academy data. The practical substitute is scope discipline: while you build, mint a read-only key carrying only the scope you're exercising, and revoke it once the integration is working with its real, minimal set of grants.
See your academy on EduGears AI LMS
Book a demo and we'll show you a live tenant branded as your academy — your name, your colors, your domain.
Book a demo →