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 EndpointGraphQL Replacement
GET /api/v2/plans/:planId/usersLearning Plan Progress API — per-user subscription status, steps completed, activity
GET /api/v2/plans/:planId/progressProgram Progress API — enrollment status, completion %, projects completed, learner hours
GET /api/v2/plans/:planId/assessment-attemptsAssessment Progress API — individual attempt records with status and results

Authentication change

REST (v2)GraphQL
HeaderAuthorization: Bearer <token>Authorization: Token <api_key>
ScopePlan-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 fieldGraphQL fieldNotes
learner_emailuserEmailcamelCase
program_keyprogramKeycamelCase
program_versionprogramVersionNow returns String! (e.g., "1.0.0")
stats.statusenrollmentStatusEnum: ENROLLED, UNENROLLED, GRADUATED, STATIC_ACCESS
stats.total_projectsprojectsRequiredTotalRenamed for clarity
stats.total_projects_completedprojectsRequiredCompletedRenamed for clarity
stats.perc_concepts_completedcompletionPercentage0–100 float
last_seenlastActiveAtRFC 3339 timestamp
enrolled_atenrolledAtRFC 3339 timestamp
graduated_atgraduatedAtNullable — only set when graduated
stats.assessment_attemptedNow in Assessment Progress API (separate endpoint)
stats.assessment_completedNow 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):

RESTGraphQL
Page size?per_page=100first: 100
Next page?page=2after: "endCursor-value"
Check for moreCompare page count to totalpageInfo.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 singleCOMPANY token 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., ENROLLED not enrolled
  • camelCase field names — e.g., userEmail not learner_email
  • Flexible field selection — request only the data you need