Skip to main content
This guide is for content providers implementing the learner launch flow: when an end-customer’s user clicks a course in their LMS, they land on your platform via the deep link you pushed through Kombo. This page covers how to authenticate that learner without a login prompt and how to match them to the user data you already sync from the LMS. For the broader content provider flow (syncing users, pushing courses, writing completions), see the content providers guide.

Why this guide exists

When a learner launches a course from their LMS, you need to answer:
  1. Who is this user?
  2. Which end-customer do they belong to?
  3. Are they authorized to view this specific course?
Without solving these, you’ll either have to show a login screen – which adds friction that the LMS integration was meant to avoid – or serve content to unauthenticated users, which isn’t acceptable for enterprise deployments. This guide covers identity – how to authenticate the learner without a login prompt and match them to the Kombo user data you already sync. Authorization is covered briefly at the end, with guidance on how to handle it using Kombo’s existing endpoints. We recommend a hub-and-spoke model where both the LMS and your platform are SAML Service Providers trusting your end-customer’s enterprise Identity Provider (Okta, Entra ID, Google Workspace, ADFS, etc.). The launch flow: The learner sees a brief redirect and lands in the content. No login screen.
Kombo is not in the authentication path. The seamless experience comes from the IdP’s existing session, not from anything Kombo does at launch time. Your platform’s uptime during course launch depends only on your infrastructure and your end-customer’s IdP – not on ours.

Why the hub-and-spoke model (and not LMS-as-IdP)

Some LMSs (e.g. SAP SuccessFactors via its OCN framework) can act as SAML Identity Providers themselves. In that model, your platform would configure a SAML trust relationship with each end-customer’s LMS instance instead of with their enterprise IdP. We recommend the enterprise IdP path as the default:
  • Enterprise customers already use their IdP for every other SaaS app. Using it for your platform too is consistent with how they manage identity everywhere else.
  • The enterprise IdP is more stable than the LMS. LMS instances change, get migrated, or get replaced; the IdP tends to outlive all of them.
  • Some LMSs (notably Workday Learning) can’t act as an IdP at all, so you’ll need the enterprise IdP path for those customers regardless.
The LMS-as-IdP model is a reasonable fallback when the end-customer won’t or can’t configure your platform directly against their enterprise IdP. It works the same way structurally – SAML assertions with a shared identifier – but the SAML trust is established with the LMS instead of the IdP.

Identity matching: the actual Kombo-specific part

Authentication alone isn’t enough. Once the learner is authenticated via SSO, your platform has some identity for them – usually an email, maybe a name. But to call Kombo’s API on their behalf (checking enrollments, writing completions), you need to map this authenticated user to a Kombo user. What you need is a shared identifier that:
  • Exists in both the SAML assertion and in Kombo’s synced user data
  • Is unique and stable
  • Comes through reliably at every launch
The shared identifier is the LMS-side user ID that Kombo exposes as the remote_id field on each user returned from GET /lms/users. If your platform receives the same identifier in the SAML assertion, you can:
  • Look the user up via GET /lms/users?remote_ids=E12345 and use the Kombo id downstream
  • Or, on action endpoints (e.g. enroll, complete), use the remote: prefix (e.g. user_id: "remote:E12345") to skip the lookup entirely
A few important details about what remote_id actually holds, per LMS:
  • SAP SuccessFactors – the Learning studentID (as displayed in Learning admin). Many SF tenants are configured so this equals the Employee Central user ID, but that’s a per-tenant choice, not a guarantee – confirm with the end-customer’s IT team which identifier their IdP should pass.
  • Workday Learning – the Workday WID, pulled from Worker_Reference. This is the master identifier Workday uses for the worker across its systems, so customers who already surface a Workday identifier to their IdP are likely passing the WID already. Note that it’s not the human-readable Employee ID – if the end-customer’s IdP only has the Employee ID, that won’t match, and you’ll need to either have the WID surfaced as an attribute or fall back to email.
  • Cornerstone OnDemand – the system’s internal user ID.
For the LMSs above, remote_id is a single clean primary key. Elsewhere in Kombo it can occasionally be a composite when the remote system has no single stable key – if you ever see that for an LMS user, parse out the relevant component and match on it, or fall back to email if that isn’t possible. We’re happy to help figure out the specific case.

Where the shared identifier comes from

This varies based on how your end-customer has set up their identity architecture. The three common cases: Case A – Enterprise IdP with HRIS as source of truth. Most common for corporate customers. The identifier (typically an employee identifier – see the per-LMS notes above for which specific field matches) is synced from the HR system into the IdP via SCIM or directory sync. To get it into the SAML assertion to your platform, the end-customer’s IT team configures the SAML app for your platform to include the identifier as a claim. Case B – Enterprise IdP with the LMS maintaining its own user database. The LMS user ID may not exist in the IdP at all. Either the end-customer has configured a sync between their LMS and their IdP (so the LMS ID is available as an attribute), or they haven’t – in which case you’ll need to fall back to a weaker identifier like email. Case C – LMS itself is the IdP. The SAML assertion comes directly from the LMS. The LMS user ID is naturally available in the assertion, but you need to know where to look for it (the NameID element vs. a specific custom attribute). The guide below applies to all three cases – what changes is the conversation your team has with the end-customer’s IT team during onboarding.

Three tiers of identity configuration

In order of preference, there are three ways to get the shared identifier into the SAML assertion. Recommend the NameID approach during onboarding, fall back to a custom SAML attribute or email matching as needed. The end-customer configures their IdP so the SAML NameID element carries the LMS user ID.
<saml:Subject>
  <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">
    E12345
  </saml:NameID>
</saml:Subject>
On your side, you read NameID from the assertion and use it directly as the user’s remote_id when calling Kombo’s API. No additional configuration on your platform. This is the cleanest setup, but it’s often not available – NameID is frequently already mapped to email for other SAML apps your end-customer uses, and changing it would break those.

Tier 2: Custom SAML attribute (alternative)

If NameID is taken, the end-customer configures a custom SAML attribute with the LMS user ID. A conventional name is lms_user_id, but it can be whatever fits their naming scheme.
<saml:AttributeStatement>
  <saml:Attribute Name="lms_user_id">
    <saml:AttributeValue>E12345</saml:AttributeValue>
  </saml:Attribute>
</saml:AttributeStatement>
Your platform needs a per-end-customer configuration option for which attribute to read. Store this alongside your other per-tenant SAML settings.

Tier 3: Email fallback

If neither the NameID nor custom-attribute approach is possible (e.g. the end-customer’s IdP doesn’t have the LMS user ID available at all, or their IT policy won’t allow custom attributes), you’ll have to fall back to matching on email. Call Kombo’s users endpoint with the work_emails filter:
curl --request GET \
  --url 'https://api.kombo.dev/v1/lms/users?work_emails=jane.doe@acme.com' \
  --header 'Authorization: Bearer <api_key>' \
  --header 'X-Integration-Id: <integration_id>'
Take the Kombo user’s id from the response and use it for subsequent API calls. Email matching is case-insensitive.
Email is a weaker identifier than an LMS user ID. The concrete risks:
  • Emails change (marriage, rebranding, moves between subsidiaries). If the LMS and IdP update at different times, the assertion email won’t match any synced Kombo user until both sides catch up, and the learner can’t launch in the meantime. Document for the end-customer that email changes need to be mirrored between the LMS and the IdP.
  • Emails aren’t guaranteed unique. In multi-tenant LMS setups (one instance serving multiple subsidiaries) the same address can map to more than one user.
  • No match, no launch. If the email doesn’t resolve to a Kombo user, you have no ID to enroll against or write completions to.
Use email matching only when the NameID and custom-attribute approaches are genuinely unavailable. If email matching won’t work for a given end-customer either, see the FAQ on falling back when no viable identifier is available.

What your engineering team needs to implement

Your platform already accepts SAML assertions from enterprise IdPs – we’re not going to document that here. The Kombo-specific pieces are:
  • Extract the shared identifier from NameID, the configured custom SAML attribute, or the email – whichever your end-customer’s IdP provides.
  • Use the identifier with Kombo’s API.
    • When you have the LMS-side user ID (from NameID or a custom attribute), pass it directly on action endpoints like enroll using the remote: prefix (e.g. user_id: "remote:E12345"); the same prefix works for course_revision_id. On GET filter endpoints, resolve the Kombo user once via GET /lms/users?remote_ids=E12345 and reuse the returned id on subsequent calls.
    • When you only have the email, resolve via work_emails first.

