Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Thinkific

Thinkific's REST API uses API key and subdomain authentication — get your API key from Settings → API and add it alongside your school subdomain to your .env file. Prompt Bolt to build an API route that fetches courses and enrollments, display results in a branded student portal, and handle enrollment webhooks after deploying to Netlify or Bolt Cloud. Thinkific offers unlimited courses on its free plan, making it an accessible starting point for course creators building custom learning portals.

What you'll learn

  • How to authenticate with the Thinkific API using API key and subdomain header authentication
  • How to fetch courses, enrollments, and user data from a Thinkific school
  • How to build a custom branded course catalog and student dashboard in Bolt.new
  • How to programmatically enroll students in free courses using the Thinkific API
  • How to handle Thinkific enrollment webhooks for automation and notifications after deployment
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate16 min read25 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

Thinkific's REST API uses API key and subdomain authentication — get your API key from Settings → API and add it alongside your school subdomain to your .env file. Prompt Bolt to build an API route that fetches courses and enrollments, display results in a branded student portal, and handle enrollment webhooks after deploying to Netlify or Bolt Cloud. Thinkific offers unlimited courses on its free plan, making it an accessible starting point for course creators building custom learning portals.

Build a Custom Thinkific Course Portal in Bolt.new

Thinkific is one of the most developer-friendly online course platforms — unlike many competitors, Thinkific provides full API access even on their free plan, making it possible to build sophisticated integrations without a paid subscription. Their REST API covers courses, users, enrollments, bundles, and orders, with straightforward header-based authentication that requires only your API key and school subdomain.

The primary use case for building a Thinkific integration in Bolt.new is a custom course portal. Thinkific's built-in student interface is functional but generic. By building a custom Bolt.new frontend, you control every aspect of the course discovery and enrollment experience: your branding, custom recommendation logic, community features, upsell flows, and analytics. Students visit your custom portal first, browse courses on your terms, and click through to Thinkific for the actual video lesson delivery.

Thinkific's API is particularly well-suited for enrollment automation. Unlike platforms where enrollment only happens through Thinkific's own checkout, Thinkific's API allows programmatic enrollment of users in free courses and courses for which you have already collected payment through an external system. This enables powerful workflows: someone purchases a bundle on your Bolt.new app using Stripe, and your backend automatically enrolls them in the correct Thinkific courses. The student never needs to create a separate Thinkific account — your app handles everything.

Integration method

Bolt Chat + API Route

Thinkific's REST API authenticates using an API key and subdomain in request headers, making it straightforward to integrate with Bolt.new. API routes proxy requests to Thinkific's API endpoints for course listings, user management, and enrollment data. Bolt generates the API routes from a prompt, keeping your credentials server-side. Enrollment event webhooks require a deployed public URL — Bolt's WebContainer cannot receive incoming connections, so deploy to Netlify or Bolt Cloud before testing Thinkific webhook delivery.

Prerequisites

  • A Thinkific account — the free plan includes API access and unlimited courses
  • Thinkific API key from Settings → API in your Thinkific admin dashboard
  • Your Thinkific school subdomain (the part before .thinkific.com in your school URL)
  • A Bolt.new project with Supabase connected for storing user mappings and enrollment data
  • A deployed URL on Netlify or Bolt Cloud for receiving Thinkific enrollment webhooks

Step-by-step guide

1

Get Your Thinkific API Key and Subdomain

Thinkific's API authentication uses two pieces of information: your API key and your school subdomain. Both appear in every API request as request headers (`X-Auth-API-Key` and `X-Auth-Subdomain`), making authentication straightforward compared to OAuth flows. Log into your Thinkific admin at your-school.thinkific.com. Navigate to Settings → API in the left sidebar. If you are on the free plan, API access is available — Thinkific is notable for providing API access across all plans including free. Click 'Generate API Key' to create a new key. Give it a descriptive name like 'Bolt Student Portal' and copy the generated key immediately — Thinkific shows it once. Your subdomain is the prefix of your Thinkific school URL. If your school is at `myschool.thinkific.com`, your subdomain is `myschool`. If you have a custom domain, your subdomain is still the original Thinkific subdomain assigned at signup — check the URL in your Thinkific admin dashboard to find it. Add both values to your .env file. The subdomain is not sensitive (it is visible in any browser visiting your Thinkific school), but the API key must be kept private. Both are needed in the same .env file for your API routes to authenticate successfully. Remember to also add these as environment variables in your deployment platform (Netlify or Bolt Cloud) when you deploy.

