Migrating from Learner Progress API v2
The legacy Learner Progress API v2 is replaced by three new GraphQL endpoints, each focused on a specific type of progress data.
Endpoint mapping
| Legacy REST Endpoint | GraphQL Replacement |
|---|---|
GET /api/v2/plans/:planId/users | Learning Plan Progress API — per-user subscription status, steps completed, activity |
GET /api/v2/plans/:planId/progress | Program Progress API — enrollment status, completion %, projects completed, learner hours |
GET /api/v2/plans/:planId/assessment-attempts | Assessment Progress API — individual attempt records with status and results |
Authentication change
| REST (v2) | GraphQL | |
|---|---|---|
| Header | Authorization: Bearer <token> | Authorization: Token <api_key> |
| Scope | Plan-scoped (one token per plan) | COMPANY:<companyId> (one token covers all plans) |
Field mapping — Program Progress
The REST /progress response maps to the ProgramProgressRecord type:
| REST field | GraphQL field | Notes |
|---|---|---|
learner_email | userEmail | camelCase |
program_key | programKey | camelCase |
program_version | programVersion | Now returns String! (e.g., "1.0.0") |
stats.status | enrollmentStatus | Enum: ENROLLED, UNENROLLED, GRADUATED, STATIC_ACCESS |
stats.total_projects | projectsRequiredTotal | Renamed for clarity |
stats.total_projects_completed | projectsRequiredCompleted | Renamed for clarity |
stats.perc_concepts_completed | completionPercentage | 0–100 float |
last_seen | lastActiveAt | RFC 3339 timestamp |
enrolled_at | enrolledAt | RFC 3339 timestamp |
graduated_at | graduatedAt | Nullable — only set when graduated |
stats.assessment_attempted | — | Now in Assessment Progress API (separate endpoint) |
stats.assessment_completed | — | Now in Assessment Progress API (separate endpoint) |
Pagination change
The REST API used offset-based pagination. The GraphQL API uses cursor-based pagination, which is more reliable for changing datasets (no skipped or duplicated records):
| REST | GraphQL | |
|---|---|---|
| Page size | ?per_page=100 | first: 100 |
| Next page | ?page=2 | after: "endCursor-value" |
| Check for more | Compare page count to total | pageInfo.hasNextPage |
See the Pagination guide for full examples.
Side-by-side example
Before (REST):
curl -H "Authorization: Bearer $TOKEN" \
"https://api.udacity.com/api/v2/plans/fd6f0490-2072-43b0-b1d0-7ee82fe96df2/progress?page=1&per_page=50"{
"results": [
{
"learner_email": "j.martinez@acmecorp.com",
"program_key": "nd00113",
"program_version": "1.0.0",
"stats": {
"status": "enrolled",
"total_projects": 5,
"total_projects_completed": 3,
"perc_concepts_completed": 65.0
},
"last_seen": "2026-03-10T14:30:00Z",
"enrolled_at": "2025-11-01T09:00:00Z"
}
],
"total": 342,
"page": 1,
"per_page": 50
}After (GraphQL):
curl -X POST https://api.udacity.com/api/public/api/v1/program-progress/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Token $API_KEY" \
-d '{
"query": "query($input: ProgramProgressInput!) { programProgress(input: $input) { totalCount pageInfo { hasNextPage endCursor } edges { node { userEmail programKey programVersion enrollmentStatus projectsRequiredTotal projectsRequiredCompleted completionPercentage lastActiveAt enrolledAt } } } }",
"variables": { "input": { "first": 50 } }
}'{
"data": {
"programProgress": {
"totalCount": 342,
"pageInfo": {
"hasNextPage": true,
"endCursor": "eyJpZCI6IjUwIn0="
},
"edges": [
{
"node": {
"userEmail": "j.martinez@acmecorp.com",
"programKey": "nd00113",
"programVersion": "1.0.0",
"enrollmentStatus": "ENROLLED",
"projectsRequiredTotal": 5,
"projectsRequiredCompleted": 3,
"completionPercentage": 65.0,
"lastActiveAt": "2026-03-10T14:30:00Z",
"enrolledAt": "2025-11-01T09:00:00Z"
}
}
]
}
}
}Key differences
- Company-scoped, not plan-scoped tokens — a single
COMPANYtoken covers all plans and programs - Separate endpoints by data type — program enrollment data, assessment attempts, and learning plan progress are each in their own focused API
- Cursor-based pagination instead of page numbers (see Pagination)
- Enum values are UPPER_CASE — e.g.,
ENROLLEDnotenrolled - camelCase field names — e.g.,
userEmailnotlearner_email - Flexible field selection — request only the data you need