Creating Courses via API
End-to-end guide to programmatic course creation
Creating courses via the REST API is one of the most common integration patterns. Whether you're syncing content from an external CMS, automating course provisioning for new clients, or building a self-service content pipeline, this tutorial walks you through everything from the minimal request to production-grade patterns.
Prerequisites: You'll need a valid API key with write permissions. Find your key in Settings → API Keys in the admin panel. All requests use the incoming/v2 endpoint prefix.
A 5-minute course on Thought Industries Academy covering the basics of API fundamentals.
Minimal Course Creation
At minimum, you need a title and courseType. This creates a draft course with an auto-generated slug.
curl -X "POST" "https://{{DOMAIN}}.thoughtindustries.com/incoming/v2/courses" \
-H 'Authorization: Bearer {{APIKEY}}' \
-H 'Content-Type: application/json' \
-d '{
"title": "Introduction to Platform APIs",
"courseType": "course"
}'The response returns the full course object with a generated id and slug. Store the id — you'll need it for updates, enrollments, and content management.
Field Reference
The course creation endpoint accepts the following fields. Required fields are highlighted — all others are optional and can be set later via update.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Display name of the course shown to learners. Max 255 characters. Must be unique within your instance. |
courseType | string | Yes | The type of learning content. Accepted values: "course", "learning_path", "webinar", "video", "article", "scorm", "xapi". |
slug | string | No | URL-friendly identifier. Auto-generated from title if omitted. Must be unique. Lowercase, hyphens only. e.g. intro-platform-apis |
description | string | No | Rich-text description displayed on the course detail page. Supports basic HTML tags. |
publishedStatus | string | No | Controls visibility of the course. Accepted values: "draft" (default), "published", "archived". |
authors | string[] | No | Array of author display names. Purely cosmetic — does not create or link user accounts. |
tags | string[] | No | Tags for categorization and filtering. New tags are created automatically if they don't exist. |
externalId | string | No | Your system's unique identifier for this course. Useful for syncing with external systems. Must be unique if provided. |
customFields | object | No | Key-value pairs for custom metadata. Keys must match custom fields configured in your admin panel. |
isFree | boolean | No | Whether the course is free to access. Defaults to false. If true, learners can self-enroll without payment. |
source | string | No | Attribution label for where the content originated. e.g. 'partner-content', 'internal-training' |
Full Example with All Fields
Here's a production-ready request using most available fields. This creates a draft course with metadata, tags, authors, and custom fields.
curl -X "POST" "https://{{DOMAIN}}.thoughtindustries.com/incoming/v2/courses" \
-H 'Authorization: Bearer {{APIKEY}}' \
-H 'Content-Type: application/json' \
-d '{
"title": "Advanced Integration Patterns",
"courseType": "course",
"slug": "advanced-integration-patterns",
"description": "<p>Learn how to build robust, production-grade integrations with the Thought Industries platform.</p>",
"publishedStatus": "draft",
"authors": ["Jane Doe", "John Smith"],
"tags": ["integration", "advanced", "api"],
"externalId": "EXT-COURSE-042",
"isFree": false,
"customFields": {
"difficulty": "advanced",
"estimatedHours": 4
}
}'Success Response
A successful creation returns 201 Created with the full course object, including the generated id and timestamps.
{
"id": "crs_a1b2c3d4e5",
"title": "Advanced Integration Patterns",
"slug": "advanced-integration-patterns",
"courseType": "course",
"publishedStatus": "draft",
"description": "<p>Learn how to build robust, production-grade integrations with the Thought Industries platform.</p>",
"authors": ["Jane Doe", "John Smith"],
"tags": ["integration", "advanced", "api"],
"externalId": "EXT-COURSE-042",
"isFree": false,
"customFields": {
"difficulty": "advanced",
"estimatedHours": 4
},
"createdAt": "2026-03-13T14:22:00.000Z",
"updatedAt": "2026-03-13T14:22:00.000Z"
}Validation Rules
The API enforces several validation rules. Understanding these will help you build resilient integrations that handle edge cases gracefully.
title is required
Every course must have a non-empty title. Max 255 characters.
courseType is required
Must be one of: "course", "learning_path", "webinar", "video", "article", "scorm", "xapi".
slug must be unique
If provided, the slug must not already exist. If omitted, one is generated from the title.
externalId must be unique
If provided, no other course can share the same externalId.
customFields keys must exist
Custom field keys must match fields configured in the admin panel. Unknown keys are silently ignored.
publishedStatus defaults to draft
If omitted, the course is created as a draft and will not be visible to learners.
HTML in description
The description field accepts basic HTML (<p>, <strong>, <em>, <ul>, <li>, <a>). Script tags are stripped.
tags are auto-created
If a tag doesn't exist, it's created automatically. Tags are case-insensitive and trimmed.
Error Responses
When validation fails, the API returns a structured error with a descriptive message and error code. Here are the most common errors:
Missing required field400
{
"error": "Validation Error",
"message": "Field 'title' is required.",
"code": "MISSING_REQUIRED_FIELD",
"statusCode": 400
}Duplicate slug422
{
"error": "Validation Error",
"message": "A course with slug 'advanced-integration-patterns' already exists.",
"code": "DUPLICATE_SLUG",
"statusCode": 422
}Invalid courseType value400
{
"error": "Validation Error",
"message": "Invalid courseType 'quiz'. Accepted values: course, learning_path, webinar, video, article, scorm, xapi.",
"code": "INVALID_FIELD_VALUE",
"statusCode": 400
}Updating After Creation
After creating a course, you'll often want to update it — publish it, add tags, or modify metadata. Use a PUT request with the course ID. Only include the fields you want to change.
curl -X "PUT" "https://{{DOMAIN}}.thoughtindustries.com/incoming/v2/courses/crs_a1b2c3d4e5" \
-H 'Authorization: Bearer {{APIKEY}}' \
-H 'Content-Type: application/json' \
-d '{
"publishedStatus": "published",
"tags": ["integration", "advanced", "api", "featured"]
}'Common Patterns
CMS Sync
Use externalId to map courses to your CMS. On sync, check if the externalId exists — create if new, update if changed.
Client Onboarding
Create a template set of courses for each new client. Use tags to group them and customFields for client-specific metadata.
Content Pipeline
Create courses as drafts, add content via subsequent API calls, then publish programmatically when ready.
Bulk Import
Loop through your content records, creating courses sequentially. Respect rate limits (100 req/min) and handle 429 responses with exponential backoff.