.env
1# .env file in your Bolt project root
2# Thinkific API credentials from Settings API in Thinkific admin
3THINKIFIC_API_KEY=your_thinkific_api_key_here
4# Your school's subdomain (the part before .thinkific.com)
5THINKIFIC_SUBDOMAIN=your-school-subdomain
6
7# For webhook verification
8THINKIFIC_WEBHOOK_SECRET=your_webhook_secret_here
9
10# Supabase for data storage
11NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
12NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key

Pro tip: Create a separate API key for your development environment and another for production. Label them clearly in Thinkific's Settings → API page. Revoking a compromised production key does not affect development and vice versa.

Expected result: You have both your Thinkific API key and school subdomain in your .env file. You confirmed API access works by checking the Settings → API page in your Thinkific admin.

2

Build Thinkific API Routes

Thinkific's API follows RESTful conventions with consistent authentication. Every request requires two headers: `X-Auth-API-Key: [your_key]` and `X-Auth-Subdomain: [your_subdomain]`. The base URL is `https://api.thinkific.com/api/public/v1`. The key endpoints for building a student portal are: `GET /courses` (all published courses), `GET /courses/{id}` (single course details), `GET /users` (list users, filterable by email), `GET /enrollments` (list enrollments, filterable by user_id or course_id), and `POST /enrollments` (enroll a user in a course). The enrollment POST endpoint accepts a user_id and course_id — it enrolls the user immediately without requiring payment, making it suitable for free courses or paid courses where payment was collected externally. Bolt's WebContainer supports all these API calls as outbound HTTP requests — you can test course listing, user lookup, and enrollment data during development in the preview. Build at least three API routes: one for the course catalog, one for user lookup by email, and one for enrollment data. Structure them clearly with consistent error handling so the frontend can display friendly messages when the API is unavailable or returns unexpected data. For enrollment creation (POST /enrollments), always validate that the user exists in Thinkific before calling the enrollment endpoint. If the user does not exist, create them first using `POST /users` with their email and name. The enrollment endpoint returns the enrollment object including the direct link to start the course.

Bolt.new Prompt

Build Thinkific API integration with three routes: (1) app/api/thinkific/courses/route.ts — GET all courses from https://api.thinkific.com/api/public/v1/courses using headers X-Auth-API-Key and X-Auth-Subdomain from process.env, return array of {id, name, description, slug, price, isFree, thumbnailUrl, studentCount}, (2) app/api/thinkific/user/route.ts — GET user by email query param from /v1/users?email=[email], return {id, email, firstName, lastName, createdAt}, (3) app/api/thinkific/enroll/route.ts — POST endpoint that accepts {userId, courseId} and calls POST /v1/enrollments to enroll a user. Handle errors with meaningful messages.

Paste this in Bolt.new chat

