Skip to main content

Learning Paths

Base URL: https://api-sls.platzi.com/production-sls-business-domains

Key Features:

  • Custom learning path creation with multi-step structure
  • Public learning path catalog browsing
  • Course catalog management
  • Learning path assignment to users and groups
  • Course assignment to individual users with optional deadline
  • Per-enrollment deadline management (PATCH / DELETE)
  • Objective tracking: company-wide report, per-objective detail, and per-student progress

API Endpoints

Course Catalog Management

GET /v3/lp-builder/{company_id}/courses/

List courses in the company's catalog with optional filters.

Path Parameters:

  • company_id (int) - Company ID

Query Parameters:

ParameterTypeRequiredDescription
namestringNoFilter by course name (partial match)
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 20, max 100)
privatebooleanNoFilter by private/public courses
organization_idintNoFilter by organization

Example Request:

GET /v3/lp-builder/123/courses/?name=python&page=1&page_size=20

Response:

{
"data": [
{
"course_id": 49,
"title": "Taller de creación de Startups 2019",
"description": "Lanza tu startup, valida ideas, muestra tracción y aprende a financiar, vender, escalar y medir los resultados con metodologías, herramientas y casos reales del mundo de la tecnología.",
"badge": "https://static.platzi.com/media/achievements/badge-taller-creacion-startups.png",
"thumbnail": null,
"link": "https://platzi.com/cursos/taller-startups-2019/",
"teachers": [
{ "name": "Freddy Vega" },
{ "name": "Christian Van Der Henst" },
{ "name": "Sam Altman" }
],
"duration_minutes": 1910,
"level": "intermedio",
"ranking": 4.7,
"category_name": "Startups"
},
{
"course_id": 1145,
"title": "Curso Profesional de Vue.js 2",
"description": "Aprende a crear aplicaciones profesionales con Vue.js. Gestiona el estado con Vuex, junto a rutas modernas con Vue Router.",
"badge": "https://static.platzi.com/media/achievements/badge-vue-js-profesional.png",
"thumbnail": null,
"link": "https://platzi.com/cursos/vuejs-profesional/",
"teachers": [{ "name": "Ignacio Anaya" }],
"duration_minutes": 520,
"level": "intermedio",
"ranking": 4.8,
"category_name": "Desarrollo e Ingeniería"
}
],
"metadata": {
"count": 1877,
"pages": 94,
"current_page": 1,
"page_size": 20
}
}

Response Fields:

FieldTypeDescription
data[].course_idintCourse ID
data[].titlestringCourse title
data[].descriptionstringCourse description
data[].badgestring | nullURL to the course badge image
data[].thumbnailstring | nullURL to the course thumbnail image
data[].linkstringPublic Platzi URL for the course
data[].teachersarrayList of teachers, each with a name field (may be null)
data[].duration_minutesintTotal course duration in minutes
data[].levelstringCourse difficulty (Spanish): básico, intermedio, avanzado
data[].rankingfloat | nullAverage course rating (null if unrated)
data[].category_namestringCourse category name
metadata.countintTotal number of courses matching the filters
metadata.pagesintTotal number of pages
metadata.current_pageintCurrent page number
metadata.page_sizeintItems per page

Public Learning Paths Discovery

GET /v3/lp-builder/{company_id}/learning-paths/public/

List available public learning paths from Platzi's catalog.

Path Parameters:

  • company_id (int) - Company ID

Query Parameters:

ParameterTypeRequiredDescription
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 20, max 100)

Example Request:

GET /v3/lp-builder/123/learning-paths/public/?page=1&page_size=20

Response:

{
"data": [
{
"learning_path_id": 201,
"title": "Backend Development with Python",
"badge": "https://static.platzi.com/media/achievements/badge-backend-python.png",
"link": "https://platzi.com/ruta/backend-python/"
}
],
"metadata": {
"count": 45,
"pages": 3,
"current_page": 1,
"page_size": 20
}
}

Response Fields:

FieldTypeDescription
data[].learning_path_idintLearning path ID
data[].titlestringLearning path title
data[].badgestring | nullURL to the badge image
data[].linkstringPublic Platzi URL for the learning path

Steps and courses are not included in this listing. Fetch them per-LP via the detail endpoint.


POST /v3/lp-builder/{company_id}/learning-paths/public/{learning_path_id}/

Add a public learning path to a company's catalog. The new entry starts in active state with the learning path's current steps. Fails if the learning path is private or already in the catalog.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Public learning path ID

Response (200 OK):

{
"data": {
"added_to_catalog": true
}
}

Error Cases:

StatusCause
400Learning path does not exist, is private, or is already in catalog

Custom Learning Path Management

GET /v3/lp-builder/{company_id}/learning-paths/

List all learning paths in a company's catalog (both public LPs added to catalog and custom company LPs).

Path Parameters:

  • company_id (int) - Company ID

Query Parameters:

ParameterTypeRequiredDescription
statestringNoFilter by catalog state. Omit to exclude deleted entries
searchstringNoCase-insensitive filter on the learning path title. Whitespace is trimmed; empty applies no filter
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 20, max 100)

The count and pages in the response reflect the filtered result set when search is provided.

Example Request:

GET /v3/lp-builder/123/learning-paths/?search=python&page=1&page_size=20

Response:

{
"data": [
{
"learning_path_id": 456,
"slug": "company-onboarding",
"title": "Company Onboarding Program",
"badge": "https://static.platzi.com/media/achievements/onboarding-badge.png",
"state": "active",
"created_by": "admin",
"private": true
}
],
"metadata": {
"count": 12,
"pages": 1,
"current_page": 1,
"page_size": 20
}
}

Response Fields:

FieldTypeDescription
data[].learning_path_idintLearning path ID
data[].slugstringURL slug of the learning path
data[].titlestringLearning path title
data[].badgestringFull badge URL (empty string if not set)
data[].statestringLearning path state (see Learning Path States)
data[].created_bystringSource: admin, placement_test, skill_matcher
data[].privatebooleanWhether this is a private (company-custom) LP

Steps and courses are not included here. Use the detail endpoint to fetch them per-LP.


POST /v3/lp-builder/{company_id}/learning-paths/

Create a new custom learning path for a company.

Path Parameters:

  • company_id (int) - Company ID

Request Body:

{
"title": "Company Onboarding Program",
"steps": [
{
"title": "Getting Started",
"courses": [101, 102, 103]
},
{
"title": "Advanced Topics",
"courses": [201, 202, 203, 204, 205]
}
],
"created_by": "admin",
"state": "active",
"group_ids": [12, 34]
}

Validation Rules:

  • title (required): String. Must be unique within the company's catalog (case-insensitive).
  • steps (optional): Array of step objects.
  • steps[].title (required): Step name.
  • steps[].courses (required, min 1): Array of course IDs. Each course must be either a public course or in the company's private catalog.
  • created_by (optional): One of admin, placement_test, skill_matcher. Default: admin.
  • state (optional): One of draft, active, archived, deleted. Default: active.
  • group_ids (optional, min 1 when present): Groups to assign the new path to immediately after creation. When present, the path is assigned only to these groups. When omitted, a manager's path is assigned to all the groups they manage; an admin's path is not assigned to anyone. Managers can only list groups they manage.

Response (201 Created): Same shape as the detail endpoint (GET /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/).

Error Cases:

StatusCause
400Learning path title already exists
400Some courses are not available for this company: [ids]
400Company with id {company_id} does not exist
400Invalid state: {state}
403out_of_scope — a manager listed a group they do not manage (checked before the path is created)
422group_ids was sent as an empty list

GET /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/

Get detailed information about a specific learning path, including its steps and courses.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID

Example Request:

GET /v3/lp-builder/123/learning-paths/456/

Response:

{
"data": {
"learning_path_id": 456,
"title": "Company Onboarding Program",
"slug": "company-onboarding",
"description": "Complete onboarding program for new hires",
"badge": "https://static.platzi.com/media/achievements/onboarding-badge.png",
"link": "https://platzi.com/ruta/company-onboarding/",
"private": true,
"steps": [
{
"title": "Getting Started",
"level": "basic",
"courses": [
{
"id": 101,
"title": "Introduction to Python",
"order": 1
}
]
}
],
"state": "active",
"created_by": "admin",
"students_count": 25
}
}

Response Fields:

FieldTypeDescription
learning_path_idintLearning path ID
titlestringLearning path title
slugstringURL slug
descriptionstring | nullDescription
badgestringFull badge URL
linkstringPublic Platzi URL for the learning path
privatebooleanWhether this is a private (company-custom) LP
stepsarrayOrdered list of steps
steps[].titlestringStep title
steps[].levelstringStep level (see Step Levels)
steps[].coursesarrayCourses in the step, with id, title, order, and additional details
statestringCatalog state: draft, active, archived, deleted
created_bystringSource: admin, placement_test, skill_matcher
students_countintNumber of users actively assigned to this LP in the company

Error Cases:

StatusCause
400Learning Path {id} not found. (not in this company's catalog)

PUT /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/

Update a custom (private) learning path's details. Public learning paths cannot be updated through this endpoint.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID

Request Body (all fields optional):

{
"title": "Updated Onboarding Program",
"description": "Comprehensive onboarding for all new hires",
"state": "active",
"steps": [
{
"title": "Fundamentals",
"courses": [101, 102, 103, 104]
},
{
"title": "Advanced Concepts",
"courses": [201, 202]
}
]
}

Behavior:

  • Only updates fields that are provided (partial update).
  • If steps is provided, it replaces all existing steps. Course order is determined by array index.
  • Course IDs in steps[].courses accept either plain integers or objects with an id field (e.g. { "id": 101 }).
  • Only new courses (not currently in the LP) are validated against the company catalog.

Response: Same shape as the detail endpoint (full LP refresh).

Error Cases:

StatusCause
400Learning path not found (not in this company's catalog)
400Cannot update public learning paths
400Learning path title already exists
400Some courses are not available for this company: [ids]
400Invalid state: {state}

DELETE /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/

Delete a learning path from the company's catalog. The learning path and its history are preserved for reporting but it is no longer assignable.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID

Response:

{
"data": {
"deleted": true
}
}

Error Cases:

StatusCause
400Learning path not found or cannot be deleted

Assignment Management

GET /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/users/

List users actively assigned to a learning path.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID

Query Parameters:

ParameterTypeRequiredDescription
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 20, max 100)

Example Request:

GET /v3/lp-builder/123/learning-paths/456/users/?page=1&page_size=20

Response:

{
"data": [
{
"company_user_id": 789,
"email": "john.doe@company.com",
"name": "John Doe",
"avatar": "https://static.platzi.com/media/avatars/john.png"
}
],
"metadata": {
"count": 25,
"pages": 2,
"current_page": 1,
"page_size": 20
}
}

Response Fields:

FieldTypeDescription
data[].company_user_idintCompany user ID
data[].emailstringUser email
data[].namestringUser display name
data[].avatarstringAvatar URL (falls back to the Platzi default avatar if user has none)

Progress, completion, and timestamp fields are not returned by this endpoint.


GET /v3/lp-builder/{company_id}/user-catalog/

List learning-path and course catalog enrollments for one or more users in a company.

Path Parameters:

  • company_id (int) - Company ID

Query Parameters (at least one of user_ids or company_user_ids is required):

ParameterTypeRequiredDescription
user_idsstringNo*Comma-separated auth user IDs
company_user_idsstringNo*Comma-separated company user IDs
enrollment_statestringNoComma-separated enrollment states: active, deleted (default: active)
catalog_statestringNoComma-separated catalog states: draft, active, archived, deleted (default: active)
learning_path_idsstringNoComma-separated learning path IDs
course_idsstringNoComma-separated course IDs
with_learning_pathbooleanNoWhen true, only rows linked to a learning path
has_deadlinebooleanNoWhen true, only rows with a deadline set. Omit to return all rows regardless of deadline
order_bystringNoSort order. One of: id_asc (default), deadline_asc, deadline_desc, created_at_desc
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 100, max: 500)

has_deadline and order_by are opt-in. Omitting them preserves the previous default behaviour (no deadline filter, sorted by id ascending).

Example Request:

GET /v3/lp-builder/123/user-catalog/?user_ids=40,41&with_learning_path=true&page_size=1

Response:

{
"data": [
{
"id": 10,
"company_user_id": 20,
"company_catalog_id": 30,
"user_id": 40,
"state": "active",
"learning_path_id": 100,
"course_id": null,
"catalog_state": "active",
"is_restricted": false,
"deadline": "2026-07-15T23:59:59+00:00",
"catalog_type": "learning_path"
}
],
"metadata": {
"count": 1,
"pages": 1,
"current_page": 1,
"page_size": 1
}
}

Use page_size=1 when you only need to know whether any matching enrollment exists.

Response Fields:

FieldTypeDescription
data[].idintEnrollment ID
data[].company_user_idintCompany user ID
data[].company_catalog_idintCatalog entry ID
data[].user_idintAuth user ID
data[].statestringEnrollment state: active, deleted
data[].learning_path_idint | nullLearning path ID, or null for course/bundle enrollments
data[].course_idint | nullCourse ID, or null for learning-path enrollments
data[].catalog_statestringCatalog entry state: draft, active, archived, deleted
data[].is_restrictedbooleanWhether the catalog entry is restricted to specific teams
data[].deadlinestring | nullCompletion deadline in ISO 8601 UTC format, or null if no deadline is set
data[].catalog_typestring | nullDerived content type: learning_path, course, or bundle (legacy)

Error Cases:

StatusCause
400Missing both user_ids and company_user_ids
400Invalid state name in enrollment_state or catalog_state

GET /v3/lp-builder/{company_id}/objectives/

List all objectives for a company in a single call. Each row groups users who share the same assigned content and deadline, including the content title.

Use this endpoint for company-wide admin views. Use GET .../user-catalog/ when you need raw enrollments for specific users.

Path Parameters:

  • company_id (int) - Company ID

Query Parameters:

ParameterTypeRequiredDescription
enrollment_statestringNoComma-separated enrollment states: active, deleted (default: active)
catalog_statestringNoComma-separated catalog states: draft, active, archived, deleted (default: active)
learning_path_idsstringNoComma-separated learning path IDs
course_idsstringNoComma-separated course IDs
has_deadlinebooleanNoWhen true, only objectives with a deadline set
catalog_typestringNoContent type filter: course or learning_path. Omit or use all for no type filter
searchstringNoCase-insensitive title filter. Whitespace is trimmed; empty applies no filter
order_bystringNoSort order. One of: created_at_desc (default, newest first), deadline_asc, deadline_desc, id_asc
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 100, max: 500). Counts grouped objectives, not individual users

When search is provided, count and pages reflect the filtered result set.

Example Request:

GET /v3/lp-builder/123/objectives/?catalog_type=course&search=ingles&order_by=deadline_asc&page=1&page_size=50

Response:

{
"data": [
{
"company_catalog_id": 1163,
"enrollment_ids": [5385, 5387],
"user_ids": [433609, 433611],
"company_user_ids": [343206, 343208],
"state": "active",
"learning_path_id": 8036,
"course_id": null,
"title": "Ruta de ventas",
"learning_path_info": {
"id": 8036,
"slug": "ruta-de-ventas",
"badge": "https://static.platzi.com/media/achievements/badge-ruta-de-ventas.png",
"courses": [
{
"id": 101,
"title": "Curso de Ventas Básico",
"slug": "ventas-basico",
"badge": "https://static.platzi.com/media/achievements/badge-ventas-basico.png"
},
{
"id": 102,
"title": "Curso de Ventas Avanzado",
"slug": "ventas-avanzado",
"badge": "https://static.platzi.com/media/achievements/badge-ventas-avanzado.png"
}
]
},
"course_info": null,
"catalog_state": "active",
"is_restricted": false,
"deadline": "2026-07-03T00:00:00+00:00",
"catalog_type": "learning_path",
"assigned_count": 2,
"progress": {
"completed": 1,
"total": 2,
"unit": "users"
}
}
],
"metadata": {
"count": 1,
"pages": 1,
"current_page": 1,
"page_size": 50
}
}

Response Fields:

FieldTypeDescription
data[].company_catalog_idintCatalog entry ID for the assigned content
data[].enrollment_idsarray of intEnrollment IDs for PATCH/DELETE on .../user-catalog/{enrollment_id}/
data[].user_idsarray of intAuth user IDs assigned to this objective (parallel to enrollment_ids)
data[].company_user_idsarray of intCompany user IDs assigned to this objective (parallel to enrollment_ids)
data[].assigned_countintNumber of assigned users (len(user_ids))
data[].learning_path_idint | nullLearning path ID when the objective is a learning path
data[].course_idint | nullCourse ID when the objective is a course
data[].titlestring | nullLearning path or course title
data[].learning_path_infoobject | null{id, slug, badge, courses} of the learning path when catalog_type is learning_path, else null. courses is a flat array of the LP's published step courses, each {id, title, slug, badge}
data[].course_infoobject | null{id, slug, badge, thumbnail} of the course when catalog_type is course, else null. badge/thumbnail are null when the course has no image set
data[].statestringEnrollment state shared by grouped users: active, deleted
data[].catalog_statestringCatalog entry state: draft, active, archived, deleted
data[].is_restrictedbooleanWhether the catalog entry is restricted to specific teams
data[].deadlinestring | nullShared completion deadline in ISO 8601 UTC, or null
data[].catalog_typestring | nullContent type: learning_path, course, or bundle (legacy)
data[].progressobject | nullCompletion progress for the objective, or null when it can't be resolved (see below)
data[].progress.completedintAssigned users who have completed the content
data[].progress.totalintAssigned users counted toward the objective (equals assigned_count)
data[].progress.unitstringUnit the counts are expressed in — always "users"

Users with the same content and deadline appear in one row. Array index i refers to the same user across enrollment_ids, user_ids, and company_user_ids.

progress reports how many assigned users have completed the objective — a course counts as completed when it is approved/100%, and a learning path when every step course is completed. total counts all assigned users, so completed may never reach total when some assigned users cannot yet be tracked. When progress cannot be resolved the whole progress object is null for every row (the rest of the listing is unaffected), so treat null as "unknown", not "zero".

Error Cases:

StatusCause
400Invalid state name in enrollment_state or catalog_state
400Invalid catalog_type (not course, learning_path, or all)

GET /v3/lp-builder/{company_id}/objectives/report/

Company-wide summary of objectives: counts by state, plus coverage of groups and students and total completions.

An "objective" is the same grouped unit returned by GET .../objectives/ with default filters (active enrollments on active catalog entries): one row per content + deadline combination.

Path Parameters:

  • company_id (int) - Company ID

Query Parameters: none.

Example Request:

GET /v3/lp-builder/123/objectives/report/

Response:

{
"data": {
"objectives": {
"total": 14,
"completed": 3,
"near_to_expire": 4,
"expired": 4,
"without_date": 2
},
"groups": {
"total": 28,
"without_objective": 15
},
"students": {
"total": 356,
"without_objective": 128
},
"total_completions": 187
}
}

Response Fields:

FieldTypeDescription
data.objectives.totalintAll active objectives (grouped by content + deadline)
data.objectives.completedintObjectives where every assigned user completed the content
data.objectives.near_to_expireintNon-completed objectives whose deadline is within the next 3 days
data.objectives.expiredintNon-completed objectives whose deadline already passed
data.objectives.without_dateintNon-completed objectives with no deadline set
data.groups.totalintActive groups (teams) in the company
data.groups.without_objectiveintActive groups where no active member has an active objective
data.students.totalintActive students in the company
data.students.without_objectiveintActive students with no active objective assigned
data.total_completionsintSum, across all objectives, of assigned users that completed each one

Objective categories are mutually exclusive with precedence completed > without_date > expired > near_to_expire. Objectives in progress with a deadline further than 3 days away only count toward total, so the categories may not add up to it.

total_completions counts completions, not people: a student who completed 3 of their objectives adds 3 to it. Use it as a volume metric, never as a headcount.

groups.without_objective and students.without_objective use the same definition of a member: only active students count, both for the group's members and for the student totals.

Completion follows the same rules as the listing's progress field (a course counts when it is approved or at 100%; a learning path when every one of its courses is completed). Unlike the listing, if completion data cannot be resolved this endpoint returns an error instead of degraded numbers.

