Progress Migration
Base URL: https://api-sls.platzi.com/production-sls-business-domains
Early access. This API is being rolled out with selected partners. Contact your account manager to enable it for your company and get a sandbox environment.
Key features:
- Migrate the full course history of each user: completion percentage, dates, and exam results
- Dry-run mode that validates the whole payload without writing anything
- Per-record error reporting with actionable error codes — one bad record never rejects the batch
- Non-destructive merge: the import never downgrades progress a user already earned on Platzi
- Idempotent at two levels (request and record), safe to retry
Workflow
GET /v1/migrations/{company_id}/catalog/courses— map your external courses toplatzi_course_idPOST /v1/migrations/{company_id}/users/resolve— verify every user exists with an active license- (Optional)
POST .../users/mappingsandPOST .../catalog/mappings— register your previous-LMS ids so records can reference users and courses by your own ids POST /v1/migrations/{company_id}/progress/importswithdry_run: true— validate, fix errors- Same request with
dry_run: false— run the real import - Poll
GET /v1/migrations/{company_id}/progress/imports/{import_id}until the status is terminal GET .../imports/{import_id}/records?status=error— review rejected records
Import statuses: validated (dry run, terminal) · processing → success | partial_success | error
Users must exist with an active license before the import — this API never creates
users. Onboard them first with the Users API or a
Massive Action (invite_users).
External id mappings (optional)
If keeping your previous-LMS ids is easier than exporting emails and Platzi course ids, register the links once and reference everything by your own ids:
POST /v1/migrations/{company_id}/users/mappings
{ "mappings": [ { "external_user_id": "EMP-001", "email": "ana@empresa.com" } ] }
POST /v1/migrations/{company_id}/catalog/mappings
{ "mappings": [ { "external_course_id": 9101, "platzi_course_id": 1620 } ] }
- External ids are flexible: numbers and strings are equivalent (
9101and"9101"are the same id), max 128 characters. - Upsert semantics: re-sending an external id overwrites its link.
GETon the same paths lists what is currently registered. - After registering, records can use
user.external_user_idinstead ofuser.email, andcontent.external_course_idinstead ofcontent.platzi_course_id(or both — they must resolve to the same entity).
The migration record
One record = the full history of one (user, course) pair. All timestamps are ISO 8601 in UTC.
{
"external_record_id": "lms-rec-84921",
"user": { "email": "ana@empresa.com" },
"content": { "type": "course", "platzi_course_id": 2599, "external_ref": "JS-101" },
"progress": {
"completion_pct": 87.5,
"started_at": "2024-03-01T00:00:00Z",
"last_activity_at": "2024-06-15T10:30:00Z",
"completed_at": null,
"classes": [
{ "platzi_class_id": 259901, "viewed_at": "2024-03-02T00:00:00Z" },
{ "platzi_class_id": 259902, "viewed_at": "2024-03-05T00:00:00Z" }
]
},
"assessment": { "status": "passed", "score_pct": 92, "taken_at": "2024-06-20T00:00:00Z" }
}
| Field | Required | Description |
|---|---|---|
external_record_id | Yes | 1–128 chars, unique per company. Per-record idempotency key: re-sending it never duplicates progress. |
user.email | Conditional | Must resolve to an active user in the company. Required unless user.external_user_id is sent. |
user.external_user_id | Conditional | Your previous-LMS user id (number or string, max 128 chars), registered beforehand with POST .../users/mappings. If both identifiers are sent, they must resolve to the same user. |
content.type | Yes | Only course in v1. Learning-path progress is recalculated by Platzi from course records — never imported directly. |
content.platzi_course_id | Conditional | Course id from the catalog endpoint. Required unless content.external_course_id is sent. |
content.external_course_id | Conditional | Your previous-LMS course id (number or string, max 128 chars), registered beforehand with POST .../catalog/mappings. If both identifiers are sent, they must resolve to the same course. |
content.external_ref | No | Free-form reference, stored for traceability only (never resolved). |
progress.completion_pct | Conditional | 0–100. Required unless assessment.status is passed (completion is assumed to be 100) or progress.classes is sent (derived from the class count). |
progress.classes | No | Class-level detail: array of { platzi_class_id, viewed_at? }. Marks those classes as viewed in Platzi. Use it when the course structure matches (e.g. the company mirrored the exact same course); class ids come from the catalog endpoint. |
progress.*_at dates | No | Missing dates default to the import date. |
assessment.status | No | passed | failed | not_taken. Declared by your system — Platzi does not re-evaluate. |
assessment.score_pct | No | 0–100, informational. |
assessment.taken_at | No | Used as the certification date when passed. |
Business rules
| Situation | Rule |
|---|---|
Record with progress.classes | Each listed class is marked as viewed (with its original viewed_at date). Classes that don't belong to the course are skipped with a warning; duplicates are ignored. Without class detail, progress is stored at course level only — no synthetic class views are fabricated. |
classes sent without completion_pct | Valid — the percentage is derived from the class count (warning completion_pct_derived). If both are sent and the class detail implies a higher percentage, the higher value is kept (warning completion_pct_adjusted). |
Course with exam + assessment.status: "passed" | The course is marked certified (approved_course = 1, certification date = taken_at). |
| Course with exam, exam not passed | Certification stays pending; only the completion percentage applies. |
| Course without exam | The assessment block is ignored (warning assessment_ignored); the course completes at 100% of classes. |
Exam-only record (passed, no progress) | Valid — completion is assumed 100 (warning completion_pct_assumed). |
| User already has progress on Platzi | Non-destructive merge: the maximum is kept per dimension (warning native_progress_kept). The import never downgrades organic progress. |
| Every migrated record | Persisted with source = "external_migration" and the import_id, so migrated history is always distinguishable and auditable. |
Error codes
Request-level errors use the standard error envelope (errors[].error_code):
invalid_request, batch_too_large (over 500 records / 100 emails), import_not_found.
Record-level errors (records[].errors[].error_code — the record is rejected, the batch continues):
| Code | Meaning |
|---|---|
invalid_record | Structural problem; the message says exactly which field. |
duplicate_external_record_id | The same external_record_id appears twice in one batch. |
user_not_found / user_inactive | Email does not resolve to an active user in the company. |
unsupported_content_type | content.type is not course. |
course_not_in_catalog | platzi_course_id is not in the company catalog. |
external_user_id_not_mapped / external_course_id_not_mapped | The external id was never registered with the mappings endpoints. |
identifier_mismatch | Both identifiers were sent (email + external id, or course id + external id) but they resolve to different entities. |
invalid_completion_pct / invalid_date_format | Value out of range / not ISO 8601. |
missing_progress | No progress block and the passed-exam rule does not apply. |
Record-level warnings (the record is still applied): assessment_ignored,
completion_pct_assumed, completion_pct_derived, completion_pct_adjusted,
class_not_in_course, duplicate_class_id, native_progress_kept.
Limits & idempotency
| Concern | Value |
|---|---|
| Max records per import request | 500 |
| Max emails per resolve request | 100 |
| Max mappings per request | 500 |
| Request idempotency | Idempotency-Key header — retrying returns the original result. |
| Record idempotency | external_record_id — re-sending never duplicates progress. |
API Reference
Complete request/response schemas for each endpoint:
- List catalog courses —
GET /v1/migrations/{company_id}/catalog/courses - Register course mappings —
POST /v1/migrations/{company_id}/catalog/mappings - List course mappings —
GET /v1/migrations/{company_id}/catalog/mappings - Resolve users —
POST /v1/migrations/{company_id}/users/resolve - Register user mappings —
POST /v1/migrations/{company_id}/users/mappings - List user mappings —
GET /v1/migrations/{company_id}/users/mappings - Create progress import —
POST /v1/migrations/{company_id}/progress/imports - Get progress import —
GET /v1/migrations/{company_id}/progress/imports/{import_id} - List import records —
GET /v1/migrations/{company_id}/progress/imports/{import_id}/records