app/api/thinkific/courses/route.ts
1// app/api/thinkific/courses/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4const THINKIFIC_API_KEY = process.env.THINKIFIC_API_KEY;
5const THINKIFIC_SUBDOMAIN = process.env.THINKIFIC_SUBDOMAIN;
6const THINKIFIC_BASE = 'https://api.thinkific.com/api/public/v1';
7
8const thinkificHeaders = {
9 'X-Auth-API-Key': THINKIFIC_API_KEY!,
10 'X-Auth-Subdomain': THINKIFIC_SUBDOMAIN!,
11 'Content-Type': 'application/json',
12};
13
14export async function GET(request: NextRequest) {
15 if (!THINKIFIC_API_KEY || !THINKIFIC_SUBDOMAIN) {
16 return NextResponse.json({ error: 'Thinkific not configured' }, { status: 500 });
17 }
18
19 const { searchParams } = new URL(request.url);
20 const page = searchParams.get('page') || '1';
21 const limit = searchParams.get('limit') || '25';
22
23 const response = await fetch(
24 `${THINKIFIC_BASE}/courses?page=${page}&limit=${limit}`,
25 { headers: thinkificHeaders }
26 );
27
28 if (!response.ok) {
29 return NextResponse.json(
30 { error: `Thinkific error: ${response.status}` },
31 { status: response.status }
32 );
33 }
34
35 const data = await response.json();
36
37 const courses = (data.items || []).map((c: any) => ({
38 id: c.id,
39 name: c.name,
40 description: c.description,
41 slug: c.slug,
42 price: c.price,
43 isFree: c.is_free,
44 thumbnailUrl: c.course_card_image_url,
45 studentCount: c.students_count,
46 courseUrl: `https://${THINKIFIC_SUBDOMAIN}.thinkific.com/courses/${c.slug}`,
47 }));
48
49 return NextResponse.json({
50 courses,
51 pagination: data.meta?.pagination,
52 });
53}

Pro tip: Thinkific's API response wraps results in an 'items' array with pagination metadata in 'meta.pagination'. Always check for data.items rather than data.courses — Thinkific uses 'items' consistently across all list endpoints.

Expected result: The /api/thinkific/courses endpoint returns your Thinkific course catalog. The /api/thinkific/user and /api/thinkific/enroll endpoints handle user lookup and programmatic enrollment.

3

Build the Course Catalog and Enrollment Flow

With API routes returning Thinkific data, build the user-facing course catalog. The catalog page fetches all published courses from `/api/thinkific/courses` and renders them as cards. Each card shows the course name, description excerpt, price (or 'Free'), thumbnail, and an enrollment or checkout button. For free courses, the enrollment flow is: user clicks Enroll → authenticate with Supabase → look up or create their Thinkific user via `/api/thinkific/user` → call `/api/thinkific/enroll` to programmatically enroll them → show success with a link to the course on Thinkific. This all happens within your Bolt app without any Thinkific pages involved. For paid courses, redirect to Thinkific's checkout page: `https://[subdomain].thinkific.com/courses/{slug}/checkout` for direct Thinkific payment, or build a Stripe payment flow in your Bolt app and use the enrollment API after successful payment. The Stripe path gives you more control over the payment experience but requires your Supabase database to track who has paid for what. The student dashboard (for logged-in users) fetches enrollment data from `/api/thinkific/user` to get the Thinkific user ID, then queries `/api/thinkific/enrollments?user_id=[id]` to get all enrolled courses with completion status. Display these as an enrolled courses list with progress indicators and Continue Learning links pointing to the Thinkific course player.

Bolt.new Prompt

Build the course catalog UI at app/courses/page.tsx. Fetch courses from /api/thinkific/courses. Show a grid of CourseCard components with name, description (max 100 chars), price or Free badge, thumbnail, and Enroll button. For free courses: clicking Enroll checks if user is logged in (if not, redirect to login), then calls /api/thinkific/enroll with the user's Thinkific ID (fetched from their Supabase profile or looked up by email). Show a success toast and a 'Start Learning' button linking to the Thinkific course URL. For paid courses: clicking shows a modal with course details and a 'Buy on Thinkific' link.

Paste this in Bolt.new chat