Size limit: the report is computed on demand over the whole company, so it is only served for companies with at most 2,000 objectives and 250,000 assigned enrollments (one enrollment per user and objective). A company above either limit gets 413 and cannot use this endpoint — build the equivalent numbers from the paginated GET .../objectives/ instead. The response is also capped at 20 seconds; above that you get 504 and can retry.

Error Cases:

StatusCause
413Company exceeds the size limit above (objectives_report_too_large)
504The report ran out of its 20s budget (objectives_report_timeout), retryable
500Completion data could not be resolved

The enrollment_ids filter of the objective endpoints

The three endpoints below (.../objectives/courses/{course_id}/, .../objectives/learning-paths/{learning_path_id}/ and .../objectives/{company_catalog_id}/students/) share the same optional enrollment_ids query param, because they all address the same grouped unit as GET .../objectives/:

ValueSelects
omittedEvery enrollment of that content, aggregated. due_date/status use the earliest deadline
comma-separated idsOnly those enrollments

Copy the enrollment_ids array straight from the listing row you are drilling into and send it comma-separated. Those ids are the only unique handle a row has: the listing groups by content and deadline, so the group itself is not a stored record. Ids belonging to another company or to another catalog entry simply do not match, and stale ids (students unassigned since the listing was fetched) drop out of the aggregate.

Omitting them is the right default for a content-level view; sending them is what lets you open one specific row when the same content was assigned twice with different dates.

Up to 1000 ids are accepted. Beyond that the request line no longer fits in the 10 KB API Gateway allows, so the endpoint answers 400 instead of letting the edge reject the call; omit the param to aggregate the whole content instead.


GET /v3/lp-builder/{company_id}/objectives/courses/{course_id}/

Detail of a course objective: what it is, when it is due and which groups it was assigned to. Progress is not part of this payload; it has its own source of truth.

Path Parameters:

  • company_id (int) - Company ID
  • course_id (int) - Course ID assigned to the students

Query Parameters:

ParameterTypeRequiredDescription
enrollment_idsstringNoSee "The enrollment_ids filter" above

Example Request:

GET /v3/lp-builder/123/objectives/courses/3651/?enrollment_ids=730118,730119

Response:

{
"data": {
"id": 1163,
"type": "course",
"title": "Curso de SQL y MySQL",
"thumbnail_url": "https://static.platzi.com/media/courses_thumb/sql-mysql.png",
"status": "at_risk",
"due_date": {
"date": "2026-07-30",
"days_remaining": 3,
"is_overdue": false
},
"assigned_groups": {
"total_students": 92,
"groups": [
{ "id": 17, "name": "Research", "student_count": 40 },
{ "id": 22, "name": "Ventas", "student_count": 35 }
]
}
}
}

Response Fields:

FieldTypeDescription
data.idintcompany_catalog_id, the id the students endpoint takes
data.typestringAlways course on this endpoint
data.titlestring | nullCourse title
data.thumbnail_urlstring | nullCourse thumbnail, falling back to its badge
data.statusstringoverdue, at_risk, on_track or no_deadline
data.due_dateobject | nullnull when the objective has no deadline
data.due_date.datestringDeadline as YYYY-MM-DD (UTC)
data.due_date.days_remainingintWhole days from today, 0 once overdue
data.due_date.is_overduebooleanWhether the deadline already passed
data.assigned_groups.total_studentsintAssigned students, deduplicated
data.assigned_groups.groups[]array{id, name, student_count} per group, most students first

status comes from the deadline alone: overdue, then at_risk (deadline within the next 3 days, the same window as GET .../objectives/report/), then on_track, and no_deadline when there is no date. There is no completed state here — completion is progress, and this endpoint does not read progress; recompose it from whichever source you use for progress.

assigned_groups counts groups as-is: a student in two groups counts in both, so student_count can add up to more than total_students, and students without a group are not represented by any row.

Error Cases:

StatusCause
400enrollment_ids are not integers, or more than 1000 were sent
404The company has no catalog entry for that course, or no active enrollment matches the filter
422Invalid company_id or course_id

GET /v3/lp-builder/{company_id}/objectives/learning-paths/{learning_path_id}/

Detail of a learning path objective. Same envelope as the course detail, but a learning path lists its courses instead of its assigned groups.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID assigned to the students

Query Parameters:

ParameterTypeRequiredDescription
enrollment_idsstringNoSee "The enrollment_ids filter" above

Example Request:

GET /v3/lp-builder/123/objectives/learning-paths/8036/?enrollment_ids=730118,730119

Response:

{
"data": {
"id": 1170,
"type": "learning_path",
"title": "Ruta de ventas",
"thumbnail_url": "https://static.platzi.com/media/achievements/badge-ruta-de-ventas.png",
"status": "on_track",
"due_date": {
"date": "2026-09-15",
"days_remaining": 50,
"is_overdue": false
},
"courses": [
{
"id": 101,
"title": "Curso de Ventas Básico",
"badge": "https://static.platzi.com/media/achievements/badge-ventas-basico.png"
},
{
"id": 102,
"title": "Curso de Ventas Avanzado",
"badge": null
}
]
}
}

Response Fields:

FieldTypeDescription
data.idintcompany_catalog_id, the id the students endpoint takes
data.typestringAlways learning_path on this endpoint
data.titlestring | nullLearning path title
data.thumbnail_urlstring | nullLearning path badge (an LP has no separate thumbnail)
data.statusstringSame values and rules as the course detail
data.due_dateobject | nullSame shape as the course detail
data.courses[]arrayThe LP's published step courses, in LP order
data.courses[].idintCourse ID
data.courses[].titlestring | nullCourse title
data.courses[].badgestring | nullCourse badge, null when the course has no image

assigned_groups is intentionally absent here: the course list replaces it for learning paths. Progress is absent from both details, per course and per objective, and so is any funnel — it all has its own source of truth. Use the students endpoint to know who is assigned.

Error Cases:

StatusCause
400enrollment_ids are not integers, or more than 1000 were sent
404The company has no catalog entry for that learning path, or no active enrollment matches the filter
422Invalid company_id or learning_path_id

GET /v3/lp-builder/{company_id}/objectives/{company_catalog_id}/students/

Students assigned to an objective, with their identity and their groups. Works the same for a course and for a learning path objective. Like the details, it carries no progress.

Path Parameters:

  • company_id (int) - Company ID
  • company_catalog_id (int) - data.id of either objective detail, or data[].company_catalog_id of the listing

Query Parameters:

ParameterTypeRequiredDescription
enrollment_idsstringNoSee "The enrollment_ids filter" above
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 100, max: 100)

Example Request:

GET /v3/lp-builder/123/objectives/1163/students/?enrollment_ids=730118,730119&page=1&page_size=50

Response:

{
"data": [
{
"id": 15042,
"user_id": 88213,
"enrollment_id": 730118,
"name": "Ava White",
"email": "ava.white@company.com",
"avatar_url": "https://static.platzi.com/media/users/avatar-ava.png",
"groups": [{ "id": 17, "name": "Research" }]
}
],
"metadata": {
"count": 92,
"pages": 2,
"current_page": 1,
"page_size": 50
}
}

Response Fields:

FieldTypeDescription
data[].idintCompany user ID, same id as the listing's company_user_ids
data[].user_idint | nullPlatzi user ID, for any correlation with the rest of the platform
data[].enrollment_idintEnrollment ID, what POST .../user-catalog/bulk-delete/ takes
data[].namestring | nullStudent name
data[].emailstring | nullStudent email
data[].avatar_urlstringAvatar, falling back to Platzi's default avatar
data[].groups[]array{id, name} of the student's active groups, alphabetically; [] when none

Progress per student (percentage, last activity, whether they finished) is not part of this payload: correlate user_id with the source you use for progress.

metadata.count matches the total_students of the objective detail for the same enrollment_ids, so both views of one objective always agree.

page_size is capped at 100 to keep one response bounded.

Error Cases:

StatusCause
400enrollment_ids are not integers, or more than 1000 were sent
404The catalog entry does not exist or belongs to another company
422Invalid company_id, company_catalog_id, page or page_size

POST /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/users/assign/

Assign a learning path to users or groups, with an optional completion deadline.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID

Request Body (at least one of group_ids or company_user_ids required):

{
"group_ids": [456, 457],
"company_user_ids": [789, 790],
"deadline": "2026-07-15T23:59:59Z"
}
FieldTypeRequiredDescription
group_idsarray of intNo*Group IDs whose members receive the assignment
company_user_idsarray of intNo*Direct company user IDs to assign
deadlinestring (ISO 8601 UTC)NoOptional completion deadline. Omit or null for no deadline

Behavior:

  • Assigns the learning path to every user in company_user_ids plus every member of each group in group_ids.
  • If the learning path is public and not yet in the company's catalog, it is added automatically before assigning.
  • If a user was previously removed, the enrollment is reactivated and the deadline updated.
  • If a user is already assigned, the deadline is updated to the new value.

Response:

{
"data": {
"assigned": true
}
}

Error Cases:

StatusCause
400Learning path {id} not found
400Learning path does not exist
400Missing company_user_ids or group_ids
400Some users do not exist: {ids}

POST /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/users/remove/

Remove a learning path from users or groups.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID

Request Body (at least one of group_ids or company_user_ids required):

{
"group_ids": [456],
"company_user_ids": [789, 790]
}

Behavior:

  • Resolves users the same way as the assign endpoint.
  • Removes the learning path from each matching user. Past progress and history are preserved for reporting.

Response:

{
"data": {
"removed": true
}
}

Error Cases:

StatusCause
400Learning path does not exist
400Missing company_user_ids or group_ids
400Some users do not exist: {ids}

POST /v3/lp-builder/{company_id}/courses/{course_id}/users/assign/

Assign a course directly to users or groups, with an optional completion deadline.

Path Parameters:

  • company_id (int) - Company ID
  • course_id (int) - Course ID

Request Body (at least one of group_ids or company_user_ids required):

{
"group_ids": [456],
"company_user_ids": [789, 790],
"deadline": "2026-07-01T23:59:59Z"
}
FieldTypeRequiredDescription
group_idsarray of intNo*Group IDs whose members receive the assignment
company_user_idsarray of intNo*Direct company user IDs to assign
deadlinestring (ISO 8601 UTC)NoOptional completion deadline. Omit or null for no deadline