Conceptual example: custom SAML attribute setup

A concrete scenario to ground the tiers: Setup:
  • End-customer uses an enterprise IdP (e.g. Entra ID, Okta).
  • Their LMS is an enterprise system where the user has a stable LMS-side ID (e.g. SuccessFactors studentID, Workday WID).
  • That ID is available in the IdP as a user attribute (synced from their HR system or LMS).
  • NameID is already mapped to email for other SAML apps, so they expose the LMS user ID as a custom attribute instead.
Configuration steps for the end-customer IT team:
  1. Add your platform as an Enterprise Application / SAML app in their IdP.
  2. Configure the SAML SSO settings (your entity ID, ACS URL, signing certificate).
  3. In the attributes/claims section, add a custom claim named lms_user_id mapped to the user attribute containing the LMS user ID.
  4. Assign the relevant users or groups to the application.
What the assertion looks like:
<saml:Assertion>
  <saml:Subject>
    <saml:NameID Format="...emailAddress">jane.doe@acme.com</saml:NameID>
  </saml:Subject>
  <saml:AttributeStatement>
    <saml:Attribute Name="lms_user_id">
      <saml:AttributeValue>E12345</saml:AttributeValue>
    </saml:Attribute>
    <!-- ... other attributes like name, email, etc. ... -->
  </saml:AttributeStatement>
</saml:Assertion>
What your platform does on receipt (pseudo-code):
# This is what Kombo exposes as `remote_id` on GET /lms/users.
lms_user_id = assertion.get_attribute("lms_user_id")     # "E12345"

# Store on the session for subsequent Kombo API calls.
session.user            = authenticated_user
session.lms_user_id     = lms_user_id   # matches Kombo's remote_id
session.integration_id  = tenant.kombo_integration_id
Later, when you call Kombo on behalf of this user:
  • On the enroll endpoint, pass user_id: "remote:E12345" directly. For complete, use the course_progression_id returned by enroll.
  • On GET endpoints with filters, resolve first: GET /lms/users?remote_ids=E12345 → use the returned Kombo id.
The specific attribute names, UI paths, and setup steps vary by IdP and by how the end-customer has provisioned their users. The end-customer’s IT team will know their setup best – the guide above is the template, not a prescriptive script.

Authorization: identity doesn’t solve everything

With everything above in place, you know who the learner is and can make API calls on their behalf. What you still don’t know is whether they’re actually authorized to view the specific course they clicked. There are two patterns we see content providers use, depending on how their end-customers work:

Pattern 1: Learning library (SSO is the gate)

The LMS admin publishes content broadly and any authenticated employee can view it. SSO handles authentication and, by extension, authorization – if you’re in the IdP, you’re an employee, and you can view the library. This is the simplest pattern and works well for self-serve content. No enrollment checks needed on your side.

Pattern 2: Assigned content (enrollment is the gate)

For compliance training, onboarding, or role-specific courses, the LMS admin assigns specific courses to specific users. Your platform should check that the learner was actually assigned the course before serving it. Use Kombo’s course progressions endpoint, filtering by user and course. A few things to know about this endpoint:
  • Filter by Kombo IDs. user_ids and course_ids on GET /lms/course-progressions take Kombo IDs. Since the SAML assertion gives you an LMS-side user ID, resolve it once with GET /lms/users?remote_ids=E12345 and reuse the returned id on subsequent calls.
  • Progressions live at the revision level. Each progression row has a course_revision_id (a specific version of a course), not a course_id directly. The course_ids filter takes the parent course ID and internally matches progressions on all of that course’s revisions – which is what you want 99% of the time.
  • Store the Kombo course ID at upsert time. Bulk upsert is an async endpoint: POST /lms/courses/bulk returns a task_id, and you poll GET /lms/courses/bulk/{task_id} until completion to get each course’s Kombo id alongside the origin_id you supplied. Persist that mapping alongside your own course data. Subsequent enrollment checks can then resolve your internal course ID to the Kombo course ID directly from your own store. If the mapping is ever lost, GET /lms/courses returns origin_id in the payload. So in practice, the check looks like:
# 1. Resolve user (from the SAML-provided remote_id)
curl --request GET \
  --url 'https://api.kombo.dev/v1/lms/users?remote_ids=E12345' \
  --header 'Authorization: Bearer <api_key>' \
  --header 'X-Integration-Id: <integration_id>'
# → kombo_user_id

# 2. Look up progressions
curl --request GET \
  --url 'https://api.kombo.dev/v1/lms/course-progressions?user_ids=<kombo_user_id>&course_ids=<kombo_course_id>' \
  --header 'Authorization: Bearer <api_key>' \
  --header 'X-Integration-Id: <integration_id>'
For writes, enroll takes user_id and course_revision_id directly (use the remote: prefix to pass LMS-side IDs without a separate lookup). Note that enroll is keyed on course_revision_id, not the parent course_id used in the filter above – grab the revision id from the course response or from a progressions row. Complete takes a course_progression_id path parameter; reuse the Kombo ID returned by enroll, or resolve it via GET /lms/course-progressions. Grant access when the progression status is ENROLLED, IN_PROGRESS, or COMPLETED. Anything else – or no progression at all – should usually mean no access.
The sync delay caveat. Because Kombo syncs enrollment data periodically rather than fetching it in real time, there’s a gap between when an admin assigns a course and when Kombo has the data. The exact gap depends on the LMS and on the sync frequency configured for your integration. Expect it to be on the order of minutes to a few hours for most LMSs.
This matters for the edge case where an admin assigns a course and the learner clicks it immediately. Our recommendation:
  • Treat the enrollment check as the authoritative gate.
  • If the check fails (no progression found), show the learner an error state that explains the likely cause – for example: “This course isn’t available to you yet. If your administrator just assigned it, please check back in a short while.”
  • Log these cases so you can tell the difference between genuine “unauthorized” attempts and sync-delay misses.
We’re actively working on reducing sync latency and exploring more granular sync options.

FAQ / gotchas

What if the end-customer’s IdP can’t pass custom attributes? Very rare in practice – all major enterprise IdPs support this. If you hit a genuine blocker, fall back to email matching. What about users who exist in the LMS but not in the IdP (e.g. contractors, external trainees)? These users won’t be able to launch courses via SSO at all. Depending on your product, you may want to offer a separate login flow for these users, or work with the end-customer to ensure all learners are in the IdP. My end-customer passes the Workday Employee ID in SAML, but remote_id on /lms/users doesn’t match. Why? Workday Learning keys its user records on the WID (an internal Workday ID from Worker_Reference), not the human-readable Employee ID. Have the end-customer surface the WID as an additional IdP attribute so it reaches your platform in the SAML assertion – no change needed on your side beyond reading the new attribute. Does the SAML trust relationship need to be set up per end-customer? Yes. Each end-customer has their own IdP and their own SAML metadata. Your platform needs a tenant-aware SAML setup where each end-customer has their own SP configuration. Most enterprise SaaS products already have this; if yours doesn’t, consider a service like WorkOS or Auth0 that federates many IdPs behind a single integration. Can I avoid the per-end-customer SAML setup? Not really – identity is inherently per-customer. The configuration work moves to whoever owns the trust relationship (your platform, WorkOS, etc.), but it doesn’t disappear. What if my end-customer can’t set up SSO at all (or no viable identifier comes through)? You have two fallback paths. First, you can show your own login screen after the redirect – not ideal for the first launch, but if you keep session lifetimes generous (weeks or months), repeat launches are seamless. Some content providers run this way in production and report it’s acceptable in practice; the friction is concentrated on the first click, not every visit. Second, depending on your product, you might offer a one-time account claim flow (learner enters email, receives verification link, creates credentials) which bridges the first login a bit more smoothly.

Next steps

Content providers guide

The broader integration flow: syncing users, pushing courses, and writing completions back.

LMS API reference

Detailed endpoint documentation for users, courses, and course progressions.