app/api/thinkific/enroll/route.ts
1// app/api/thinkific/enroll/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4const THINKIFIC_API_KEY = process.env.THINKIFIC_API_KEY;
5const THINKIFIC_SUBDOMAIN = process.env.THINKIFIC_SUBDOMAIN;
6const THINKIFIC_BASE = 'https://api.thinkific.com/api/public/v1';
7
8export async function POST(request: NextRequest) {
9 if (!THINKIFIC_API_KEY || !THINKIFIC_SUBDOMAIN) {
10 return NextResponse.json({ error: 'Thinkific not configured' }, { status: 500 });
11 }
12
13 const { userId, courseId } = await request.json();
14
15 if (!userId || !courseId) {
16 return NextResponse.json({ error: 'userId and courseId are required' }, { status: 400 });
17 }
18
19 const headers = {
20 'X-Auth-API-Key': THINKIFIC_API_KEY,
21 'X-Auth-Subdomain': THINKIFIC_SUBDOMAIN,
22 'Content-Type': 'application/json',
23 };
24
25 const response = await fetch(`${THINKIFIC_BASE}/enrollments`, {
26 method: 'POST',
27 headers,
28 body: JSON.stringify({
29 user_id: userId,
30 course_id: courseId,
31 activated_at: new Date().toISOString(),
32 }),
33 });
34
35 if (!response.ok) {
36 const error = await response.json();
37 return NextResponse.json(
38 { error: error.error || `Enrollment failed: ${response.status}` },
39 { status: response.status }
40 );
41 }
42
43 const enrollment = await response.json();
44
45 return NextResponse.json({
46 success: true,
47 enrollmentId: enrollment.id,
48 courseUrl: `https://${THINKIFIC_SUBDOMAIN}.thinkific.com/courses/${enrollment.course?.slug}`,
49 });
50}

Pro tip: When programmatically enrolling users, always include the activated_at timestamp in the enrollment POST request. Without it, Thinkific may create the enrollment in an inactive state that requires manual activation.

Expected result: The course catalog displays Thinkific courses with correct pricing. Free course enrollment works end-to-end: user clicks Enroll, gets enrolled via API, and receives a link to the Thinkific course player.

4

Configure Thinkific Webhooks for Enrollment Events

Thinkific webhooks deliver real-time notifications when students enroll in courses, complete lessons, or earn certificates. Setting up webhook delivery requires a deployed, publicly accessible URL — Bolt's WebContainer cannot receive incoming HTTP requests since it runs entirely in the browser with no public server. Deploy to Netlify or Bolt Cloud first by clicking Publish in Bolt's navigation. After deployment, go to Thinkific admin → Settings → Webhooks (or Apps & Integrations → Webhooks depending on your Thinkific version). Add a new webhook with your deployed URL, such as `https://your-app.netlify.app/api/thinkific/webhook`, and select the events to receive: `enrollment.created`, `enrollment.completed`, and optionally `lesson.completed`. Thinkific includes an HMAC signature in the `X-Thinkific-Hmac-SHA256` request header for webhook verification. Your webhook handler computes the expected HMAC by hashing the raw request body with your webhook secret using SHA-256, then compares it to the header value. This verification ensures the webhook came from Thinkific and not from a malicious third party. For each `enrollment.created` event, store the enrollment in Supabase and trigger a welcome sequence: send a welcome email via SendGrid or Resend, add the student to your email list, and optionally grant access to bonus resources. Respond to Thinkific's webhook with a 200 status quickly (within 10 seconds) — process any slow operations asynchronously after responding to avoid webhook timeout failures.

Bolt.new Prompt

Create a Thinkific webhook handler at app/api/thinkific/webhook/route.ts. Verify the X-Thinkific-Hmac-SHA256 header by computing HMAC-SHA256 of the raw request body with THINKIFIC_WEBHOOK_SECRET. If valid, handle enrollment.created events: extract user email, name, course name, and course ID from the payload. Save to Supabase enrollments table (student_email, student_name, course_name, course_id, enrolled_at, source: 'webhook'). For enrollment.completed events, update the enrollment record to set completed: true and completed_at: timestamp. Return 200 for success.

Paste this in Bolt.new chat