Behavior:

  • The course must be either a public Platzi course or already in the company's private catalog. Sending a private course that belongs to another company returns 400.
  • If the course has no catalog entry for this company yet, one is created automatically.
  • Re-assigning a previously removed enrollment reactivates it and updates the deadline.
  • If a user is already assigned, the deadline is updated to the new value.

Response:

{
"data": {
"assigned": true
}
}

Error Cases:

StatusCause
400Some courses are not available for this company: [ids]
400Missing company_user_ids or group_ids
400Some users do not exist: {ids}

POST /v3/lp-builder/{company_id}/courses/{course_id}/users/remove/

Remove a course assignment from users or groups.

Path Parameters:

  • company_id (int) - Company ID
  • course_id (int) - Course ID

Request Body (at least one of group_ids or company_user_ids required):

{
"group_ids": [456],
"company_user_ids": [789, 790]
}

Behavior:

  • Soft-deletes the enrollment records. Past progress and history are preserved.

Response:

{
"data": {
"removed": true
}
}

Error Cases:

StatusCause
400Course {id} not found in company catalog
400Missing company_user_ids or group_ids
400Some users do not exist: {ids}

PUT /v3/lp-builder/{company_id}/courses/{course_id}/users/assign/

Edit an existing course objective: replace its students and set its deadline in one call.

An objective is a group of enrollments that share a content and a deadline, not a single record, so enrollment_ids is what names the one being edited. Take them from the enrollment_ids of GET .../objectives/.

The two halves of the body answer different questions, and mixing them up silently under-edits the objective:

  • enrollment_ids is the before state: the enrollments the objective had when it was loaded, copied verbatim from the listing row. It says which assignment to rewrite, not who to keep. Never trim it to the students that survive the edit.
  • company_user_ids and group_ids are the after state: they say who ends up assigned. New students have no enrollment yet, so they can only be named here.

Path Parameters:

  • company_id (int) - Company ID
  • course_id (int) - Course ID

Request Body:

{
"enrollment_ids": [730118, 730119, 730120],
"group_ids": [456],
"company_user_ids": [789, 790],
"deadline": "2026-12-31T23:59:59Z"
}
FieldTypeRequiredDescription
enrollment_idsarray of intYesEnrollments the objective had when it was loaded, verbatim from the listing row
group_idsarray of intNo*Group IDs whose members make up the final set of students
company_user_idsarray of intNo*Company user IDs that make up the final set of students
deadlinestring (ISO 8601 UTC) or nullYesNew deadline for every student of the objective. null clears it

* At least one of group_ids or company_user_ids is required: the final set of students cannot be empty. Sending neither is rejected as a validation error before anything is written. To leave nobody assigned, delete the enrollments instead.

Behavior:

  • group_ids and company_user_ids describe the final set of students, not a delta. Groups are expanded to their members and merged with the direct IDs.
  • A student already in the objective keeps the same enrollment. Selecting them again, whether directly or through a group, changes nothing unless the deadline changed.
  • A student not yet assigned gets a new enrollment with the objective's deadline. A previously removed one is reactivated.
  • A student left out of the final set has their enrollment soft-deleted, so the objective no longer appears in their catalog. Progress and history are preserved.
  • Send the complete enrollment_ids of the objective. Enrollments left out of the list are invisible to the edit and stay assigned.
  • The ids are validated as a whole: if any of them is no longer an active enrollment of this objective, because another admin edited it in the meantime, the call fails with 400 and nothing is written. Reload the objective and retry.
  • A student can hold only one active enrollment per content. Adding a student who is already assigned to the same course under a different deadline moves their enrollment into this objective; those IDs come back in moved_enrollment_ids, and the objective they left disappears if it runs out of students.
  • Setting a deadline that another objective of the same course already uses merges both into a single objective.

Response:

{
"data": {
"updated": true,
"company_catalog_id": 30,
"deadline": "2026-12-31T23:59:59+00:00",
"enrollment_ids": [730118, 730125],
"added_company_user_ids": [790],
"removed_enrollment_ids": [730119],
"moved_enrollment_ids": []
}
}
FieldTypeDescription
enrollment_idsarray of intEnrollments of the objective after the edit. Use these to address it next time
added_company_user_idsarray of intStudents that were not assigned to the content before
removed_enrollment_idsarray of intEnrollments that were soft-deleted
moved_enrollment_idsarray of intEnrollments pulled in from another objective of the same content
deadlinestring or nullDeadline now shared by every student of the objective

Error Cases:

StatusCause
400Some enrollment_ids do not belong to this objective: [ids]
400Some users do not exist: {ids}
404Course {id} not found in company catalog
422enrollment_ids empty, duplicated, or deadline missing
422Neither group_ids nor company_user_ids sent, or both empty

PUT /v3/lp-builder/{company_id}/learning-paths/{learning_path_id}/users/assign/

Edit an existing learning path objective. Same request body, response and behavior as the course endpoint above, addressing the content by learning path instead.

Path Parameters:

  • company_id (int) - Company ID
  • learning_path_id (int) - Learning path ID

