Reseller integration — end-to-end
A step-by-step recipe for embedding Schole's course catalogue inside your own platform — an LMS, a marketplace, or a corporate portal — so your clients can browse Schole content, launch lessons single-sign-on, and see completion flow back into their reporting.
A reseller integration reuses building blocks documented elsewhere in this portal — the Partner SSO hand-off, the course catalogue, and xAPI tracking. This guide stitches them into one linear flow. Each step links to the authoritative reference.
Authenticate
Ask your Schole contact for an OAuth2 client-credentials application
scoped to the organisation you are reselling for. You receive a client_id
and client_secret. Exchange them for a bearer token; every call in this
guide carries that token.
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 }
Use the trailing slash on /oauth/token/ — some HTTP clients
drop the body when following the redirect on a slash-less POST.
Resellers typically manage several end-clients, each with their own user base. Two models:
client_id / client_secret and its own
tenant. Cleanest separation of users, catalogue and reporting. Provisioned manually today,
so best when you onboard clients gradually.provider_id (e.g. clientA-12345). Simpler to start, but mixes
users — use only if you need a single account from day one.There is no self-service provisioning yet, so coordinate with your Schole contact on the model and on creating each client's credentials.
Sync the catalogue
GET /api/v1/curricula/templates/ is the backbone of the browsing experience —
it returns every template visible to your organisation. Mirror each one as a course (or an
activity) on your side. The endpoint is filterable, sortable and paginated, so a nightly
delta sync is cheap.
# Full first pull (page through until `next` is null)
curl -G https://app.schole.ai/api/v1/curricula/templates/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
--data-urlencode "page=1" \
--data-urlencode "page_size=100"
# Nightly delta sync — only what changed since the last run
curl -G https://app.schole.ai/api/v1/curricula/templates/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
--data-urlencode "modified_after=2026-06-01T00:00:00Z" \
--data-urlencode "sort=-modified_date,title"
| Query param | Use |
|---|---|
search |
Keyword match on title or description. |
language |
Language code (en, de, fr, it). |
lesson_type |
mandatory, premade, or custom. |
sort |
Comma-separated; prefix - for descending. Allowed: title, creation_date, modified_date, lesson_type, language, category, participant_count. Default title. |
modified_after |
ISO-8601 timestamp. Returns only templates changed since then — the basis of delta sync. |
The response is the standard paginated envelope (count / next /
previous / results) — see
Pagination. For the exact, always-current
field list of a template object, explore the live schema in
Swagger UI rather than hard-coding fields.
Templates can be retired (soft-deleted). A retired template is hidden from this list by default and can no longer be picked for new lessons, but existing learners who already started it keep working. Because retired rows simply drop out of the default list, your nightly reconciliation should treat a template that has disappeared as removed.
- Pass
?include_inactive=trueto also return retired templates, then read each row'sis_activeboolean to tell them apart.
Scenarios are voice-driven role-plays — a learner practises a real conversation
against an AI character and is scored on the points they covered. They are a separate
catalogue from lessons, with the same query vocabulary
(search, language, sort,
modified_after, include_inactive):
curl https://app.schole.ai/api/v1/scenarios/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN"
- Key on
scenario_id(scn_…). It is stable across re-authoring and across the organizations that adopt a shared scenario. - Adopt before assigning.
POST /api/v1/scenarios/library/adds a scenario to your client's library, which is what entitles their learners to play it. Assigning adopts automatically — see Step 4. is_sharedmarks platform-wide content, available to every client, as opposed to a scenario that client owns.
GET /api/v1/curricula/templates/ as auto-generated wrapper lessons. Those rows
carry "is_scenario_wrapper": true and the matching scenario_id.
Skip them when building your catalogue, or you will list the same content twice. They keep
working indefinitely — we will not remove them — but new scenarios are never
wrapped.
Provision learners
Upsert each learner with POST /api/v1/users/ when they are onboarded on your side
(and again whenever their details change — it is idempotent). Key every learner on
provider + provider_id (your stable id) so re-syncs never
create duplicates and so the SSO hand-off in Step 4 can find them.
curl -X POST https://app.schole.ai/api/v1/users/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"provider": "totara",
"provider_id": "clientA-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"
}'
# → 201 Created (new) or 200 OK (updated). The response carries Schole's internal user id.
provideris a free label you choose and use consistently (e.g."totara"). It marks these users as partner-managed, so they skip Schole's own skills-selection onboarding.- Richer profile = better lessons. Because each lesson is personalised on the
fly, the more context you pass (
user_summary,job_titles,department,tools,daily_tasks), the more tailored the content. All optional. - IDs in responses are Schole-internal integers. Assume internal IDs everywhere
unless a field is explicitly your
provider_id.
Launch a lesson (single sign-on)
When a learner clicks a course in your UI, hand them off into Schole already logged in. Mint a
one-time login token for them, then open Schole's /auto-login route pointed at the
lesson launcher. This is the Partner SSO
flow — summarised here for the reseller path.
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": "clientA-12345" }'
# → { "login_token": "eyJ…", "expires_in": 3600 }
Two optional fields on that same call, useful if you want the lesson to feel embedded in your site rather than a hand-off to Schole's own UI:
hide_exit_button(boolean, defaultfalse) — hides Schole's in-lesson exit control for the whole session, including the lesson-complete screen, so the learner closes the tab back to your site instead of navigating around inside Schole.return_url(optional URL) — anhttp(s)URL on your site, typically the course page the learner launched from. It applies to the lesson the link opens. Withhide_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.
Open the launcher in a new tab, passing the token and a percent-encoded next that
targets the template you mirrored in Step 2:
https://app.schole.ai/auto-login?token=eyJ…&next=%2Fstart-lesson%3Ftemplate_id%3D123
Scenarios use the same token and the same flow, pointed at the scenario launcher:
https://app.schole.ai/auto-login?token=eyJ…&next=%2Fstart-scenario%3Fscenario_id%3Dscn_4f3a…
Assign the scenario first — this also adopts it into the client's library, so a separate adoption call is not needed:
curl -X POST https://app.schole.ai/api/v1/scenarios/assignments/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "scenario_id": "scn_4f3a…", "user_id": 4711 }'
# → 201 Created, { …, "adopted": true }
The launcher checks server-side whether this learner may open this scenario, so one your
client no longer has, or one that has been withdrawn, shows an explanatory page rather than
a spinner. Pass
?adopt=false on the assign call if you would rather it fail when the scenario
is not already in the library, matching the lesson endpoints.
login_token is single-use and lives ~1 hour. In an LMS a
learner reopens a course page many times, so a token baked into the rendered link breaks on the
second click. Instead point each course link at a small redirect endpoint on your side
that calls partner-exchange on every click and forwards to /auto-login.
No change is required on Schole's side — this is the intended pattern.
Launching a template_id instantiates the lesson on the fly, so you do not need
to pre-create anything. If you'd rather seed a learner's home with assigned courses ahead of time,
create the curriculum explicitly with POST /api/v1/curricula/:
curl -X POST https://app.schole.ai/api/v1/curricula/ \
-H "Authorization: Bearer $SCHOLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "user_id": 4567, "template_id": 123 }' # user_id = Schole internal id from Step 3
Track completion
Completion fires per lesson. You have two ways to get it back into your platform — use either or both.
attempted / progressed /
completed statements to your Learning Record Store as they happen.xAPI: give your Schole contact your LRS endpoint and its auth. Schole supports both HTTP Basic Auth and OAuth2 client-credentials (Bearer) — for an LMS, provide the token endpoint, client id and secret, and Schole obtains and refreshes the Bearer token itself. Full setup in the xAPI section.
Analytics pull: GET /api/v1/analytics/users/<id>/summary/
(also /progress/ and /scores/), where <id> is Schole's
internal user id. For a richer event feed, see
Webhooks & Events.
- Credentials issued and a token can be obtained (Step 1).
- Catalogue sync runs and returns templates; nightly delta uses
modified_after(Step 2). - User upsert is idempotent on
provider_id(Step 3). - Course links mint a fresh token per click and land the learner in the lesson (Step 4).
- Completion is flowing back via xAPI and/or the analytics API (Step 5).