app/api/thinkific/webhook/route.ts
1// app/api/thinkific/webhook/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { createHmac } from 'crypto';
4import { createClient } from '@supabase/supabase-js';
5
6const WEBHOOK_SECRET = process.env.THINKIFIC_WEBHOOK_SECRET;
7
8const supabase = createClient(
9 process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 process.env.SUPABASE_SERVICE_ROLE_KEY!
11);
12
13export async function POST(request: NextRequest) {
14 const body = await request.text();
15 const signature = request.headers.get('X-Thinkific-Hmac-SHA256');
16
17 // Verify webhook signature
18 if (WEBHOOK_SECRET && signature) {
19 const expectedSig = createHmac('sha256', WEBHOOK_SECRET)
20 .update(body)
21 .digest('base64');
22
23 if (expectedSig !== signature) {
24 console.error('Thinkific webhook signature mismatch');
25 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
26 }
27 }
28
29 const payload = JSON.parse(body);
30 const { type, resource } = payload;
31
32 if (type === 'enrollment.created') {
33 const { user, course } = resource;
34
35 await supabase.from('enrollments').upsert({
36 student_email: user?.email,
37 student_name: `${user?.first_name || ''} ${user?.last_name || ''}`.trim(),
38 course_name: course?.name,
39 course_id: course?.id,
40 thinkific_enrollment_id: resource?.id,
41 enrolled_at: new Date().toISOString(),
42 source: 'webhook',
43 }, { onConflict: 'thinkific_enrollment_id' });
44 }
45
46 if (type === 'enrollment.completed') {
47 await supabase
48 .from('enrollments')
49 .update({ completed: true, completed_at: new Date().toISOString() })
50 .eq('thinkific_enrollment_id', resource?.id);
51 }
52
53 return NextResponse.json({ received: true });
54}

Pro tip: Always read the webhook body as raw text before parsing as JSON — HMAC signature verification requires the exact byte-for-byte raw body. Parsing as JSON first may alter whitespace or key ordering, causing signature mismatches.

Expected result: Thinkific webhooks deliver to the deployed URL. New enrollments and completions create or update records in Supabase automatically.

5

Deploy and Test the Complete Integration

With all components built, deploy to Netlify or Bolt Cloud. Click Publish in Bolt's navigation. After deployment, add environment variables to your hosting platform: `THINKIFIC_API_KEY`, `THINKIFIC_SUBDOMAIN`, `THINKIFIC_WEBHOOK_SECRET`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, and `SUPABASE_SERVICE_ROLE_KEY` (for the webhook handler). Trigger a redeploy after adding all variables. Test the complete flow in this order: first verify the course catalog loads at `/courses` on your deployed URL. Then create a test user in your Bolt app and verify the Thinkific user lookup by email returns a valid Thinkific user ID. Test the free course enrollment flow for an existing free course in your Thinkific school. Finally, verify webhook delivery by making a test enrollment in Thinkific's admin UI (manually enroll a test user from your Thinkific admin) and checking whether the webhook triggers and creates a record in Supabase. For debugging, check Netlify's Function Logs (or Bolt Cloud logs) for any API route errors. Thinkific's webhook settings page shows a delivery history — check the response status for recent webhook deliveries. A 200 response means your handler accepted the webhook; any 4xx or 5xx means a bug in the handler or misconfigured environment variables. For ongoing maintenance, Thinkific's API occasionally adds new fields to responses without breaking changes. Subscribe to Thinkific's developer newsletter or check their API changelog to stay informed of updates that might add useful data to your integration.

Bolt.new Prompt

Add a complete admin dashboard at app/admin/page.tsx (accessible only to users with admin role in Supabase). Show three sections: (1) Enrollment stats — total enrollments this month, total completions, most popular course (from Supabase enrollments table), (2) Recent enrollments — table of last 20 enrollments with student name, email, course, and date, (3) API health — show a green/red status for the Thinkific API connection by making a test call to /api/thinkific/courses and displaying OK or Error with the status code.

Paste this in Bolt.new chat

Pro tip: Set up a Supabase database function with pg_cron to periodically sync enrollment data from Thinkific API alongside webhook delivery. Webhooks can occasionally be missed, and a daily sync ensures your Supabase enrollment data stays accurate.

Expected result: The deployed app serves the Thinkific course catalog, handles free course enrollment, and receives webhook events. The admin dashboard shows enrollment stats from Supabase.

Common use cases

Custom Branded Course Catalog

Replace Thinkific's standard course directory with a custom-designed catalog that matches your brand and highlights courses based on your audience's interests. The catalog pulls live data from the Thinkific API so it stays current automatically as you add or update courses in Thinkific's admin.

Bolt.new Prompt

