Using GraphQL

The Udacity Public API uses GraphQL. This guide covers the Udacity-specific patterns you need to know — if you’re already familiar with GraphQL, skip straight to the sections that matter.

Multi-endpoint architecture

Unlike a single-graph API, Udacity exposes a separate GraphQL endpoint per domain. Each endpoint has its own schema and handles one area of data:

EndpointWhat it returns
https://api.udacity.com/api/public/api/v1/catalog/graphqlPrograms, assessments, and learning plans in your company catalog
https://api.udacity.com/api/public/api/v1/program-progress/graphqlEnrollment status and completion progress per learner per program
https://api.udacity.com/api/public/api/v1/assessment-progress/graphqlAssessment attempt results per learner
https://api.udacity.com/api/public/api/v1/learning-plan-progress/graphqlLearner progress through learning plan steps
https://api.udacity.com/api/public/api/v1/tokens/graphqlCreate, revoke, and manage API tokens (JWT auth, not token auth)

In practice this means: to build a full learner dashboard, you make separate requests to the catalog endpoint (what’s available), the program progress endpoint (how learners are doing), and optionally the assessment progress endpoint (assessment results). You cannot query across domains in a single request.

Making requests

Every request is a POST with a JSON body containing your query:

curl -X POST https://api.udacity.com/api/public/api/v1/catalog/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Token YOUR_API_KEY" \
  -d '{
    "query": "query GetCatalog($contractId: ID) { catalog(contractId: $contractId) { companyId programs { key title } } }",
    "variables": { "contractId": "optional-contract-id" }
  }'
FieldTypeRequiredDescription
querystringYesThe GraphQL query or mutation
variablesobjectNoValues for any variables declared in the query
operationNamestringNoWhich operation to run (only needed if the query string contains multiple operations)

The input pattern

All progress queries use a structured input object that combines filtering, sorting, and pagination into a single argument. Once you understand this pattern, every progress endpoint works the same way:

query GetProgramProgress($input: ProgramProgressInput!) {
  programProgress(input: $input) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges {
      node {
        userEmail
        enrollmentStatus
        completionPercentage
      }
    }
  }
}

Variables:

{
  "input": {
    "filter": {
      "enrollmentStatus": ["ENROLLED"],
      "programType": ["NANODEGREE"]
    },
    "orderBy": "LAST_ACTIVE",
    "order": "DESC",
    "first": 50,
    "after": null
  }
}

The input object always supports the same structure:

FieldPurpose
filterObject with domain-specific filter fields (varies by endpoint)
orderByEnum specifying which field to sort by
orderASC or DESC
firstPage size (default 50)
afterCursor from pageInfo.endCursor for the next page

The Catalog API is the exception — it returns the entire catalog in one response with no pagination or filtering needed.

Always use variables

Use variables instead of string interpolation for dynamic values. This prevents injection issues and makes queries reusable:

# Good — use variables
query GetCatalog($contractId: ID) {
  catalog(contractId: $contractId) {
    companyId
    programs { key title }
  }
}

# Pass separately:
# { "contractId": "your-contract-id" }
# Bad — hardcoded values
query {
  catalog(contractId: "abc-123") {
    companyId
  }
}

Select only the fields you need

GraphQL returns exactly the fields you request — no more. Requesting fewer fields reduces response size and can improve performance:

# Lightweight — just keys and titles
{ catalog { programs { key title } } }

# Full detail — includes all metadata
{
  catalog {
    programs {
      key title type summary duration
      modifiedDate syllabusUrl imageUrl
      ssoLaunchLinksByContract { contractId launchUrl }
    }
  }
}

Next steps