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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | No | Filter by course name (partial match) |
| page | int | No | Page number (default: 1) |
| page_size | int | No | Items per page (default: 20, max 100) |
| private | boolean | No | Filter by private/public courses |
| organization_id | int | No | Filter 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:
| Field | Type | Description |
|---|---|---|
data[].course_id | int | Course ID |
data[].title | string | Course title |
data[].description | string | Course description |
data[].badge | string | null | URL to the course badge image |
data[].thumbnail | string | null | URL to the course thumbnail image |
data[].link | string | Public Platzi URL for the course |
data[].teachers | array | List of teachers, each with a name field (may be null) |
data[].duration_minutes | int | Total course duration in minutes |
data[].level | string | Course difficulty (Spanish): básico, intermedio, avanzado |
data[].ranking | float | null | Average course rating (null if unrated) |
data[].category_name | string | Course category name |
metadata.count | int | Total number of courses matching the filters |
metadata.pages | int | Total number of pages |
metadata.current_page | int | Current page number |
metadata.page_size | int | Items 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | int | No | Page number (default: 1) |
| page_size | int | No | Items 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:
| Field | Type | Description |
|---|---|---|
data[].learning_path_id | int | Learning path ID |
data[].title | string | Learning path title |
data[].badge | string | null | URL to the badge image |
data[].link | string | Public 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 IDlearning_path_id(int) - Public learning path ID
Response (200 OK):
{
"data": {
"added_to_catalog": true
}
}
Error Cases:
| Status | Cause |
|---|---|
| 400 | Learning 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| state | string | No | Filter by catalog state. Omit to exclude deleted entries |
| search | string | No | Case-insensitive filter on the learning path title. Whitespace is trimmed; empty applies no filter |
| page | int | No | Page number (default: 1) |
| page_size | int | No | Items 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:
| Field | Type | Description |
|---|---|---|
data[].learning_path_id | int | Learning path ID |
data[].slug | string | URL slug of the learning path |
data[].title | string | Learning path title |
data[].badge | string | Full badge URL (empty string if not set) |
data[].state | string | Learning path state (see Learning Path States) |
data[].created_by | string | Source: admin, placement_test, skill_matcher |
data[].private | boolean | Whether 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 ofadmin,placement_test,skill_matcher. Default:admin.state(optional): One ofdraft,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:
| Status | Cause |
|---|---|
| 400 | Learning path title already exists |
| 400 | Some courses are not available for this company: [ids] |
| 400 | Company with id {company_id} does not exist |
| 400 | Invalid state: {state} |
| 403 | out_of_scope — a manager listed a group they do not manage (checked before the path is created) |
| 422 | group_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 IDlearning_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:
| Field | Type | Description |
|---|---|---|
learning_path_id | int | Learning path ID |
title | string | Learning path title |
slug | string | URL slug |
description | string | null | Description |
badge | string | Full badge URL |
link | string | Public Platzi URL for the learning path |
private | boolean | Whether this is a private (company-custom) LP |
steps | array | Ordered list of steps |
steps[].title | string | Step title |
steps[].level | string | Step level (see Step Levels) |
steps[].courses | array | Courses in the step, with id, title, order, and additional details |
state | string | Catalog state: draft, active, archived, deleted |
created_by | string | Source: admin, placement_test, skill_matcher |
students_count | int | Number of users actively assigned to this LP in the company |
Error Cases:
| Status | Cause |
|---|---|
| 400 | Learning 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 IDlearning_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
stepsis provided, it replaces all existing steps. Course order is determined by array index. - Course IDs in
steps[].coursesaccept either plain integers or objects with anidfield (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:
| Status | Cause |
|---|---|
| 400 | Learning path not found (not in this company's catalog) |
| 400 | Cannot update public learning paths |
| 400 | Learning path title already exists |
| 400 | Some courses are not available for this company: [ids] |
| 400 | Invalid 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 IDlearning_path_id(int) - Learning path ID
Response:
{
"data": {
"deleted": true
}
}
Error Cases:
| Status | Cause |
|---|---|
| 400 | Learning 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 IDlearning_path_id(int) - Learning path ID
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | int | No | Page number (default: 1) |
| page_size | int | No | Items 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:
| Field | Type | Description |
|---|---|---|
data[].company_user_id | int | Company user ID |
data[].email | string | User email |
data[].name | string | User display name |
data[].avatar | string | Avatar 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):
| Parameter | Type | Required | Description |
|---|---|---|---|
user_ids | string | No* | Comma-separated auth user IDs |
company_user_ids | string | No* | Comma-separated company user IDs |
enrollment_state | string | No | Comma-separated enrollment states: active, deleted (default: active) |
catalog_state | string | No | Comma-separated catalog states: draft, active, archived, deleted (default: active) |
learning_path_ids | string | No | Comma-separated learning path IDs |
course_ids | string | No | Comma-separated course IDs |
with_learning_path | boolean | No | When true, only rows linked to a learning path |
has_deadline | boolean | No | When true, only rows with a deadline set. Omit to return all rows regardless of deadline |
order_by | string | No | Sort order. One of: id_asc (default), deadline_asc, deadline_desc, created_at_desc |
page | int | No | Page number (default: 1) |
page_size | int | No | Items per page (default: 100, max: 500) |
has_deadlineandorder_byare opt-in. Omitting them preserves the previous default behaviour (no deadline filter, sorted byidascending).
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:
| Field | Type | Description |
|---|---|---|
data[].id | int | Enrollment ID |
data[].company_user_id | int | Company user ID |
data[].company_catalog_id | int | Catalog entry ID |
data[].user_id | int | Auth user ID |
data[].state | string | Enrollment state: active, deleted |
data[].learning_path_id | int | null | Learning path ID, or null for course/bundle enrollments |
data[].course_id | int | null | Course ID, or null for learning-path enrollments |
data[].catalog_state | string | Catalog entry state: draft, active, archived, deleted |
data[].is_restricted | boolean | Whether the catalog entry is restricted to specific teams |
data[].deadline | string | null | Completion deadline in ISO 8601 UTC format, or null if no deadline is set |
data[].catalog_type | string | null | Derived content type: learning_path, course, or bundle (legacy) |
Error Cases:
| Status | Cause |
|---|---|
| 400 | Missing both user_ids and company_user_ids |
| 400 | Invalid 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
enrollment_state | string | No | Comma-separated enrollment states: active, deleted (default: active) |
catalog_state | string | No | Comma-separated catalog states: draft, active, archived, deleted (default: active) |
learning_path_ids | string | No | Comma-separated learning path IDs |
course_ids | string | No | Comma-separated course IDs |
has_deadline | boolean | No | When true, only objectives with a deadline set |
catalog_type | string | No | Content type filter: course or learning_path. Omit or use all for no type filter |
search | string | No | Case-insensitive title filter. Whitespace is trimmed; empty applies no filter |
order_by | string | No | Sort order. One of: created_at_desc (default, newest first), deadline_asc, deadline_desc, id_asc |
page | int | No | Page number (default: 1) |
page_size | int | No | Items 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:
| Field | Type | Description |
|---|---|---|
data[].company_catalog_id | int | Catalog entry ID for the assigned content |
data[].enrollment_ids | array of int | Enrollment IDs for PATCH/DELETE on .../user-catalog/{enrollment_id}/ |
data[].user_ids | array of int | Auth user IDs assigned to this objective (parallel to enrollment_ids) |
data[].company_user_ids | array of int | Company user IDs assigned to this objective (parallel to enrollment_ids) |
data[].assigned_count | int | Number of assigned users (len(user_ids)) |
data[].learning_path_id | int | null | Learning path ID when the objective is a learning path |
data[].course_id | int | null | Course ID when the objective is a course |
data[].title | string | null | Learning path or course title |
data[].learning_path_info | object | 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_info | object | 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[].state | string | Enrollment state shared by grouped users: active, deleted |
data[].catalog_state | string | Catalog entry state: draft, active, archived, deleted |
data[].is_restricted | boolean | Whether the catalog entry is restricted to specific teams |
data[].deadline | string | null | Shared completion deadline in ISO 8601 UTC, or null |
data[].catalog_type | string | null | Content type: learning_path, course, or bundle (legacy) |
data[].progress | object | null | Completion progress for the objective, or null when it can't be resolved (see below) |
data[].progress.completed | int | Assigned users who have completed the content |
data[].progress.total | int | Assigned users counted toward the objective (equals assigned_count) |
data[].progress.unit | string | Unit 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:
| Status | Cause |
|---|---|
| 400 | Invalid state name in enrollment_state or catalog_state |
| 400 | Invalid 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:
| Field | Type | Description |
|---|---|---|
data.objectives.total | int | All active objectives (grouped by content + deadline) |
data.objectives.completed | int | Objectives where every assigned user completed the content |
data.objectives.near_to_expire | int | Non-completed objectives whose deadline is within the next 3 days |
data.objectives.expired | int | Non-completed objectives whose deadline already passed |
data.objectives.without_date | int | Non-completed objectives with no deadline set |
data.groups.total | int | Active groups (teams) in the company |
data.groups.without_objective | int | Active groups where no active member has an active objective |
data.students.total | int | Active students in the company |
data.students.without_objective | int | Active students with no active objective assigned |
data.total_completions | int | Sum, 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:
| Status | Cause |
|---|---|
| 413 | Company exceeds the size limit above (objectives_report_too_large) |
| 504 | The report ran out of its 20s budget (objectives_report_timeout), retryable |
| 500 | Completion 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/:
| Value | Selects |
|---|---|
| omitted | Every enrollment of that content, aggregated. due_date/status use the earliest deadline |
| comma-separated ids | Only 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 IDcourse_id(int) - Course ID assigned to the students
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
enrollment_ids | string | No | See "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:
| Field | Type | Description |
|---|---|---|
data.id | int | company_catalog_id, the id the students endpoint takes |
data.type | string | Always course on this endpoint |
data.title | string | null | Course title |
data.thumbnail_url | string | null | Course thumbnail, falling back to its badge |
data.status | string | overdue, at_risk, on_track or no_deadline |
data.due_date | object | null | null when the objective has no deadline |
data.due_date.date | string | Deadline as YYYY-MM-DD (UTC) |
data.due_date.days_remaining | int | Whole days from today, 0 once overdue |
data.due_date.is_overdue | boolean | Whether the deadline already passed |
data.assigned_groups.total_students | int | Assigned 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:
| Status | Cause |
|---|---|
| 400 | enrollment_ids are not integers, or more than 1000 were sent |
| 404 | The company has no catalog entry for that course, or no active enrollment matches the filter |
| 422 | Invalid 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 IDlearning_path_id(int) - Learning path ID assigned to the students
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
enrollment_ids | string | No | See "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:
| Field | Type | Description |
|---|---|---|
data.id | int | company_catalog_id, the id the students endpoint takes |
data.type | string | Always learning_path on this endpoint |
data.title | string | null | Learning path title |
data.thumbnail_url | string | null | Learning path badge (an LP has no separate thumbnail) |
data.status | string | Same values and rules as the course detail |
data.due_date | object | null | Same shape as the course detail |
data.courses[] | array | The LP's published step courses, in LP order |
data.courses[].id | int | Course ID |
data.courses[].title | string | null | Course title |
data.courses[].badge | string | null | Course 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:
| Status | Cause |
|---|---|
| 400 | enrollment_ids are not integers, or more than 1000 were sent |
| 404 | The company has no catalog entry for that learning path, or no active enrollment matches the filter |
| 422 | Invalid 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 IDcompany_catalog_id(int) -data.idof either objective detail, ordata[].company_catalog_idof the listing
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
enrollment_ids | string | No | See "The enrollment_ids filter" above |
page | int | No | Page number (default: 1) |
page_size | int | No | Items 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:
| Field | Type | Description |
|---|---|---|
data[].id | int | Company user ID, same id as the listing's company_user_ids |
data[].user_id | int | null | Platzi user ID, for any correlation with the rest of the platform |
data[].enrollment_id | int | Enrollment ID, what POST .../user-catalog/bulk-delete/ takes |
data[].name | string | null | Student name |
data[].email | string | null | Student email |
data[].avatar_url | string | Avatar, 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:
| Status | Cause |
|---|---|
| 400 | enrollment_ids are not integers, or more than 1000 were sent |
| 404 | The catalog entry does not exist or belongs to another company |
| 422 | Invalid 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 IDlearning_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"
}
| Field | Type | Required | Description |
|---|---|---|---|
group_ids | array of int | No* | Group IDs whose members receive the assignment |
company_user_ids | array of int | No* | Direct company user IDs to assign |
deadline | string (ISO 8601 UTC) | No | Optional completion deadline. Omit or null for no deadline |
Behavior:
- Assigns the learning path to every user in
company_user_idsplus every member of each group ingroup_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:
| Status | Cause |
|---|---|
| 400 | Learning path {id} not found |
| 400 | Learning path does not exist |
| 400 | Missing company_user_ids or group_ids |
| 400 | Some 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 IDlearning_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:
| Status | Cause |
|---|---|
| 400 | Learning path does not exist |
| 400 | Missing company_user_ids or group_ids |
| 400 | Some 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 IDcourse_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"
}
| Field | Type | Required | Description |
|---|---|---|---|
group_ids | array of int | No* | Group IDs whose members receive the assignment |
company_user_ids | array of int | No* | Direct company user IDs to assign |
deadline | string (ISO 8601 UTC) | No | Optional 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:
| Status | Cause |
|---|---|
| 400 | Some courses are not available for this company: [ids] |
| 400 | Missing company_user_ids or group_ids |
| 400 | Some 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 IDcourse_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:
| Status | Cause |
|---|---|
| 400 | Course {id} not found in company catalog |
| 400 | Missing company_user_ids or group_ids |
| 400 | Some 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_idsis 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_idsandgroup_idsare 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 IDcourse_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"
}
| Field | Type | Required | Description |
|---|---|---|---|
enrollment_ids | array of int | Yes | Enrollments the objective had when it was loaded, verbatim from the listing row |
group_ids | array of int | No* | Group IDs whose members make up the final set of students |
company_user_ids | array of int | No* | Company user IDs that make up the final set of students |
deadline | string (ISO 8601 UTC) or null | Yes | New 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_idsandcompany_user_idsdescribe 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_idsof 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": []
}
}
| Field | Type | Description |
|---|---|---|
enrollment_ids | array of int | Enrollments of the objective after the edit. Use these to address it next time |
added_company_user_ids | array of int | Students that were not assigned to the content before |
removed_enrollment_ids | array of int | Enrollments that were soft-deleted |
moved_enrollment_ids | array of int | Enrollments pulled in from another objective of the same content |
deadline | string or null | Deadline now shared by every student of the objective |
Error Cases:
| Status | Cause |
|---|---|
| 400 | Some enrollment_ids do not belong to this objective: [ids] |
| 400 | Some users do not exist: {ids} |
| 404 | Course {id} not found in company catalog |
| 422 | enrollment_ids empty, duplicated, or deadline missing |
| 422 | Neither 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 IDlearning_path_id(int) - Learning path ID
Error Cases:
| Status | Cause |
|---|---|
| 400 | Some enrollment_ids do not belong to this objective: [ids] |
| 400 | Some users do not exist: {ids} |
| 404 | Learning path {id} not found in company catalog |
| 422 | enrollment_ids empty, duplicated, or deadline missing |
| 422 | Neither 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 IDenrollment_id(int) - Enrollment ID (theidfield returned byGET .../user-catalog/)
Request Body:
{
"deadline": "2026-08-01T00:00:00Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
deadline | string (ISO 8601 UTC) or null | Yes | New 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:
| Status | Cause |
|---|---|
| 404 | Enrollment 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 IDenrollment_id(int) - Enrollment ID
Response:
{
"data": {
"deleted": true
}
}
Error Cases:
| Status | Cause |
|---|---|
| 404 | Enrollment 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]
}
| Field | Type | Required | Description |
|---|---|---|---|
enrollment_ids | array of int | Yes | Enrollment 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:
| Field | Type | Description |
|---|---|---|
data.deleted_count | int | Enrollments soft-deleted in this call |
data.deleted_enrollment_ids | array of int | IDs successfully deleted |
data.not_found_enrollment_ids | array of int | IDs that do not exist or do not belong to the company |
data.already_deleted_enrollment_ids | array of int | IDs that were already deleted |
Error Cases:
| Status | Cause |
|---|---|
| 400 | enrollment_ids empty or missing |
| 422 | Duplicate IDs in enrollment_ids |
Learning Path States
Possible values of the state field on a learning path:
| State | Description |
|---|---|
| draft | Work in progress |
| active | Ready for use |
| archived | No longer active but preserved |
| deleted | Removed from active use |
The list endpoint filters out
deletedentries by default; pass?state=deletedto include them.
Step Levels
Possible values of steps[].level in the detail endpoint:
| Level | Description |
|---|---|
| basic | Fundamental concepts |
| intermediate | Building on basics |
| advanced | Complex topics |
| optional | Optional / supplementary |
| custom | Company-defined step |
| default | No specific level assigned |
Error Handling
Common Error Scenarios
| Error | Status | Message |
|---|---|---|
| LP not found in catalog | 400 | Learning Path {id} not found. / Learning path does not exist |
| LP not public / not found | 400 | Learning path {id} does not exist or is not public |
| Already in catalog | 400 | Learning path {id} already exists in company catalog |
| Update on public LP | 400 | Cannot update public learning paths |
| Invalid course IDs | 400 | Some courses are not available for this company: [ids] |
| Duplicate title | 400 | Learning path title already exists |
| Invalid state | 400 | Invalid state: {state} |
| Missing assignment target | 400 | Missing company_user_ids or group_ids |
| Unknown user IDs | 400 | Some users do not exist: {ids} |
| Unknown company | 400 | Company with id {company_id} does not exist |
| Course unavailable | 400 | Some courses are not available for this company: [ids] |
| Course not in catalog | 400 | Course {id} not found in company catalog |
| Enrollment not found | 404 | Enrollment 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 existindicates foreign IDs) - Check that the learning path is not in
deletedstate - For custom learning paths, make sure they have been created in the company catalog before assigning
- Verify group memberships if assigning via
group_ids