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
Teachable Endpoints
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.

POST 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

Parameters: list_courses
NameTypeDescription
published boolean Only published courses. Default: all.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/teachable/list_courses \
  -H "Authorization: Bearer $COTLET_API_KEY"
Python
result = client.call("teachable", "list_courses")
Node.js
const result = await cotlet.call('teachable', 'list_courses');
CLI
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"
    }
  ]
}

POST 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

Parameters: get_curriculum
NameTypeDescription
course_id required string Course ID from list_courses.

Request

cURL
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"}'
Python
result = client.call("teachable", "get_curriculum", {"course_id":"course_8c41d2"})
Node.js
const result = await cotlet.call('teachable', 'get_curriculum', {"course_id":"course_8c41d2"});
CLI
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

read

POST 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

Parameters: get_lesson
NameTypeDescription
course_id required string Course ID from list_courses.
lesson_id required string Lesson ID from get_curriculum.

Request

cURL
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"}'
Python
result = client.call("teachable", "get_lesson", {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"})
Node.js
const result = await cotlet.call('teachable', 'get_lesson', {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"});
CLI
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"
    }
  ]
}

POST 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

Parameters: list_students
NameTypeDescription
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
curl -X POST https://cotlet.dev/api/v1/teachable/list_students \
  -H "Authorization: Bearer $COTLET_API_KEY"
Python
result = client.call("teachable", "list_students")
Node.js
const result = await cotlet.call('teachable', 'list_students');
CLI
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
}

POST 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

Parameters: list_comments
NameTypeDescription
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
curl -X POST https://cotlet.dev/api/v1/teachable/list_comments \
  -H "Authorization: Bearer $COTLET_API_KEY"
Python
result = client.call("teachable", "list_comments")
Node.js
const result = await cotlet.call('teachable', 'list_comments');
CLI
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
}

POST 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

Parameters: list_coupons
NameTypeDescription
include_archived boolean Also list archived coupons. Default: false.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/teachable/list_coupons \
  -H "Authorization: Bearer $COTLET_API_KEY"
Python
result = client.call("teachable", "list_coupons")
Node.js
const result = await cotlet.call('teachable', 'list_coupons');
CLI
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"
    }
  ]
}

POST 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

Parameters: create_lesson
NameTypeDescription
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
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"}'
Python
result = client.call("teachable", "create_lesson", {"course_id":"course_8c41d2","section_id":"section_8c41d2","title":"title"})
Node.js
const result = await cotlet.call('teachable', 'create_lesson', {"course_id":"course_8c41d2","section_id":"section_8c41d2","title":"title"});
CLI
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
}

Update lesson

write Confirm

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

Parameters: update_lesson
NameTypeDescription
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
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."}'
Python
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."})
Node.js
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."});
CLI
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"
}

Publish lesson

write Confirm

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

Parameters: publish_lesson
NameTypeDescription
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
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"}'
Python
result = client.call("teachable", "publish_lesson", {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"})
Node.js
const result = await cotlet.call('teachable', 'publish_lesson', {"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"});
CLI
cotlet call teachable publish_lesson --data '{"course_id":"course_8c41d2","lesson_id":"lesson_8c41d2"}'

Example response

{
  "lesson_id": "35120874",
  "published": true
}

Create coupon

write Confirm

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

Parameters: create_coupon
NameTypeDescription
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
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}'
Python
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})
Node.js
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});
CLI
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"
}

Archive coupon

write Confirm

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

Parameters: archive_coupon
NameTypeDescription
coupon_id required string Coupon ID from list_coupons.

Request

cURL
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"}'
Python
result = client.call("teachable", "archive_coupon", {"coupon_id":"coupon_8c41d2"})
Node.js
const result = await cotlet.call('teachable', 'archive_coupon', {"coupon_id":"coupon_8c41d2"});
CLI
cotlet call teachable archive_coupon --data '{"coupon_id":"coupon_8c41d2"}'

Example response

{
  "coupon_id": "3917702",
  "code": "SUMMER20",
  "status": "archived"
}

Enroll student

write Confirm

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

Parameters: enroll_student
NameTypeDescription
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
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"}'
Python
result = client.call("teachable", "enroll_student", {"email":"jane@example.com","course_id":"course_8c41d2"})
Node.js
const result = await cotlet.call('teachable', 'enroll_student', {"email":"jane@example.com","course_id":"course_8c41d2"});
CLI
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
}

Send email

write Confirm

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

Parameters: send_email
NameTypeDescription
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
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"}'
Python
result = client.call("teachable", "send_email", {"to":"users","subject":"subject","body_markdown":"body markdown"})
Node.js
const result = await cotlet.call('teachable', 'send_email', {"to":"users","subject":"subject","body_markdown":"body markdown"});
CLI
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"
}

Reply to comment

write Confirm

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

Parameters: reply_to_comment
NameTypeDescription
comment_id required string Comment ID from list_comments.
body required string Reply text.

Request

cURL
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"}'
Python
result = client.call("teachable", "reply_to_comment", {"comment_id":"comment_8c41d2","body":"body"})
Node.js
const result = await cotlet.call('teachable', 'reply_to_comment', {"comment_id":"comment_8c41d2","body":"body"});
CLI
cotlet call teachable reply_to_comment --data '{"comment_id":"comment_8c41d2","body":"body"}'

Example response

{
  "reply_id": "88120502",
  "comment_id": "88120431",
  "status": "published"
}

Moderate comment

write Confirm

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

Parameters: moderate_comment
NameTypeDescription
comment_id required string Comment ID from list_comments.
decision required enum approve or deny.

Request

cURL
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"}'
Python
result = client.call("teachable", "moderate_comment", {"comment_id":"88120431","decision":"approve"})
Node.js
const result = await cotlet.call('teachable', 'moderate_comment', {"comment_id":"88120431","decision":"approve"});
CLI
cotlet call teachable moderate_comment --data '{"comment_id":"88120431","decision":"approve"}'

Example response

{
  "comment_id": "88120431",
  "status": "approved"
}