Integration Examples

Complete, runnable scripts you can use as starting points for integrating with the Udacity Public API.

Python

Fetch the full catalog

Fetch all programs, assessments, and learning plans from the Catalog API. No pagination required.

Python
import os
import requests

API_KEY = os.environ["UDACITY_API_KEY"]
CATALOG_URL = "https://api.udacity.com/api/public/api/v1/catalog/graphql"

query = """
{
  catalog {
    companyId
    programs { key title type duration }
    assessments { id title category }
    learningPlans { id title key }
  }
}
"""

response = requests.post(
    CATALOG_URL,
    json={"query": query},
    headers={
        "Content-Type": "application/json",
        "Authorization": f"Token {API_KEY}",
    },
)
response.raise_for_status()
result = response.json()

if "errors" in result:
    raise Exception(f"GraphQL errors: {result['errors']}")

catalog = result["data"]["catalog"]
print(f"Company: {catalog['companyId']}")
print(f"Programs: {len(catalog['programs'])}")
print(f"Assessments: {len(catalog['assessments'])}")
print(f"Learning Plans: {len(catalog['learningPlans'])}")

for program in catalog["programs"]:
    print(f"  - {program['title']} ({program['key']}, {program['type']})")
Python

Paginate through all program progress

Fetch every program progress record using cursor-based pagination. This pattern works for any paginated endpoint.

Python
import os
import requests
import time
import { getCatalogApiUrl, getProgramProgressApiUrl } from "@/lib/api-urls";

API_KEY = os.environ["UDACITY_API_KEY"]
PROGRESS_URL = "https://api.udacity.com/api/public/api/v1/program-progress/graphql"

QUERY = """
query GetProgress($input: ProgramProgressInput!) {
  programProgress(input: $input) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges {
      node {
        userId
        userEmail
        enrollmentStatus
        programKey
        completionPercentage
        lastActiveAt
      }
    }
  }
}
"""

PAGE_SIZE = 100

def fetch_all_progress():
    all_records = []
    cursor = None

    while True:
        variables = {"input": {"first": PAGE_SIZE}}
        if cursor:
            variables["input"]["after"] = cursor

        response = requests.post(
            PROGRESS_URL,
            json={"query": QUERY, "variables": variables},
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Token {API_KEY}",
            },
        )

        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            print(f"Rate limited — waiting {retry_after}s")
            time.sleep(retry_after)
            continue

        response.raise_for_status()
        result = response.json()

        if "errors" in result:
            raise Exception(f"GraphQL errors: {result['errors']}")

        data = result["data"]["programProgress"]
        records = [edge["node"] for edge in data["edges"]]
        all_records.extend(records)

        print(f"Fetched {len(all_records)} / {data['totalCount']} records")

        if not data["pageInfo"]["hasNextPage"]:
            break

        cursor = data["pageInfo"]["endCursor"]

    return all_records

records = fetch_all_progress()
print(f"\nTotal records: {len(records)}")

enrolled = [r for r in records if r["enrollmentStatus"] == "ENROLLED"]
print(f"Currently enrolled: {len(enrolled)}")
Node.js

Paginate and filter progress

Fetch program progress records filtered to enrolled learners, with pagination. Uses the built-in fetch API (Node.js 18+).

JavaScript
const API_KEY = process.env.UDACITY_API_KEY;
const PROGRESS_URL = "https://api.udacity.com/api/public/api/v1/program-progress/graphql";

const QUERY = `
query GetProgress($input: ProgramProgressInput!) {
  programProgress(input: $input) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges {
      node {
        userId
        userEmail
        enrollmentStatus
        programKey
        completionPercentage
        lastActiveAt
      }
    }
  }
}
`;

async function fetchPage(cursor) {
  const variables = {
    input: {
      filter: { enrollmentStatus: ["ENROLLED"] },
      orderBy: "LAST_ACTIVE",
      order: "DESC",
      first: 100,
      ...(cursor ? { after: cursor } : {}),
    },
  };

  const response = await fetch(PROGRESS_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Token ${API_KEY}`,
    },
    body: JSON.stringify({ query: QUERY, variables }),
  });

  if (response.status === 429) {
    const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
    console.log(`Rate limited — waiting ${retryAfter}s`);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return fetchPage(cursor);
  }

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  }

  const result = await response.json();
  if (result.errors) {
    throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
  }

  return result.data.programProgress;
}

async function fetchAllEnrolled() {
  const allRecords = [];
  let cursor = null;

  do {
    const data = await fetchPage(cursor);
    const records = data.edges.map((e) => e.node);
    allRecords.push(...records);
    console.log(`Fetched ${allRecords.length} / ${data.totalCount}`);
    cursor = data.pageInfo.hasNextPage ? data.pageInfo.endCursor : null;
  } while (cursor);

  return allRecords;
}

const records = await fetchAllEnrolled();
console.log(`\nTotal enrolled: ${records.length}`);
for (const r of records.slice(0, 5)) {
  console.log(`  ${r.userEmail} — ${r.programKey} — ${r.completionPercentage}%`);
}
Patterns

Key patterns

  • Environment variable for API key — never hardcode credentials
  • Check for errors in the response — GraphQL returns HTTP 200 even when the query fails
  • Handle 429 rate limits — back off using the Retry-After header (see Rate Limits)
  • Cursor-based pagination — loop until hasNextPage is false (see Pagination)