Integrations
Schole connects to the tools your organisation already uses — from your HR system to your Learning Record Store — keeping data in sync without manual work.
This integration lets a partner platform drop its already-signed-in users straight into a Schole lesson — no second registration, no second login. Your backend pre-registers each learner over the API, then mints a one-time link that opens the lesson in a new tab, already authenticated.
Ask your Schole administrator for an OAuth2 client-credentials application
scoped to your organisation. You receive a client_id and client_secret.
Every API call below is authenticated with a bearer token obtained from them:
curl -X POST https://app.schole.ai/oauth/token/ \
-d grant_type=client_credentials \
-d client_id=$SCHOLE_CLIENT_ID \
-d client_secret=$SCHOLE_CLIENT_SECRET
# → { "access_token": "…", "token_type": "Bearer", "expires_in": 43200 }
Call POST /api/v1/users/ when a user is onboarded on your side (and again
whenever their details change — it is an idempotent upsert). Identify each learner with
provider + provider_id (your stable user id) so re-syncs never
create duplicates.
curl -X POST https://app.schole.ai/api/v1/users/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"provider": "partner",
"provider_id": "ext-12345",
"email": "anna@acme.com",
"first_name": "Anna",
"last_name": "Keller",
"preferred_language": "de",
"user_summary": "Backend engineer, 3y; wants to learn Kubernetes.",
"job_titles": ["Backend Engineer"],
"department": "Platform",
"tools": ["Python", "Docker"],
"daily_tasks": ["Code review", "On-call"]
}'
# → 201 Created (new) or 200 OK (updated)
| Field | Why it matters |
|---|---|
provider + provider_id |
Stable identity for the learner. Required for the hand-off in Step 2 and for idempotent re-syncs. Also tells Schole this user is partner-managed, so they skip Schole's skills-selection onboarding. |
preferred_language |
Pass it if you know it (en, de, fr, it). Lessons are delivered in this language; if omitted the learner is asked to choose. |
user_summary |
A free-text description of the learner. Feeds Schole's AI personalization of lesson content. |
course + cohort |
The course the learner bought and the intake they're joining. Enrolls the learner and grants any premium access the course carries. See Course & cohort enrollment below for the full contract, including a mode for partners who can't always supply a pre-registered course. |
job_titles, department, tools, daily_tasks |
Optional. Improves AI personalization of lessons. Not required to reach the lesson — partner-provisioned learners skip Schole's onboarding automatically. |
course + cohort name the commercial course/deal and dated
intake a learner belongs to — send both or neither. Not the same id as
template_id (Step 2), which opens a piece of lesson content.
course |
The course's Schole slug (e.g. agentic-ai), case-insensitive. No self-serve lookup yet — ask your Schole contact for slugs, or send your own names and ask us to map them. |
cohort |
The intake's published start date, YYYY-MM-DD, matched exactly. Ask your Schole contact for the dates that apply to you. |
One without the other, or a value matching nothing on file, is a 400
naming the problem. Omitting both is fine — the learner is still created, just
unenrolled. Re-sending a different cohort moves the learner (and their
access window) to that intake.
Flexible matching (opt-in — ask your Schole contact) is for
integrations that can't always supply a pre-registered course or intake: a missing
course, a missing cohort, or an unrecognized
course value all still enroll the learner instead of failing, on a fixed
default window (currently 60 days) rather than a date you supplied. An unrecognized
course mints a new course scoped to your integration, so another
partner's text can never collide with yours.
Two things to know before relying on it: send a stable id, not a
display title, as course — renaming it later mints a different course
and strands existing enrollments. And a minted course carries no lesson content and
the default access window until Schole curates it, so send your contact a reference
list (id, institution, display name) as you add courses.
Mint a login_token for the learner by calling
POST /api/v1/auth/partner-exchange/ from your backend with their id. The
token is single-use and valid for 1 hour — long
enough to embed directly in a lesson link rendered on your page, or to mint on click
(see the note in Step 3 on which to choose).
curl -X POST https://app.schole.ai/api/v1/auth/partner-exchange/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "external_id": "ext-12345" }'
# → { "login_token": "eyJ…", "expires_in": 3600 }
Two optional fields shape what happens inside the lesson for that link:
| Field | Why it matters |
|---|---|
hide_exit_button |
Boolean, default false. Hides Schole's in-lesson exit control for the whole session, including the lesson-complete screen — use this if you want the learner to close the tab and return to your site rather than navigate around inside Schole. |
return_url |
Optional http(s) URL on your site, typically the course page the learner launched from. It applies to the lesson the link opens. With hide_exit_button, the lesson-complete screen shows a button back to it and redirects there automatically after a short countdown. Without it, the learner keeps Schole's full completion screen and its Continue button leads back to your URL instead of into Schole. |
curl -X POST https://app.schole.ai/api/v1/auth/partner-exchange/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_id": "ext-12345",
"hide_exit_button": true,
"return_url": "https://partner.example.com/courses/123"
}'
# → { "login_token": "eyJ…", "expires_in": 3600 }
Open Schole's /auto-login route in a new tab, passing the token and a
next path. Schole redeems the token, establishes the session, and forwards
the learner to next.
To drop the learner straight into a ready lesson, point next
at the lesson launcher with a template_id. The launcher instantiates the lesson
for that learner (or reuses it if they've started it before) and opens it — content
generation begins automatically.
https://app.schole.ai/auto-login?token=eyJ…&next=%2Fstart-lesson%3Ftemplate_id%3D123
Get a template_id from the catalog: GET /api/v1/curricula/templates/.
You can also send the learner to any other relative Schole path (e.g. their home,
/home) if you don't want to target a specific lesson.
The login_token is single-use (burned the first time it's redeemed) and
valid for 1 hour. Two things to keep in mind when you embed links on a page:
- One token per link. A redeemed token can't be reused, so a
back-button or refresh re-click of the same baked-in link will fail. If you need
re-clickable links, mint on click instead (e.g. point the link at a small redirect
endpoint on your side that calls
partner-exchangeand forwards to/auto-login). - The token rides in the URL — so it appears in browser history and
the
Refererheader. For pages that stay open a long time, minting on click keeps the token fresh and its exposure brief.
next must be a relative Schole path; if it carries its own query string
(e.g. ?template_id=123) percent-encode it so it isn't parsed as part of the
outer URL.
Two options, both already available:
- Push (xAPI): point Schole at your Learning Record Store and receive
attempted/progressed/completedstatements in real time — see the xAPI section below. - Pull (analytics): read a learner's progress on demand with
GET /api/v1/analytics/users/<id>/summary/(and/progress/,/scores/).
If you run an LMS (Canvas, Moodle, Blackboard, D2L…), you can launch learners into Schole with LTI 1.3 instead of the custom hand-off above. Schole is the Tool; your LMS is the Platform. A learner clicks a Schole link in your LMS and lands in the lesson, already logged in — no pre-registration call required (accounts are created just-in-time from the launch).
issuer, the client_id issued to Schole, the deployment_id, your OIDC auth URL and your JWKS URL.| LMS setting | Schole URL |
|---|---|
| OIDC login init URL | https://app.schole.ai/api/v1/lti/login/ |
| Redirect / launch URL | https://app.schole.ai/api/v1/lti/launch/ |
- The learner clicks the Schole link in your LMS → the LMS hits Schole's login URL (OIDC third-party init).
- Schole redirects back to your platform's auth URL; your LMS returns a signed
id_token. - Schole verifies the
id_tokenagainst your JWKS, creates/updates the learner (identity fromsub; name,emailandlocalefrom standard claims), and opens the lesson — logged in.
No. With LTI the account is created just-in-time from the launch, so a learner can launch and start a lesson with no prior API call.
Pre-registering with POST /api/v1/users/ (see Partner SSO,
Step 1) is optional and only useful when you want something LTI doesn't
carry: richer AI personalization (user_summary, role, tools,
daily tasks) or pre-launch state (assigned lessons, dashboards)
before anyone clicks. If you do both, match on email so the API record and the LTI
launch resolve to the same learner — so send email in the launch claims.
Add a custom parameter template_id to the link's launch
configuration and the learner lands directly on that lesson (otherwise they land on
their home). Template ids come from GET /api/v1/curricula/templates/.
template_id=123
Launches open in a new tab (in-iframe embedding is not yet supported). Grade passback (AGS), roster sync (NRPS) and Deep Linking content selection are planned increments.
Kombo.dev connects Schole to your existing HR system so that user accounts and teams stay in sync automatically — no manual provisioning required.
Supported HRIS platforms include Workday, SAP SuccessFactors, Personio, BambooHR, and many others.
| Event in your HRIS | What happens in Schole |
|---|---|
| Employee joins | A Schole account is created automatically |
| Employee's name or job title changes | Their Schole profile is updated |
| Employee leaves | Their Schole account is deactivated |
| A group / team is created or updated | The corresponding Schole team is created or its members updated |
Kombo is configured per organisation. To enable it, contact your Schole administrator with:
- Your Kombo integration ID (obtained from the Kombo dashboard after connecting your HRIS)
- A webhook secret for secure delivery of real-time events
- A Kombo API key for the nightly reconciliation sync
xAPI 1.0.3 (also known as Experience API or Tin Can) is an industry standard for recording learning activity. Schole can send xAPI statements to your organisation's Learning Record Store (LRS), giving you a detailed, standardised record of how learners engage with content.
| Event | xAPI verb |
|---|---|
| Learner starts a subsection | attempted |
| Learner completes a subsection (course not yet finished) | progressed |
| Learner finishes the whole course | completed |
attempted is scoped to the specific subsection the learner just started —
its object.id is an opaque internal IRI keyed on that instantiated lesson
(not a public URL you can resolve or call), e.g.
https://app.schole.ai/lessons/482/subsections/931.
progressed and completed are scoped to the course
instead: object.id is
https://app.schole.ai/curricula/templates/<template_id> — the same
template_id you already use to launch a lesson — so every statement about one
course, from every learner and every re-instantiation, shares a single stable Activity your
LRS can aggregate against. They fire once per subsection completed (a checkpoint, not
per-message), carry the learner's overall percentage across all sections of the
course under result.extensions (the
cmi5 progress extension
IRI, 0–100), and a result.completion boolean (false until the
course is done, true on the one completed statement).
Verb and extension keys are IRIs too, e.g. http://adlnet.gov/expapi/verbs/completed
or https://app.schole.ai/xapi/extensions/user_id — none of these are meant to be
fetched, they're just unique names. Learners are identified by email
(actor.mbox); statements also carry the learner's Integration API user id
and, when set, your external_id as context.extensions, so you can
correlate statements with the Users and Analytics endpoints.
progressed and completed are delivered at least once: a failed send
is retried automatically with backoff, and anything still failing (e.g. your LRS
momentarily unreachable, or rejecting a learner it hasn't synced yet) is retried again on a
short interval until it succeeds — nothing is silently dropped. attempted is
best-effort — a failed send is logged and not retried, since nothing depends on it arriving.
A retry re-sends the exact same statement — same id, same body — so your
receiver can safely dedupe or reprocess on that id without special-casing
retried deliveries.
Scenarios report separately from lessons, as
http://adlnet.gov/expapi/activities/simulation activities keyed on the
scenario_id you already hold from
GET /api/v1/scenarios/:
{SCHOLE_PUBLIC_URL}/scenarios/<scenario_id>. That id is stable across
tenants and across re-authoring, so one Activity aggregates every learner who ever ran
the scenario.
| Verb | When |
|---|---|
completed | The learner finished the scenario. |
passed / failed |
Sent alongside completed when the scenario defines a pass mark.
Ungraded scenarios send neither, and omit result.success entirely —
absence means "not graded", not "failed".
|
-
result.scoreis an xAPI score object (scaled,raw,min,max) carrying a 0-100 competence percentage over the concepts the scenario is designed to teach — not a count of correct answers. It is frozen at completion, so later edits to the scenario never restate a past result. -
result.durationcovers the session itself, andresult.extensionscarries a per-concept breakdown under.../extensions/scenario_skills— the part of a role-play an LMS cannot derive from a completion flag. -
Abandoned runs send no terminal statement. Most role-play sessions end
without completing, and an abandonment is not a result. Use
GET /api/v1/scenarios/assignments/<id>/progress/, which exposesended_reason, if you need to distinguish "in progress" from "gave up". -
Duplicate protection. Statement ids are derived deterministically from
the run, so a retry re-sends an identical statement under the same
id. Your receiver may return409for one it already holds; we treat that as success.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"actor": {
"objectType": "Agent",
"mbox": "mailto:alice@example.com",
"name": "Alice Smith"
},
"verb": { "id": "http://adlnet.gov/expapi/verbs/attempted", "display": { "en-US": "attempted" } },
"object": {
"objectType": "Activity",
"id": "https://app.schole.ai/lessons/482/subsections/931",
"definition": { "type": "http://adlnet.gov/expapi/activities/lesson", "name": { "en-US": "Intro to Negotiation" } }
},
"result": { "extensions": { "https://w3id.org/xapi/cmi5/result/extensions/progress": 0 } },
"context": { "extensions": { "https://app.schole.ai/xapi/extensions/user_id": 102, "https://app.schole.ai/xapi/extensions/external_id": "clientA-12345" } },
"timestamp": "2026-06-23T14:32:07.123456+00:00"
}
{
"id": "5b1f6a2e-9d3c-4a1b-8e2f-7c4d6a9b3e10",
"actor": {
"objectType": "Agent",
"mbox": "mailto:alice@example.com",
"name": "Alice Smith"
},
"verb": { "id": "http://adlnet.gov/expapi/verbs/progressed", "display": { "en-US": "progressed" } },
"object": {
"objectType": "Activity",
"id": "https://app.schole.ai/curricula/templates/57",
"definition": { "type": "http://adlnet.gov/expapi/activities/course", "name": { "en-US": "Negotiation Fundamentals" } }
},
"result": { "completion": false, "extensions": { "https://w3id.org/xapi/cmi5/result/extensions/progress": 60 } },
"context": { "extensions": { "https://app.schole.ai/xapi/extensions/user_id": 102, "https://app.schole.ai/xapi/extensions/external_id": "clientA-12345" } },
"timestamp": "2026-06-23T14:33:42.654321+00:00"
}
{
"id": "8e2f7c4d-6a9b-3e10-5b1f-6a2e9d3c4a1b",
"actor": {
"objectType": "Agent",
"mbox": "mailto:alice@example.com",
"name": "Alice Smith"
},
"verb": { "id": "http://adlnet.gov/expapi/verbs/completed", "display": { "en-US": "completed" } },
"object": {
"objectType": "Activity",
"id": "https://app.schole.ai/curricula/templates/57",
"definition": { "type": "http://adlnet.gov/expapi/activities/course", "name": { "en-US": "Negotiation Fundamentals" } }
},
"result": { "completion": true, "extensions": { "https://w3id.org/xapi/cmi5/result/extensions/progress": 100 } },
"context": { "extensions": { "https://app.schole.ai/xapi/extensions/user_id": 102, "https://app.schole.ai/xapi/extensions/external_id": "clientA-12345" } },
"timestamp": "2026-06-23T14:35:10.987654+00:00"
}
xAPI is configured per organisation. To enable it, provide your Schole administrator with:
- Your LRS endpoint URL (e.g.
https://lrs.example.com/xapi/) - The authentication method your LRS expects — Schole supports both:
- HTTP Basic Auth — a username/password (key & secret) sent on each request.
- OAuth2 client credentials (Bearer) — a token endpoint URL, client ID, client secret, and optional scope. Schole obtains a short-lived Bearer token via the
client_credentialsgrant, caches it, and refreshes it automatically.
Once configured, Schole will begin sending statements to your LRS automatically. Statements follow the standard xAPI format and include the learner's identity, the activity, and a timestamp.