REST API reference
Teachable API
15 endpoints for Teachable. Each one is a POST with a JSON body, called with your API key.
- Base URL
- https://cotlet.dev/api/v1/teachable
- Connects with
- Your own Teachable school owner account
- Integration page
- Teachable
| Endpoint | Access | Description |
|---|---|---|
| list_courses | read | Courses in your school with name, published state and URL. |
| get_curriculum | read | Sections and lessons of a course, in order, with published and public preview flags. |
| get_lesson | read | Lesson content as blocks: text as Markdown, plus the type of each video, PDF, audio, quiz and other block. |
| list_students | read | Students with name, email and join date, or the students of one course. |
| list_comments | read | Lesson comments across your school or one course, with author, lesson, status and unread flag. |
| list_coupons | read | School and product coupons with code, discount, uses, expiry and status. |
| create_lesson | write | New unpublished lesson at the end of a section, with text from Markdown. |
| update_lesson | write | Rename a lesson or replace the text of its Text & Image block. Other blocks stay as they are. |
| publish_lesson | write | Publish a lesson so enrolled students see it, or unpublish it. |
| create_coupon | write | Coupon code for all products, one product type or one pricing plan. |
| archive_coupon | write | Unpublish a coupon so the code stops working. Archived codes can be used again. |
| enroll_student | write | Enroll a student in a course for free, adding them to your school first if the email is new. |
| send_email | write | Send an email from your school to chosen users, the students of a course or all users, as on the Emails, Compose page. |
| reply_to_comment | write | Reply to a lesson comment as the school owner. The reply is public to the course. |
| moderate_comment | write | Approve or deny a comment held in Pending Approval. |
List courses
readPOST https://cotlet.dev/api/v1/teachable/list_courses
Returns every course in your school, in the order the Courses page shows them. Use course_id from each item in the other tools. Set published to see only the courses students can enroll in.
Parameters
| Name | Type | Description |
|---|---|---|
| published | boolean | Only published courses. Default: all. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/list_courses \
-H "Authorization: Bearer $COTLET_API_KEY" result = client.call("teachable", "list_courses") const result = await cotlet.call('teachable', 'list_courses'); cotlet call teachable list_courses Example response
{
"courses": [
{
"course_id": "1204512",
"name": "Python for Beginners",
"published": true,
"url": "https://yourschool.teachable.com/p/python-for-beginners"
},
{
"course_id": "1260077",
"name": "Team Onboarding",
"published": false,
"url": "https://yourschool.teachable.com/p/team-onboarding"
}
]
} Get curriculum
readPOST https://cotlet.dev/api/v1/teachable/get_curriculum
Returns the Curriculum tab of one course: sections in order, and the lessons inside each section in order. Each lesson has a published flag and a public preview flag. Use lesson_id with get_lesson, update_lesson and publish_lesson.
Parameters
| Name | Type | Description |
|---|---|---|
| course_id required | string | Course ID from list_courses. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/get_curriculum \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"course_id":"course_8c41d2"}' result = client.call("teachable", "get_curriculum", {"course_id":"course_8c41d2"}) const result = await cotlet.call('teachable', 'get_curriculum', {"course_id":"course_8c41d2"}); cotlet call teachable get_curriculum --data '{"course_id":"course_8c41d2"}' Example response
{
"course_id": "1204512",
"sections": [
{
"section_id": "9120331",
"name": "Basics",
"lessons": [
{
"lesson_id": "35120871",
"name": "Lesson 1: Welcome",
"published": true,
"public_preview": true
},
{
"lesson_id": "35120874",
"name": "Lesson 3: Functions",
"published": true,
"public_preview": false
}
]
}
]
} Get lesson
readPOST https://cotlet.dev/api/v1/teachable/get_lesson
Returns the blocks of one lesson in the order the lesson editor shows them. Text & Image blocks come back as Markdown with a block_id. Other blocks (video, PDF viewer, audio, resource, quiz and so on) come back with their type and title only.
Parameters
| Name | Type | Description |
|---|---|---|
| course_id required | string | Course ID from list_courses. |
| lesson_id required | string | Lesson ID from get_curriculum. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/get_lesson \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}' result = client.call("teachable", "get_lesson", {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}) const result = await cotlet.call('teachable', 'get_lesson', {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}); cotlet call teachable get_lesson --data '{"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}' Example response
{
"lesson_id": "35120874",
"name": "Lesson 3: Functions",
"published": true,
"blocks": [
{
"block_id": "b_5521",
"type": "text_and_image",
"body_markdown": "# Functions\n\nA function groups steps you want to reuse."
},
{
"block_id": "b_5522",
"type": "video",
"title": "Functions in 6 minutes"
}
]
} List students
readPOST https://cotlet.dev/api/v1/teachable/list_students
Lists students of your school, newest first. Pass course_id to list only the students enrolled in one course. Pages hold up to 100 students; pass next_cursor back as cursor for the next page. Use the emails with send_email when you need part of a course.
Parameters
| Name | Type | Description |
|---|---|---|
| course_id | string | Only students enrolled in this course. |
| limit | integer | 1 to 100. Default: 50. |
| cursor | string | Next page cursor from the previous call. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/list_students \
-H "Authorization: Bearer $COTLET_API_KEY" result = client.call("teachable", "list_students") const result = await cotlet.call('teachable', 'list_students'); cotlet call teachable list_students Example response
{
"students": [
{
"user_id": "70418822",
"name": "Maria Kowal",
"email": "maria.kowal@example.com",
"joined_at": "2026-09-28T10:14:00Z"
}
],
"next_cursor": null
} List comments
readPOST https://cotlet.dev/api/v1/teachable/list_comments
Lists comments the way Site, Comments does, newest first. Status pending returns the Pending Approval queue, which exists only when Comment moderation is on; denied returns comments you denied, which stay in your admin. Use comment_id with moderate_comment and reply_to_comment.
Parameters
| Name | Type | Description |
|---|---|---|
| course_id | string | Only comments in this course. Default: all courses. |
| status | enum | all, pending or denied. Default: all. |
| unread_only | boolean | Only comments you have not marked as read. |
| limit | integer | 1 to 100. Default: 50. |
| cursor | string | Next page cursor from the previous call. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/list_comments \
-H "Authorization: Bearer $COTLET_API_KEY" result = client.call("teachable", "list_comments") const result = await cotlet.call('teachable', 'list_comments'); cotlet call teachable list_comments Example response
{
"comments": [
{
"comment_id": "88120431",
"course_id": "1204512",
"lesson": "Lesson 3: Functions",
"author": "Maria Kowal",
"body": "Can a function return two values?",
"status": "pending",
"unread": true,
"created_at": "2026-10-09T14:32:00Z"
}
],
"next_cursor": null
} List coupons
readPOST https://cotlet.dev/api/v1/teachable/list_coupons
Returns the coupons from the Coupons page: school coupons (all products or one product type) and product coupons (one pricing plan). Archived coupons are left out unless you ask for them. Check the list before create_coupon so a code is not used twice.
Parameters
| Name | Type | Description |
|---|---|---|
| include_archived | boolean | Also list archived coupons. Default: false. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/list_coupons \
-H "Authorization: Bearer $COTLET_API_KEY" result = client.call("teachable", "list_coupons") const result = await cotlet.call('teachable', 'list_coupons'); cotlet call teachable list_coupons Example response
{
"coupons": [
{
"coupon_id": "3917702",
"name": "Summer sale",
"code": "SUMMER20",
"discount": "20%",
"applies_to": "all_products",
"expires_at": "2026-09-01",
"quantity": 500,
"used": 212,
"status": "archived"
}
]
} Create lesson
writePOST https://cotlet.dev/api/v1/teachable/create_lesson
Adds a lesson to a section, like New lesson in the Curriculum tab. New lessons start unpublished, so no student sees it until publish_lesson runs. That is why create_lesson runs without asking first.
Parameters
| Name | Type | Description |
|---|---|---|
| course_id required | string | Course ID from list_courses. |
| section_id required | string | Section ID from get_curriculum. |
| title required | string | Lesson name. |
| body_markdown | string | Text for a Text & Image block. Images by URL. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/create_lesson \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"course_id":"course_8c41d2","section_id":"section_8c41d2","title":"title"}' result = client.call("teachable", "create_lesson", {"course_id":"course_8c41d2","section_id":"section_8c41d2","title":"title"}) const result = await cotlet.call('teachable', 'create_lesson', {"course_id":"course_8c41d2","section_id":"section_8c41d2","title":"title"}); cotlet call teachable create_lesson --data '{"course_id":"course_8c41d2","section_id":"section_8c41d2","title":"title"}' Example response
{
"lesson_id": "35120990",
"name": "Lesson 4: Loops",
"section_id": "9120331",
"published": false
} POST https://cotlet.dev/api/v1/teachable/update_lesson
Renames a lesson and replaces the text of one Text & Image block; video, PDF, audio and quiz blocks are not touched. Teachable saves edits to a published lesson live at once, so students see the change right away. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| course_id required | string | Course ID from list_courses. |
| lesson_id required | string | Lesson ID from get_curriculum. |
| title | string | New lesson name. |
| body_markdown | string | New text for the Text & Image block. |
| block_id | string | Text & Image block ID from get_lesson. Default: the first one. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/update_lesson \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"course_id":"1204512","lesson_id":"35120874","title":"Lesson 3: Functions","body_markdown":"# Functions\n\nA function groups steps you want to reuse."}' result = client.call("teachable", "update_lesson", {"course_id":"1204512","lesson_id":"35120874","title":"Lesson 3: Functions","body_markdown":"# Functions\n\nA function groups steps you want to reuse."}) const result = await cotlet.call('teachable', 'update_lesson', {"course_id":"1204512","lesson_id":"35120874","title":"Lesson 3: Functions","body_markdown":"# Functions\n\nA function groups steps you want to reuse."}); cotlet call teachable update_lesson --data '{"course_id":"1204512","lesson_id":"35120874","title":"Lesson 3: Functions","body_markdown":"# Functions\n\nA function groups steps you want to reuse."}' Example response
{
"lesson_id": "35120874",
"name": "Lesson 3: Functions",
"updated_block_id": "b_5521",
"published": true,
"updated_at": "2026-10-11T09:20:00+02:00"
} POST https://cotlet.dev/api/v1/teachable/publish_lesson
Does what Publish and Unpublish do in the lesson editor. Unpublished lessons are hidden from students, and a section whose lessons are all unpublished is hidden too. The course itself must be published and enrollable on its Information tab. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| course_id required | string | Course ID from list_courses. |
| lesson_id required | string | Lesson ID from get_curriculum. |
| published | boolean | false unpublishes. Default: true. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/publish_lesson \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}' result = client.call("teachable", "publish_lesson", {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}) const result = await cotlet.call('teachable', 'publish_lesson', {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}); cotlet call teachable publish_lesson --data '{"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}' Example response
{
"lesson_id": "35120874",
"published": true
} POST https://cotlet.dev/api/v1/teachable/create_coupon
Creates a coupon on the Coupons page. The code holds letters and numbers only. Quantity is the number available, up to 10,000. Teachable does not sell a paid product below $1.00 USD. The code takes money off your sales. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| name required | string | Internal name, for your own list. |
| code required | string | Letters and numbers only. |
| discount_type required | enum | percent or amount. |
| discount required | integer | Percent off, or the fixed amount off. |
| applies_to | enum | all_products, courses, coaching, digital_downloads, memberships, bundles or pricing_plan. Default: all_products. |
| pricing_plan_id | string | Required when applies_to is pricing_plan. |
| expires_at | date | Last day the code works. |
| quantity | integer | Number available, 1 to 10000. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/create_coupon \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Black Friday 2026","code":"BLACKFRIDAY","discount_type":"percent","discount":30,"applies_to":"courses","expires_at":"2026-12-01","quantity":100}' result = client.call("teachable", "create_coupon", {"name":"Black Friday 2026","code":"BLACKFRIDAY","discount_type":"percent","discount":30,"applies_to":"courses","expires_at":"2026-12-01","quantity":100}) const result = await cotlet.call('teachable', 'create_coupon', {"name":"Black Friday 2026","code":"BLACKFRIDAY","discount_type":"percent","discount":30,"applies_to":"courses","expires_at":"2026-12-01","quantity":100}); cotlet call teachable create_coupon --data '{"name":"Black Friday 2026","code":"BLACKFRIDAY","discount_type":"percent","discount":30,"applies_to":"courses","expires_at":"2026-12-01","quantity":100}' Example response
{
"coupon_id": "3918274",
"name": "Black Friday 2026",
"code": "BLACKFRIDAY",
"discount": "30%",
"applies_to": "courses",
"expires_at": "2026-12-01",
"quantity": 100,
"used": 0,
"status": "active"
} POST https://cotlet.dev/api/v1/teachable/archive_coupon
Archives a coupon, which is how Teachable unpublishes one. Buyers who have the code can no longer use it. An archived code can be used for a new coupon later. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| coupon_id required | string | Coupon ID from list_coupons. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/archive_coupon \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"coupon_id":"coupon_8c41d2"}' result = client.call("teachable", "archive_coupon", {"coupon_id":"coupon_8c41d2"}) const result = await cotlet.call('teachable', 'archive_coupon', {"coupon_id":"coupon_8c41d2"}); cotlet call teachable archive_coupon --data '{"coupon_id":"coupon_8c41d2"}' Example response
{
"coupon_id": "3917702",
"code": "SUMMER20",
"status": "archived"
} POST https://cotlet.dev/api/v1/teachable/enroll_student
Does what manual enrollment does in the Users menu: the student gets access to the course with no payment. A new email is added to your school first, because adding a student does not enroll them. Teachable sends no confirmation email for manual enrollments, and plan limits such as access duration or enrollment caps do not apply. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| email required | string | Student email. |
| course_id required | string | Course ID from list_courses. |
| name | string | Full name, used when the student is new. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/enroll_student \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"jane@example.com","course_id":"course_8c41d2"}' result = client.call("teachable", "enroll_student", {"email":"jane@example.com","course_id":"course_8c41d2"}) const result = await cotlet.call('teachable', 'enroll_student', {"email":"jane@example.com","course_id":"course_8c41d2"}); cotlet call teachable enroll_student --data '{"email":"jane@example.com","course_id":"course_8c41d2"}' Example response
{
"user_id": "70418901",
"email": "maria.kowal@example.com",
"course_id": "1204512",
"status": "enrolled",
"user_created": true,
"confirmation_email_sent": false
} POST https://cotlet.dev/api/v1/teachable/send_email
Sends a custom email from your school. Students who opted out of instructional and promotional emails in their profile do not receive it. To reach part of a course, take the emails from list_students and pass them in user_emails. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| to required | enum | users, course or all_users. |
| user_emails | string[] | Recipients when to is users. |
| course_id | string | Recipients when to is course. |
| subject required | string | Email subject. |
| body_markdown required | string | Email body. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/send_email \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"users","subject":"subject","body_markdown":"body markdown"}' result = client.call("teachable", "send_email", {"to":"users","subject":"subject","body_markdown":"body markdown"}) const result = await cotlet.call('teachable', 'send_email', {"to":"users","subject":"subject","body_markdown":"body markdown"}); cotlet call teachable send_email --data '{"to":"users","subject":"subject","body_markdown":"body markdown"}' Example response
{
"email_id": "em_4471",
"to": "course",
"course_id": "1204512",
"subject": "Live Q&A moved to Thursday",
"status": "sent",
"sent_at": "2026-10-11T09:30:00+02:00"
} POST https://cotlet.dev/api/v1/teachable/reply_to_comment
Posts a reply under one comment, as the Reply button does in your comment admin. Comments are public and cannot be made private, so the whole course sees the reply. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| comment_id required | string | Comment ID from list_comments. |
| body required | string | Reply text. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/reply_to_comment \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"comment_id":"comment_8c41d2","body":"body"}' result = client.call("teachable", "reply_to_comment", {"comment_id":"comment_8c41d2","body":"body"}) const result = await cotlet.call('teachable', 'reply_to_comment', {"comment_id":"comment_8c41d2","body":"body"}); cotlet call teachable reply_to_comment --data '{"comment_id":"comment_8c41d2","body":"body"}' Example response
{
"reply_id": "88120502",
"comment_id": "88120431",
"status": "published"
} POST https://cotlet.dev/api/v1/teachable/moderate_comment
Does what Approve and Deny do on a pending comment. An approved comment appears on the lesson. A denied comment stays in your comment admin with a Denied tag and is hidden from students. Pending Approval exists only when Comment moderation is on. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| comment_id required | string | Comment ID from list_comments. |
| decision required | enum | approve or deny. |
Request
curl -X POST https://cotlet.dev/api/v1/teachable/moderate_comment \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"comment_id":"88120431","decision":"approve"}' result = client.call("teachable", "moderate_comment", {"comment_id":"88120431","decision":"approve"}) const result = await cotlet.call('teachable', 'moderate_comment', {"comment_id":"88120431","decision":"approve"}); cotlet call teachable moderate_comment --data '{"comment_id":"88120431","decision":"approve"}' Example response
{
"comment_id": "88120431",
"status": "approved"
}