API reference
The HTTP API behind the CouchCertified platform. This page covers authentication, conventions, the team-management endpoints used by the team pages, and the public endpoints.
Authentication
Sign-in is password-less. To authenticate a script:
- Request a magic link:
POST /api/auth/magic-linkwith{"email": "you@district.org"}. The link arrives by email. - The link's
codeis single-use. Exchanging it (GET /api/auth/callback?code=…) yields a short-lived access token (JWT) and a refresh-cookie session. - Send the token on every call:
Authorization: Bearer <token>.
Browsers refresh the access token automatically from the session cookie; scripts should simply re-authenticate when they receive 401.
Conventions
- Requests and responses are JSON (
Content-Type: application/json), except the CSV export. - Errors share one shape:
{"error": {"code": "...", "message": "..."}}with a matching HTTP status (400bad input,401not signed in,403insufficient role,404not found,409conflict,429rate-limited with aRetry-Afterheader). - Public, unauthenticated endpoints are rate-limited (typically 60/min per IP and 5/hr per email address).
- Timestamps are Unix epoch seconds unless suffixed
_iso.
Team management
All team endpoints require authentication and a role on that team. The minimum role is listed per endpoint — Viewer+ means Viewer, Manager, or Owner; Manager+ means Manager or Owner. Calls against a team you don't belong to return 404.
| Endpoint | Role | What it does |
|---|---|---|
GET /api/team/mine | any | Your teams + your role in each. Returns {teams:[{group_id, uuid, name, role}]}. |
GET /api/team/:groupId | Viewer+ | Overview: team name, your role, and per-course seat utilization {purchased, assigned, reserved, remaining, completed}. |
PATCH /api/team/:groupId{name} | Owner | Rename the team — the new name shows everywhere (invites, activity log). Owner only; audited. |
GET /api/team/:groupId/members?page= &per_page= &sort=name|role|courses|progress &dir= &roles= &q= | Viewer+ | The roster: members with per-course content-progress %, plus pending invites. roles= is a comma-separated filter over owner,manager,viewer,member (e.g. roles=member for learners, roles=owner,manager,viewer for leadership); q= searches name/email (3-char min). |
GET /api/team/:groupId/members/:userId | Viewer+ | One member's detail: profile, role, every team enrollment with progress/status, and (Manager+) the active certificate's verification code. |
POST /api/team/:groupId/assign{course_slug, emails?[], member_ids?[], self?, csv?} | Manager+ | Assign a course to up to 40 recipients: existing members are enrolled, new emails get seat-reserving invites. Idempotent; refuses over-allocation per recipient. Returns {assigned[], invited[], skipped[{email, reason}]}. |
POST /api/team/:groupId/assign/previewsame body as assign | Manager+ | Dry run of assign — classifies every recipient (enroll / invite / skip + reason, with seat exhaustion simulated in order) and writes nothing. |
POST /api/team/:groupId/members/invite{email, role} | Manager+ | Role-only invite (Viewer/Manager/Owner) — joins the leadership on next login, no seat used. Only an Owner may grant owner. |
POST /api/team/:groupId/members/:userId/role{role} | Manager+ | Promote/demote. Managers can't touch Owners or other Managers; a team always keeps ≥ 1 Owner (409 otherwise). |
DELETE /api/team/:groupId/members/:userId | Manager+ | Remove a member: withdraws their unfinished team enrollments (frees seats), keeps earned certificates. Re-adding later restores progress. |
DELETE /api/team/:groupId/members/:userId/courses/:courseSlug | Manager+ | Unenroll one course (frees its seat), keeping the membership and other courses. Completed courses are spent → 404. |
DELETE /api/team/:groupId/invites/:inviteId | Manager+ | Revoke a pending invite; its reserved seat returns to the pool. |
POST /api/team/:groupId/invites/:inviteId/resend | Manager+ | Re-arm a pending or lapsed invite for a fresh 30 days and re-email the link. Reviving a lapsed invite re-reserves a seat (409 if none left). |
GET /api/team/:groupId/audit?limit= &before= | Manager+ | The activity log, newest first with keyset pagination ({entries[], next_before}). |
GET /api/team/:groupId/export | Viewer+ | Compliance CSV: one row per teacher per course — status, the enrolled-for state and what its certificate is worth (hours, plus PDPs where that state awards them; completed only), certificate code + issue/expiry. |
Data export
Export the underlying learning-event trail (every meaningful action — enroll, complete, quiz pass/fail, certificate issue — as an append-only, xAPI-shaped record) joined with its enrollment, course, and certificate context. Add ?format=csv (default, spreadsheet-friendly) or ?format=xapi (an {statements:[…]} array for an LRS). A very large export is capped and flagged with an X-Export-Truncated response header.
| Endpoint | Who | What it does |
|---|---|---|
GET /api/me/export?format=csv|xapi | any learner | Your own complete learning history — the data-portability / GDPR export. Only ever your events; never anyone else's. |
Certificate verification
Every certificate carries a verification code (it's in the compliance CSV and on the certificate itself). Anyone — an HR office, a license reviewer — can confirm a certificate at https://learn.couchcertified.com/verify/<code> with no account.
Example: pull your roster
# 1. find your team id
curl -s https://learn.couchcertified.com/api/team/mine \
-H "Authorization: Bearer $TOKEN"
# → {"teams":[{"group_id":12,"uuid":"…","name":"Demo District","role":"owner"}]}
# 2. roster, sorted by progress
curl -s "https://learn.couchcertified.com/api/team/12/members?sort=progress&dir=desc" \
-H "Authorization: Bearer $TOKEN"
# 3. compliance CSV
curl -s https://learn.couchcertified.com/api/team/12/export \
-H "Authorization: Bearer $TOKEN" -o team-progress.csv