To integrate Replit with Teachable, generate an API key from your Teachable school settings, store it in Replit Secrets (lock icon π), and use Python or Node.js to manage courses, enrollments, and users. Set up webhook endpoints in Replit to receive real-time notifications when students enroll, complete lessons, or purchase courses.
Why Connect Replit to Teachable?
Teachable is one of the most popular platforms for online course creators, with the API and webhook system enabling custom workflows beyond the platform's native automation. You can sync student data to a CRM, grant Slack community access on enrollment, issue custom certificates on completion, or build internal dashboards tracking enrollment and completion rates.
The Teachable API is REST-based with API key authentication. The key is passed in the apiKey header for every request. API endpoints cover schools, courses, users, enrollments, and purchases.
Teachable webhooks complement the API by pushing real-time event notifications to your Replit server. When a student enrolls, completes a lecture, or purchases a course, Teachable sends a POST request to your configured webhook URL. Your Replit server processes these events and triggers downstream actions. This event-driven pattern is more efficient than polling the API for changes.
Integration method
You connect Replit to Teachable by generating an API key from your Teachable school admin settings, storing it in Replit Secrets, and using Python or Node.js to query courses, manage enrollments, and retrieve user data. For real-time student events (enrollment, course completion, purchases), you configure a webhook endpoint in Teachable that points to your deployed Replit URL, receiving POST requests whenever those events occur.
Prerequisites
- A Replit account with a Python or Node.js project created
- A Teachable school on a plan that includes API access (Pro plan or higher at teachable.com)
- An API key generated from your Teachable school admin settings under Settings > General
- A deployed Replit URL (not the editor preview URL) for webhook configuration β webhooks require a public HTTPS endpoint
- Basic understanding of HTTP webhooks and how to handle POST request payloads
Step-by-step guide
Generate a Teachable API Key
Generate a Teachable API Key
Log into your Teachable school admin dashboard. Navigate to Settings (gear icon in the sidebar) and then to the General tab. Scroll down to find the API Keys section. Click 'New API Key', give it a descriptive name (e.g., 'Replit Integration'), and click Create. Teachable will display the API key once when it is first created β copy it immediately and store it somewhere safe, as you cannot view the full key again after leaving the page. If you lose the key, you will need to create a new one and delete the old one. Teachable API access requires a Pro plan ($99/month as of 2026) or higher. Starter plan users do not have API access. Check your current plan in Settings > Plan if you are unsure. The API key is used in the apiKey request header (not Authorization header, not a query parameter). The exact header name is 'apiKey' β note the camelCase. This is different from most APIs. The base URL for the Teachable API is https://developers.teachable.com/v1 for the public API or https://{yourschool}.teachable.com/api/v1 for school-specific endpoints depending on which endpoint you use.
Pro tip: Copy the API key immediately when created β Teachable only shows the full key once. Store it in Replit Secrets right away before doing anything else.
Expected result: You have a Teachable API key from the school admin settings page.
Store the API Key in Replit Secrets
Store the API Key in Replit Secrets
Click the lock icon π in the Replit sidebar to open Secrets. Add two secrets: - Key: TEACHABLE_API_KEY β Value: your Teachable API key - Key: TEACHABLE_SCHOOL_SUBDOMAIN β Value: your school subdomain (e.g., if your school is at myschool.teachable.com, enter 'myschool') The subdomain is needed to construct the correct API base URL for school-specific endpoints. Access the key in Python with os.environ['TEACHABLE_API_KEY'] and in Node.js with process.env.TEACHABLE_API_KEY. Teachable API keys have school-level access β they can read and modify all courses, enrollments, and users in your school. Treat them as highly sensitive credentials. Never include them in frontend JavaScript or client-facing API responses.
Pro tip: Store the school subdomain as a separate secret rather than hardcoding it in your server file β this makes it easy to update if your school URL changes.
Expected result: TEACHABLE_API_KEY and TEACHABLE_SCHOOL_SUBDOMAIN appear in the Replit Secrets pane.
Query Courses and Enrollments with Python
Query Courses and Enrollments with Python
Install requests and Flask with 'pip install requests flask'. The Teachable API uses the apiKey header for authentication. Note: Teachable has two API versions β the public API at developers.teachable.com/v1 and the school-level API at {subdomain}.teachable.com/api/v1. Enrollment management (enroll/unenroll) typically uses the school-level API, while course listing can use either. The Flask server below provides endpoints for listing courses, getting enrollment data, and enrolling users. The enroll endpoint is particularly useful for programmatic course access β you can grant free access to a course without the student needing to purchase it through Teachable.
1import os2import requests3from flask import Flask, request, jsonify45app = Flask(__name__)67TEACHABLE_API_KEY = os.environ["TEACHABLE_API_KEY"]8SCHOOL_SUBDOMAIN = os.environ["TEACHABLE_SCHOOL_SUBDOMAIN"]910PUBLIC_API = "https://developers.teachable.com/v1"11SCHOOL_API = f"https://{SCHOOL_SUBDOMAIN}.teachable.com/api/v1"1213HEADERS = {14 "apiKey": TEACHABLE_API_KEY,15 "Content-Type": "application/json",16 "Accept": "application/json"17}181920@app.route("/courses")21def list_courses():22 """List all courses in the school."""23 page = request.args.get("page", 1)24 per = request.args.get("per", 25)25 resp = requests.get(26 f"{PUBLIC_API}/courses",27 params={"page": page, "per": per},28 headers=HEADERS29 )30 resp.raise_for_status()31 return jsonify(resp.json())323334@app.route("/courses/<int:course_id>/students")35def course_students(course_id):36 """Get enrolled students for a specific course."""37 page = request.args.get("page", 1)38 resp = requests.get(39 f"{PUBLIC_API}/courses/{course_id}/students",40 params={"page": page},41 headers=HEADERS42 )43 resp.raise_for_status()44 return jsonify(resp.json())454647@app.route("/enroll", methods=["POST"])48def enroll_user():49 """50 Enroll a user in a course.51 Body: { "email": "user@example.com", "course_id": 12345 }52 """53 data = request.get_json()54 email = data.get("email")55 course_id = data.get("course_id")5657 if not email or not course_id:58 return jsonify({"error": "email and course_id are required"}), 4005960 payload = {61 "users": [{62 "email": email63 }]64 }65 resp = requests.post(66 f"{PUBLIC_API}/courses/{course_id}/students",67 json=payload,68 headers=HEADERS69 )70 resp.raise_for_status()71 return jsonify(resp.json())727374@app.route("/users")75def list_users():76 """List all users in the school."""77 page = request.args.get("page", 1)78 per = request.args.get("per", 25)79 resp = requests.get(80 f"{PUBLIC_API}/users",81 params={"page": page, "per": per},82 headers=HEADERS83 )84 resp.raise_for_status()85 return jsonify(resp.json())868788@app.route("/users/find")89def find_user():90 """Look up a user by email."""91 email = request.args.get("email")92 if not email:93 return jsonify({"error": "email parameter required"}), 40094 resp = requests.get(95 f"{PUBLIC_API}/users",96 params={"email": email},97 headers=HEADERS98 )99 resp.raise_for_status()100 return jsonify(resp.json())101102103if __name__ == "__main__":104 app.run(host="0.0.0.0", port=3000, debug=True)Pro tip: Use the /users/find endpoint to check if a user already exists before enrolling β trying to enroll a non-existent email will create a new user account in Teachable.
Expected result: GET /courses returns a list of courses in your Teachable school and POST /enroll creates a course enrollment for the specified email address.
Create a Webhook Receiver for Student Events
Create a Webhook Receiver for Student Events
Teachable webhooks send POST requests to your Replit server when specific events occur: enrollment.created, enrollment.completed, purchase.created, purchase.refunded, and others. Your server must return HTTP 200 within a few seconds, or Teachable will retry the webhook. The webhook payload is a JSON object containing the event type and relevant data. For enrollment events, the data includes the student's name, email, and the course they enrolled in. For purchase events, it includes transaction amount and product details. Deploy your Replit app first, then configure the webhook URL in Teachable under Settings > Integrations > Webhooks. Teachable does not sign webhooks with a shared secret by default β verify event authenticity by checking expected event structure or implementing IP allowlisting if security is critical. The Node.js server below implements webhook handling for the most common Teachable events:
1const express = require('express');2const axios = require('axios');34const app = express();5app.use(express.json());67const TEACHABLE_API_KEY = process.env.TEACHABLE_API_KEY;8const SCHOOL_SUBDOMAIN = process.env.TEACHABLE_SCHOOL_SUBDOMAIN;9const PUBLIC_API = 'https://developers.teachable.com/v1';1011const HEADERS = {12 apiKey: TEACHABLE_API_KEY,13 'Content-Type': 'application/json',14 Accept: 'application/json'15};1617// Webhook receiver for Teachable events18app.post('/webhook', async (req, res) => {19 const event = req.body;20 const eventType = event.type;21 const data = event.object || event.data || {};2223 console.log(`Received Teachable webhook: ${eventType}`);2425 try {26 switch (eventType) {27 case 'enrollment.created': {28 const student = data.user || {};29 const course = data.course || {};30 console.log(`New enrollment: ${student.email} enrolled in ${course.name}`);31 // Add your post-enrollment logic here:32 // - Send welcome email33 // - Post to Slack34 // - Update CRM record35 break;36 }37 case 'enrollment.completed': {38 const student = data.user || {};39 const course = data.course || {};40 console.log(`Course completed: ${student.email} finished ${course.name}`);41 // Add your completion logic:42 // - Issue certificate43 // - Unlock next course44 // - Send completion email45 break;46 }47 case 'purchase.created': {48 const buyer = data.user || {};49 const product = data.course || data.bundle || {};50 const amount = data.amount || 0;51 console.log(`Purchase: ${buyer.email} bought ${product.name} for $${amount / 100}`);52 break;53 }54 case 'purchase.refunded': {55 const buyer = data.user || {};56 console.log(`Refund processed for ${buyer.email}`);57 // Revoke access, update CRM, etc.58 break;59 }60 default:61 console.log(`Unhandled event type: ${eventType}`);62 }6364 // Always return 200 quickly65 res.json({ received: true });66 } catch (err) {67 console.error('Webhook processing error:', err.message);68 // Still return 200 to prevent Teachable from retrying69 res.json({ received: true, error: err.message });70 }71});7273// REST API routes74app.get('/courses', async (req, res) => {75 try {76 const { data } = await axios.get(`${PUBLIC_API}/courses`, { headers: HEADERS });77 res.json(data);78 } catch (err) {79 res.status(err.response?.status || 500).json({ error: err.message });80 }81});8283app.post('/enroll', async (req, res) => {84 const { email, course_id } = req.body;85 if (!email || !course_id) {86 return res.status(400).json({ error: 'email and course_id are required' });87 }88 try {89 const { data } = await axios.post(90 `${PUBLIC_API}/courses/${course_id}/students`,91 { users: [{ email }] },92 { headers: HEADERS }93 );94 res.json(data);95 } catch (err) {96 res.status(err.response?.status || 500).json({ error: err.message });97 }98});99100app.listen(3000, '0.0.0.0', () => {101 console.log('Teachable integration server running on port 3000');102});Pro tip: Always return HTTP 200 from your webhook endpoint immediately, even if processing fails. Log errors and handle them asynchronously. If you return a non-200 status, Teachable will retry the webhook, potentially triggering duplicate actions.
Expected result: POST /webhook correctly receives and logs Teachable enrollment and purchase events, and returns HTTP 200 to prevent retries.
Deploy and Configure Webhooks in Teachable
Deploy and Configure Webhooks in Teachable
Deploy your Replit app by clicking the Deploy button. For webhook receivers that must be reliably available whenever a student enrolls or purchases, choose a Reserved VM deployment so the server is always running and the webhook endpoint is guaranteed responsive. After deployment, copy your Replit deployment URL (e.g., https://your-app.yourusername.repl.co). In Teachable admin, go to Settings > Integrations > Webhooks. Click 'Add a new webhook endpoint' and paste your deployment URL with the /webhook path appended. Select the event types you want to receive (enrollment.created, enrollment.completed, purchase.created, etc.). Teachable will send a test webhook event when you save β check your Replit deployment logs to confirm the event was received. Once confirmed, your Replit server will receive real-time notifications for every qualifying student event.
1[[ports]]2internalPort = 30003externalPort = 8045[deployment]6run = ["node", "server.js"]7deploymentTarget = "cloudrun"Pro tip: Use Reserved VM for a Teachable webhook receiver to avoid missed events during Autoscale cold starts. A missed enrollment event could result in students not receiving their welcome email or community access.
Expected result: Your deployed Replit server receives Teachable webhook events, and configuring the webhook in Teachable's admin shows a successful test event in your server logs.
Common use cases
Auto-Enroll Leads from CRM into Courses
A Replit backend listens for new lead events from a CRM webhook, checks if the lead matches criteria for a free course offer, and uses the Teachable API to automatically enroll them. This removes the manual step of individually enrolling leads into introductory courses and ensures every qualifying lead gets immediate access.
Build a Flask server with a /crm-webhook endpoint that receives new lead data, checks if they qualify for a free course, and enrolls them in a specific Teachable course using the Teachable API.
Copy this prompt to try it in Replit
Slack Community Access on Enrollment
When a student enrolls in a paid course, a Replit webhook receiver catches the Teachable enrollment event and sends the student an invitation to a private Slack workspace. The Replit server verifies the webhook payload, extracts the student's email, and uses the Slack API to generate an invite link. This automates community onboarding for course students.
Create a Node.js webhook server that listens for Teachable enrollment events, extracts the student email, and sends them a Slack workspace invitation using the Slack API.
Copy this prompt to try it in Replit
Completion Certificate Email with Branding
A Replit server listens for Teachable course completion webhooks, generates a custom PDF certificate with the student's name and course title using a template, and emails it to the student. Teachable's native certificates are limited in customization β this approach allows fully branded certificates with custom fonts, logos, and signatures.
Write a Python Flask server that receives a Teachable course_completion webhook, generates a personalized PDF certificate using reportlab, and emails it to the student using SendGrid.
Copy this prompt to try it in Replit
Troubleshooting
HTTP 401 Unauthorized when calling the Teachable API
Cause: The API key is missing from the request header, or the header name is wrong. Teachable uses the header name 'apiKey' (camelCase), not 'Authorization' or 'X-API-Key'.
Solution: Verify that your request includes the header 'apiKey' with your API key value. The exact header name is case-sensitive in some frameworks. Check that TEACHABLE_API_KEY in Replit Secrets contains the correct value without extra whitespace.
1# Correct header name β note camelCase 'apiKey'2headers = {3 'apiKey': os.environ['TEACHABLE_API_KEY'],4 'Content-Type': 'application/json'5}Teachable webhook not arriving at Replit server
Cause: The webhook URL in Teachable points to the Replit editor preview URL (which changes and requires authentication) instead of the public deployment URL.
Solution: Use the public deployment URL from the Replit Deploy tab, not the editor preview URL. The deployment URL format is https://your-app-name.yourusername.repl.co. Deploy the app first, then copy the deployment URL into the Teachable webhook settings.
Enrollment API returns 404 Not Found for a course ID
Cause: The course ID in the API request does not match an existing course in your school, or you are using a course ID from a different school's API response.
Solution: First call the /courses endpoint to list all courses and confirm the course ID exists. Course IDs in Teachable are school-specific integers β they are not globally unique across all Teachable schools.
1# Always fetch and verify course IDs from your school first2courses_resp = requests.get(f'{PUBLIC_API}/courses', headers=HEADERS)3courses = courses_resp.json().get('courses', [])4print([{c['id']: c['name']} for c in courses])Teachable webhook events are received but processed twice
Cause: Your webhook endpoint returned a non-200 status or took longer than Teachable's timeout, causing Teachable to retry the event. If processing completed before the retry, the same event runs twice.
Solution: Always return HTTP 200 immediately from the webhook handler, then process the event asynchronously. Use an idempotency key (e.g., store processed event IDs) to prevent duplicate processing if retries do occur.
1processed_events = set()23@app.route('/webhook', methods=['POST'])4def webhook():5 event = request.get_json()6 event_id = event.get('id', '')7 if event_id in processed_events:8 return jsonify({'received': True, 'note': 'duplicate'})9 processed_events.add(event_id)10 # process event...11 return jsonify({'received': True})Best practices
- Store TEACHABLE_API_KEY in Replit Secrets (lock icon π) β never expose it in client-side code or API responses.
- Use Reserved VM deployment for webhook receivers to ensure the endpoint is always available and never misses enrollment or purchase events.
- Return HTTP 200 from webhook handlers immediately before processing β log errors asynchronously to prevent duplicate webhook retries.
- Implement event deduplication using a processed event ID set or database to handle Teachable webhook retries safely.
- Verify enrollment before sending downstream actions like community invitations β call the Teachable API to confirm enrollment status rather than trusting the webhook payload alone.
- Store webhook event payloads to a database before processing so you can replay failed events without re-triggering from Teachable.
- Test webhook handling with Teachable's built-in test event sender before connecting live student workflows.
- Use pagination parameters (page, per) when listing courses or students β schools with many courses or large student lists require multiple API calls to retrieve all records.
Alternatives
Podia bundles courses, memberships, and digital downloads in one platform with a simpler API, making it a better choice for creators who sell multiple product types beyond just courses.
Canvas LMS is a full-featured learning management system used by universities and enterprises, offering a more powerful API for complex academic workflows beyond what Teachable provides.
Mailchimp is the better choice if your primary goal is email marketing automation for course students rather than managing enrollments and course data.
Frequently asked questions
How do I get a Teachable API key?
Log into your Teachable school admin and go to Settings > General. Scroll down to the API Keys section and click 'New API Key'. Give it a name and click Create. Copy the key immediately β Teachable only shows it once. API access requires a Pro plan or higher.
Can I enroll students into courses programmatically with the Teachable API?
Yes. Send a POST request to /courses/{course_id}/students with the user's email in the request body using the apiKey header for authentication. If the user does not have a Teachable account, one will be created automatically. This is useful for enrolling leads who signed up through an external form or CRM.
How do Teachable webhooks work with Replit?
Teachable webhooks send POST requests to your Replit deployment URL whenever student events occur (enrollment, completion, purchase). Deploy your Replit app first to get a public URL, then configure that URL as a webhook endpoint in Teachable Settings > Integrations > Webhooks. Your Replit server must return HTTP 200 within a few seconds to acknowledge receipt.
Is there a free tier for the Teachable API?
No. Teachable API access requires a Pro plan ($99/month as of 2026) or higher. The free and Basic plans only provide access to the Teachable web UI and do not include API credentials.
What Teachable webhook events can I receive in Replit?
Common Teachable webhook events include enrollment.created (student enrolled), enrollment.completed (course completed), purchase.created (purchase successful), purchase.refunded (refund issued), and lecture.completed (individual lesson finished). Configure which events to receive in Teachable Settings > Integrations > Webhooks when adding your endpoint.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation