# Peerfold — full agent knowledge
> Everything an AI coding agent needs to build on Peerfold — the API, the auth flows, the SDKs, the recipes, and the rules that keep you out of trouble.
Everything an agent needs to build against Peerfold, in one file: what the platform is, how to connect, which authentication flow to choose, the resource map, the recipes, the content model, the guardrails, and a generated index of every API operation.
## What Peerfold is
Peerfold is a hosted learning platform. A workspace (a *tenant*) holds courses, learners, enrollments, progress, quizzes and exams, certificates, memberships and orders. It ships a learner portal of its own at `your-workspace.peerfold.app`, and it exposes the same data over a public HTTP API so you can build any surface you want on top of it: a catalog page on a marketing site, a fully custom course player, an internal dashboard, an onboarding flow inside your product.
You are almost certainly here to do one of three things. **Configure** — change branding, courses or settings, which an agent can do over MCP without writing code. **Embed** — drop Peerfold widgets into a site you already own. **Go headless** — own the frontend end to end and call the API for everything. This kit covers all three, and is most useful for the third.
Two facts shape most design decisions. First, **published courses are immutable revisions** — a learner always reads the revision that was published, so editing a draft never changes what someone is part-way through. Second, **the API is generated from one OpenAPI 3.1 contract**, which also generates the route validation, the client types and the MCP tool schemas — so if the docs and the API disagree, that is a bug, not a nuance.
## Connecting your agent
If your tool speaks **MCP**, connect it and use its tenant-scoped tools to author courses, enroll learners, read reports, and discover and try endpoints, with no curl commands to write. **An AI assistant needs no key.** Claude and ChatGPT both take the server URL `https://app.peerfold.com/api/mcp` as a custom connector and nothing else: a browser opens, you approve the workspace, and that is the whole setup. Nothing is typed into a file, so nothing can leak out of one.
```bash
claude mcp add peerfold https://app.peerfold.com/api/mcp
```
Cursor and other MCP clients take the same endpoint — `https://app.peerfold.com/api/mcp`, transport **Streamable HTTP** — in their own MCP config, and OAuth the same way. Each connection is revocable on its own under **Settings → Developers → Connected AI assistants**.
A **secret key** is for a programmatic client with no browser to redirect through — a script, a backend service, a CI job. Present it as a bearer header:
```bash
claude mcp add --transport http peerfold https://app.peerfold.com/api/mcp \
--header "Authorization: Bearer sk_live_…"
```
Tools that do not support MCP use the HTTP API directly; everything below works the same way for them.
### Without MCP: a key and a base URL
Create a key pair in the workspace under **Settings → Developers**. You get a publishable key (`pk_live_…`, browser-safe) and a secret key (`sk_live_…`, shown exactly once, server-side only). Then:
| Plane | Base URL | Credential |
| --- | --- | --- |
| Learner (browser) | `https://your-workspace.peerfold.app` | Short-lived learner JWT (15 min) |
| Admin / server | `https://app.peerfold.com` | Secret key `sk_live_…` |
```bash
# Admin plane — server-side only.
curl https://app.peerfold.com/api/v1/lms/learners \
-H "Authorization: Bearer $PEERFOLD_SECRET_KEY" \
-H "Peerfold-Version: 2026-07-01"
```
Pin the API version with the `Peerfold-Version` header. Responses carry `X-Request-Id` and rate-limit headers; errors are RFC 7807 `application/problem+json` with a stable machine-readable `code`.
### The SDKs
`@peerfold/api-client` is a typed, zero-runtime-dependency wrapper over `fetch` (browser, Node and edge). `@peerfold/react` adds a provider and hooks. `@peerfold/js` is a prebuilt browser bundle exposing `window.Peerfold` for script-tag use, plus the embeddable cart drawer. Types in the client are inferred from the same schemas that validate the live API, so a contract change breaks your build rather than your users.
```ts
import { PeerfoldClient } from "@peerfold/api-client";
// Browser / learner plane.
const client = new PeerfoldClient({
baseUrl: "https://your-workspace.peerfold.app",
learnerToken: () => tokenStore.get(), // string or a provider fn
});
// Server / admin plane — NEVER construct this in browser code.
const admin = new PeerfoldClient({
baseUrl: "https://app.peerfold.com",
secretKey: process.env.PEERFOLD_SECRET_KEY,
});
```
## Choosing an auth model
This is the decision people get wrong, and it is expensive to undo. Learner-plane calls need a **learner JWT** (15 minutes, tenant-scoped, one learner). The flows differ only in how that JWT is obtained.
| Use this | When | Secret lives |
| --- | --- | --- |
| **Admin secret key** | Server-to-server only: reporting, bulk enrollment, provisioning, back-office jobs. No end user involved. | Your server |
| **Learner PKCE** | You own a frontend and want Peerfold to handle sign-in. Default choice for a headless portal or SPA. | Nowhere — no secret in the browser |
| **Trusted relay (token exchange)** | Your app already authenticated the person and you want to hand them straight in, no second login. | Your server / serverless function |
| **Membership assertion** | A HubSpot CMS page whose visitor is already a logged-in member. Zero-login hand-off, rendered server-side. | The CMS theme's settings (digest only) |
### Admin secret key
`Authorization: Bearer sk_live_…` against `https://app.peerfold.com`. It is the whole workspace, so it never touches a browser, a client bundle, or a committed file. It can also mint a learner token directly (`POST /api/v1/auth/learner-token` with `email` or `member_id`) — that is the trusted-subsystem shortcut, and it means anyone holding the key can act as any learner.
### Learner PKCE (the default for your own frontend)
1. Register your redirect URI under **Settings → Developers** (it is an allowlist; unregistered URIs are rejected).
2. Send the browser to `GET https://your-workspace.peerfold.app/api/v1/auth/authorize` with `client_id` (your publishable key), `redirect_uri`, `code_challenge` (S256) and `state`. Peerfold renders its own login page — email + login code.
3. On success it redirects back with `code`; exchange it at `POST https://your-workspace.peerfold.app/api/v1/auth/token` with your `code_verifier`.
4. You get an access token (15 min) and a refresh token. Refresh at the same endpoint with `grant_type=refresh_token`; refresh tokens rotate on use.
No secret key is involved anywhere in this flow, which is exactly why it is the default: a leaked artifact leaks nothing.
### Trusted relay
If your own app already knows who the user is, put a small server endpoint in front of `POST /api/v1/auth/token-exchange` (secret-key authed). It asserts the signed-in user server-side and returns a learner token to the browser; the browser never sees the key. `@peerfold/js` has this wired — `createPeerfold({ relayUrl })` re-mints through your relay whenever the token expires.
```js
const { client, refresh } = Peerfold.createPeerfold({
baseUrl: "https://your-workspace.peerfold.app",
relayUrl: "/api/peerfold-token", // your endpoint; it holds the secret key
});
await refresh();
const catalog = await client.catalog.list();
```
### Membership assertion (HubSpot CMS)
A HubSpot CMS page cannot call an API at render time, so the theme instead renders a signed assertion for the logged-in member and the browser trades it for a learner token at `POST /api/v1/auth/membership-assertion`. The payload is `portalId|contactVid|email|tsSeconds` and the signature is `md5(secret + "|" + payload)` as lowercase hex — md5 because HubL exposes exactly one hash filter. The signing secret lives in theme settings and only its digest is ever rendered; assertions are emitted only inside the logged-in branch, and they are short-lived.
There is also a lead-gen pair for anonymous visitors — `POST /api/v1/auth/leadgen-identify` resolves a HubSpot tracking cookie to a known contact, and `POST /api/v1/auth/leadgen-send` emails them a login code. Use it to turn "we already know this person" into a one-click sign-in, never as an authentication decision on its own.
## The resource map
| Area | Plane | What you get |
| --- | --- | --- |
| Identity | Learner | `GET/PATCH /api/v1/me` — the signed-in learner's profile. |
| Catalog & courses | Learner | `GET /api/v1/lms/catalog` (cursor-paginated), `GET /api/v1/lms/courses/{slug}` for the published revision with its chapters, lessons and block content. |
| Enrollment & progress | Learner | `GET/POST /api/v1/lms/enrollments`, `POST /api/v1/lms/enrollments/{id}/progress`, `GET /api/v1/lms/progress`. |
| Assessment | Learner | `POST …/quiz-attempts` and `POST …/exam-attempts` — both graded server-side. Answers are never scored in the browser. |
| Certificates | Learner + public | `GET /api/v1/lms/certificates`; `GET /api/v1/lms/certificates/verify/{serial}` is public, for a verification page. |
| Cart & checkout | Publishable key | `/api/v1/lms/cart/*` — create, read, add/remove, checkout. Anonymous; no learner session needed. |
| Learner admin | Secret key | `/api/v1/lms/learners*` — list, create, update, enroll, read certificates; `DELETE /api/v1/lms/enrollments/{id}` revokes. |
| Reporting | Secret key | `/api/v1/lms/reports/courses`, `…/courses/{id}/completion`, `/api/v1/lms/progress/export` (CSV or JSON). |
| Exams & credits | Secret key | `/api/v1/lms/lessons/{id}/exam` and its `…/questions`; `/api/v1/lms/ceu-types`, `/api/v1/lms/courses/{id}/ceus`. |
| Keys & webhooks | Secret key | `/api/v1/keys*`, `/api/v1/webhooks*` including deliveries and redelivery. |
Platform routes (`/api/v1/me`, `/api/v1/auth/*`, keys, webhooks) are product-neutral; LMS resources sit under `/api/v1/lms/*`. Lists are **cursor-paginated** — pass the `next_cursor` you were given, or let the SDK's `iterate()` walk it. The full, generated endpoint index is in `llms-full.txt`; the live contract is at `https://app.peerfold.com/api/v1/openapi.json`.
## Golden paths
### A catalog page
The catalog is learner-scoped: it returns what *this* learner is allowed to see, so gated and members-only courses appear correctly without you filtering anything.
```tsx
import { useCatalog } from "@peerfold/react";
export function Catalog() {
const { data: courses, loading, error, reload } = useCatalog();
if (loading) return