Build a course catalog page that fetches all published courses from my Thinkific school. Create an API route at app/api/thinkific/courses/route.ts that calls https://api.thinkific.com/api/public/v1/courses with headers X-Auth-API-Key: [THINKIFIC_API_KEY] and X-Auth-Subdomain: [THINKIFIC_SUBDOMAIN]. Display courses as cards with name, description, price (format as $XX or 'Free'), thumbnail, and an Enroll button. Add category filter buttons based on available course topics. Sort by price ascending by default with a toggle for newest first.

Copy this prompt to try it in Bolt.new

Automated Bundle Enrollment

When a customer purchases a bundle package on your Bolt.new app via Stripe, automatically enroll them in all included Thinkific courses using the API. The customer pays once on your site and gets instant access to multiple courses without needing to navigate Thinkific's checkout.

Bolt.new Prompt

Build a bundle purchase flow. When a user completes a Stripe payment for the 'Complete Bundle' ($297), call the Thinkific enrollment API (POST to /api/public/v1/enrollments) to enroll the user in three courses with IDs [COURSE_ID_1], [COURSE_ID_2], [COURSE_ID_3]. Use the Stripe webhook handler to trigger this enrollment after checkout.session.completed. Create a new Thinkific user account if one does not exist for the customer's email using POST /api/public/v1/users. Store enrollment confirmation in Supabase.

Copy this prompt to try it in Bolt.new

Progress Dashboard for Students

Show students a personalized learning dashboard with all their enrolled courses, completion percentages, recently accessed lessons, and certificates earned. The dashboard fetches enrollment data from Thinkific and presents it in a more motivating interface than Thinkific's default student portal.

Bolt.new Prompt

Create a student progress dashboard. After login, look up the student's Thinkific user by email using GET /api/public/v1/users?email=[email] with the API key headers. Then fetch their enrollments from GET /api/public/v1/enrollments?user_id=[id]. Display each enrolled course with: name, completion percentage, started_at date, and a 'Continue' button linking to the Thinkific course player. Show a completion certificate download button for courses where completed is true. Add a streak tracker showing consecutive days of learning activity.

Copy this prompt to try it in Bolt.new

Troubleshooting

Thinkific API returns 401 Unauthorized on all requests

Cause: The X-Auth-API-Key or X-Auth-Subdomain headers are missing, incorrect, or not being passed to the API route from environment variables.

Solution: Verify both THINKIFIC_API_KEY and THINKIFIC_SUBDOMAIN are set in your .env file. Ensure your API route reads both from process.env and passes them as headers named exactly 'X-Auth-API-Key' and 'X-Auth-Subdomain'. Test the values by logging them (remove logs before production) to confirm they are not empty strings.

typescript
1// Correct Thinkific authentication headers
2const response = await fetch(`${THINKIFIC_BASE}/courses`, {
3 headers: {
4 'X-Auth-API-Key': process.env.THINKIFIC_API_KEY!,
5 'X-Auth-Subdomain': process.env.THINKIFIC_SUBDOMAIN!,
6 'Content-Type': 'application/json',
7 },
8});

Programmatic enrollment fails with 'user not found' or 422 Unprocessable Entity

Cause: The user_id provided to the enrollment endpoint does not exist in Thinkific, or the course_id is incorrect. Both must be valid Thinkific IDs (numeric integers), not email addresses or slugs.

Solution: Ensure you are using the Thinkific user ID (numeric) from the user lookup endpoint, not the Supabase user ID (UUID) or the user's email address. Similarly, use the Thinkific course ID (numeric), not the course slug. If the user does not exist in Thinkific, create them first with POST /v1/users before attempting enrollment.

typescript
1// First: find or create Thinkific user
2const userResponse = await fetch(`${THINKIFIC_BASE}/users?email=${encodeURIComponent(email)}`, {
3 headers: thinkificHeaders,
4});
5const userData = await userResponse.json();
6const thinkificUserId = userData.items?.[0]?.id; // numeric ID
7
8// Then: enroll using numeric IDs only
9body: JSON.stringify({ user_id: thinkificUserId, course_id: numericCourseId })