Error Cases:

StatusCause
400Some enrollment_ids do not belong to this objective: [ids]
400Some users do not exist: {ids}
404Learning path {id} not found in company catalog
422enrollment_ids empty, duplicated, or deadline missing
422Neither group_ids nor company_user_ids sent, or both empty

PATCH /v3/lp-builder/{company_id}/user-catalog/{enrollment_id}/

Update the deadline of an existing enrollment (learning path or course).

Path Parameters:

  • company_id (int) - Company ID
  • enrollment_id (int) - Enrollment ID (the id field returned by GET .../user-catalog/)

Request Body:

{
"deadline": "2026-08-01T00:00:00Z"
}
FieldTypeRequiredDescription
deadlinestring (ISO 8601 UTC) or nullYesNew deadline. Pass null to remove the deadline

Response:

{
"data": {
"id": 12345,
"company_user_id": 789,
"company_catalog_id": 30,
"state": 0,
"deadline": "2026-08-01T00:00:00+00:00",
"updated_at": "2026-06-22T15:00:00+00:00"
}
}

Error Cases:

StatusCause
404Enrollment not found, already deleted, or belongs to a different company

DELETE /v3/lp-builder/{company_id}/user-catalog/{enrollment_id}/

Soft-delete an enrollment. The enrollment record is preserved for reporting but the user loses active access.

Path Parameters:

  • company_id (int) - Company ID
  • enrollment_id (int) - Enrollment ID

Response:

{
"data": {
"deleted": true
}
}

Error Cases:

StatusCause
404Enrollment not found, already deleted, or belongs to a different company

POST /v3/lp-builder/{company_id}/user-catalog/bulk-delete/

Soft-delete multiple enrollments in one call, identified by enrollment ID.

Use enrollment IDs from GET .../objectives/ (enrollment_ids[]) or GET .../user-catalog/ (id). IDs that are already deleted, not found, or belong to another company are skipped without failing the batch.

Path Parameters:

  • company_id (int) - Company ID

Request Body:

{
"enrollment_ids": [5385, 5387, 5386]
}
FieldTypeRequiredDescription
enrollment_idsarray of intYesEnrollment IDs to delete (min: 1, max: 500). Must be unique

Response:

{
"data": {
"deleted_count": 2,
"deleted_enrollment_ids": [5385, 5387],
"not_found_enrollment_ids": [],
"already_deleted_enrollment_ids": [5386]
}
}

Response Fields:

FieldTypeDescription
data.deleted_countintEnrollments soft-deleted in this call
data.deleted_enrollment_idsarray of intIDs successfully deleted
data.not_found_enrollment_idsarray of intIDs that do not exist or do not belong to the company
data.already_deleted_enrollment_idsarray of intIDs that were already deleted

Error Cases:

StatusCause
400enrollment_ids empty or missing
422Duplicate IDs in enrollment_ids

Learning Path States

Possible values of the state field on a learning path:

StateDescription
draftWork in progress
activeReady for use
archivedNo longer active but preserved
deletedRemoved from active use

The list endpoint filters out deleted entries by default; pass ?state=deleted to include them.


Step Levels

Possible values of steps[].level in the detail endpoint:

LevelDescription
basicFundamental concepts
intermediateBuilding on basics
advancedComplex topics
optionalOptional / supplementary
customCompany-defined step
defaultNo specific level assigned

Error Handling

Common Error Scenarios

ErrorStatusMessage
LP not found in catalog400Learning Path {id} not found. / Learning path does not exist
LP not public / not found400Learning path {id} does not exist or is not public
Already in catalog400Learning path {id} already exists in company catalog
Update on public LP400Cannot update public learning paths
Invalid course IDs400Some courses are not available for this company: [ids]
Duplicate title400Learning path title already exists
Invalid state400Invalid state: {state}
Missing assignment target400Missing company_user_ids or group_ids
Unknown user IDs400Some users do not exist: {ids}
Unknown company400Company with id {company_id} does not exist
Course unavailable400Some courses are not available for this company: [ids]
Course not in catalog400Course {id} not found in company catalog
Enrollment not found404Enrollment does not exist, is deleted, or belongs to another company

Best Practices

Learning Path Design

Structure:

  • 3-5 steps per learning path (optimal)
  • 2-8 courses per step
  • Clear progression from basic to advanced
  • Consistent difficulty levels within steps

Naming:

  • Descriptive titles: "Backend Development with Python"
  • Clear step names: "Fundamentals", "Intermediate", "Advanced"
  • Avoid generic names: "Path 1", "Step A"

Course Selection

Best Practices:

  • Ensure private courses are in the company catalog before adding them to a step
  • Verify course availability before assignment
  • Consider course difficulty progression

Assignment Strategy

Recommendations:

  • Assign to groups for team-based learning
  • Use direct user assignment for personalized paths
  • Monitor completion rates regularly

Troubleshooting

Assignment Failures

Symptoms: Users not receiving learning path assignments

Resolution:

  • Verify users belong to the company (Some users do not exist indicates foreign IDs)
  • Check that the learning path is not in deleted state
  • For custom learning paths, make sure they have been created in the company catalog before assigning
  • Verify group memberships if assigning via group_ids