Thinkific Has Two APIs. Here's What's in Each One.
Thinkific exposes both a REST API and a GraphQL API. Most exporters use one or the other and lose information. A complete migration uses both — REST for enrollments and custom fields, GraphQL for course structure and quizzes.
Two endpoints, two credentials
REST lives at `https://api.thinkific.com/api/public/v1/` and authenticates with two headers: `X-Auth-API-Key` and `X-Auth-Subdomain`. GraphQL lives at `https://api.thinkific.com/stable/graphql` and authenticates with `Authorization: Bearer <token>`. The keys are not the same — your REST API key is generated under Settings → Code & Analytics → API; your GraphQL access token is generated separately under the same admin area. Postman collections that work against one will not work against the other without re-auth.
What only REST gives you
Custom profile field definitions (`GET /custom_profile_field_definitions`) — the canonical mapping of every custom user field your school has set up. Without this, you'd be guessing at field names from instance data. Enrollment expiration dates (`GET /enrollments?course_id=...`) — `expiry_date`, `activated_at`, `is_free_trial`, and `expired` flags are all REST-only on the enrollment endpoint. Clean paginated user lists (`GET /users?page=...`) with a familiar `meta.pagination.next_page` structure.
What only GraphQL gives you
Full nested course structure in a single query — chapters with lessons with content, no N+1 round trips. Direct PDF URLs via `PdfContent.url` (an S3 link on `s3.amazonaws.com/thinkific-import/<tenant>/...`). Quiz question text, answer choices, and correct-answer flags via `QuizContent.quiz.questions.nodes.choices.nodes.correct`. Separate `firstName` and `lastName` fields on User — REST conflates these into a single space-joined `user_name`, which is unreliable to split (compound surnames, missing first names, etc.).
What's in neither
Unauthenticated direct video URLs. Thinkific's GraphQL `VideoContent` type exposes `videoId` (a Wistia ID), `playUrl` (an authenticated take URL), and `thumbnail` — but not a clean `mp4` field you can hit with `curl`. If you need to migrate the actual video files, plan on either coordinating with Thinkific support, using `videoId` to authenticate against Wistia separately, or — what we did for our Theory Time client — accepting a client-supplied mapping spreadsheet of `lesson_id → new_video_url` and swapping during import.
The join keys you need to know
User identity has two forms in Thinkific's data: REST returns a numeric `user_id` (e.g. `10308360`), GraphQL returns a string `gid` (e.g. `"User-10308360"`). Both encode the same underlying record. We normalize to strings on export and persist the GraphQL `gid` as optional metadata for re-runs. Course IDs are simpler — both APIs use the same numeric ID. Quiz IDs only come through GraphQL.
How we use both in the exporter
Our exporter runs each phase against the API that returns the cleanest data. Phase 1: users via REST (paginated, fast, gives us emails for matching). Phase 2: custom fields via REST (the only place they exist). Phase 3: enrollments via REST per course (gives us expiry dates and free-trial flags). Phase 4: course structure via GraphQL per course (gives us the curriculum tree and PDF URLs). Phase 5: quizzes via GraphQL per quiz ID (gives us the full Q&A). Each phase outputs a separate JSON file. The importer joins them up.