Thinkific webhook signature verification fails even with correct secret

Cause: The webhook body was parsed as JSON before HMAC verification, which changes the exact byte sequence. The HMAC must be computed against the raw request body bytes as received.

Solution: Use request.text() to read the raw body before any JSON parsing. Store the raw string for HMAC computation, then parse it separately with JSON.parse(body) for processing. Never pass the JSON-parsed object to the HMAC function.

typescript
1// Correct: read raw body first
2const body = await request.text();
3const signature = request.headers.get('X-Thinkific-Hmac-SHA256');
4const expectedSig = createHmac('sha256', WEBHOOK_SECRET!).update(body).digest('base64');
5const payload = JSON.parse(body); // parse separately after verification

Course catalog shows courses but thumbnails are not loading

Cause: Thinkific course card images are stored on Thinkific's CDN with URLs that may require authentication or have domain restrictions. The image URL from the API may not be directly accessible from your app's domain.

Solution: Check whether the thumbnailUrl from the Thinkific API response is a complete HTTPS URL. If the URL requires authentication or is behind Thinkific's CDN, use a default placeholder image for courses without accessible thumbnails. Add an onError handler to the img element to fall back to a placeholder when the image fails to load.

typescript
1<img
2 src={course.thumbnailUrl || '/placeholder-course.jpg'}
3 alt={course.name}
4 onError={(e) => { e.currentTarget.src = '/placeholder-course.jpg'; }}
5 className="w-full h-40 object-cover"
6/>

Best practices

  • Never expose your Thinkific API key in client-side components — proxy all Thinkific API calls through Next.js API routes where the key is accessed from process.env
  • Cache Thinkific course catalog responses in Supabase for 15-30 minutes — course data changes infrequently and caching prevents rate limit issues
  • Always look up or create a Thinkific user before enrolling — never assume a Supabase user has a corresponding Thinkific account
  • Read webhook request bodies as raw text before JSON parsing — HMAC signature verification requires the unmodified byte sequence
  • Use upsert (not insert) when saving enrollment webhook data to Supabase — Thinkific may retry webhook delivery, and duplicate enrollments in your database cause confusion
  • Include the activated_at timestamp when creating programmatic enrollments — without it, Thinkific may create inactive enrollments
  • Test webhook delivery using Thinkific's test enrollment feature before going live — manually enroll a test user through Thinkific admin to trigger real webhook events without affecting production student data

Alternatives

Frequently asked questions

Is Thinkific's API available on the free plan?

Yes. Thinkific provides API access on all plans including the free plan, which is unusually generous compared to competitors like Teachable. This makes Thinkific an excellent choice for developers who want to build custom integrations without a paid subscription. Some advanced API features may be restricted to paid plans — check Thinkific's API documentation for specific endpoint availability by plan.

Can I enroll students programmatically without Thinkific's checkout?

Yes, for free courses. Thinkific's POST /enrollments endpoint creates enrollments directly. For paid courses, you can also use this endpoint after collecting payment through an external system (like Stripe in your Bolt app), then programmatically enroll the student after confirmed payment. This approach lets you build a custom purchase flow while using Thinkific for course delivery.

Do Thinkific API calls work in Bolt's WebContainer preview?

Yes. Thinkific API calls are outbound HTTP requests — Bolt's WebContainer can make outbound calls during development. You can test course listing, user lookup, and enrollment in the preview without deploying. Thinkific webhook delivery requires a deployed public URL since the WebContainer cannot receive incoming HTTP connections.

How do I handle students who have different emails in my app and Thinkific?

Add a settings page in your Bolt app where users can input their Thinkific email if it differs from their portal login email. Store this override email in the Supabase user profile and use it for Thinkific user lookups. When the lookup by primary email returns zero results, show the user a prompt to enter their Thinkific email manually.

Can I access individual lesson completion data through the Thinkific API?

Thinkific provides course-level enrollment and completion status through the API. Individual lesson completion data is available through the enrollment.completed webhook event and the enrollment's completion percentage field. For granular lesson-level analytics, Thinkific's admin dashboard provides more detail than the API currently exposes.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.