Create Alternate Purchase Path
Create an alternate purchase path on a content item. TI item or external URL target.
Creates one alternate purchase path on a content item. Use this for a single write. To replace the full set across many courses, use Bulk replace alternate purchase paths.
label is rich-text HTML. The server sanitizes it on write to a, b, strong, i,
em, u, br, p, and span. Links keep http/https only and are rewritten to
target="_blank" with rel="noopener noreferrer". Disallowed tags are stripped; their
text is kept.
Exactly one target kind is stored: ti_item (an active, priced course, learning path,
subscription, or collection) or external_url (http/https plus targetUrlTitle).
https://example.thoughtindustries.com/v3/admin/content/:contentId/alternate-purchase-pathsThis endpoint is served by the v3 API. Its full path is
https://{instance}.thoughtindustries.com/v3/admin/content/{contentId}/alternate-purchase-paths, not the/incoming/v2base URL used by the rest of this reference. Use the full URL shown in the examples below.
Example request — TI item target
curl -X POST "https://{instance}.thoughtindustries.com/v3/admin/content/3a131ac3-1a74-420d-b4da-ae10b18b2c68/alternate-purchase-paths" \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"label": "Subscribe for unlimited access",
"targetType": "ti_item",
"targetId": "7a6b5c4d-3e2f-1a0b-9c8d-7e6f5a4b3c2d",
"hidePrimaryCta": false
}'Example request — external URL target
curl -X POST "https://{instance}.thoughtindustries.com/v3/admin/content/3a131ac3-1a74-420d-b4da-ae10b18b2c68/alternate-purchase-paths" \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"label": "Buy from our store",
"targetType": "external_url",
"targetUrl": "https://example.com/checkout",
"targetUrlTitle": "Example Store",
"hidePrimaryCta": true
}'Parameters
Requires the settings.alternatePurchasePaths permission. A company API key satisfies this.
| Name | Type | Required | Location | Description |
|---|---|---|---|---|
contentId | uuid | Yes | path | Source course UUID. Learning path IDs are rejected (404). |
label | string | Yes | body | CTA label. Maximum 2000 characters after sanitize. |
targetType | string | Yes | body | ti_item or external_url. |
targetId | uuid | When ti_item | body | Target item UUID. Must be purchasable (active + priced). |
targetUrl | url | When external_url | body | http or https only. Maximum 2048 characters. Reachability is not checked. |
targetUrlTitle | string | When external_url | body | Display title. Maximum 255 characters. Must be empty for ti_item. |
displayOrder | integer | No | body | Sort key. Default 0. Must be a non-negative integer. |
hidePrimaryCta | boolean | No | body | Default false. |
targetId kinds the write will accept: course, learningPath, subscription
(any purchasable, priced bundle in ti.bundles), and collection (a purchasable,
priced discount group). Confirm a candidate with
Filter purchasable targets
before attaching it.
Example response (201)
Note the data wrapper — v3 endpoints nest their payload under data.
{
"data": {
"id": "443d8182-d461-4568-a777-918ca729fd85",
"courseId": "3a131ac3-1a74-420d-b4da-ae10b18b2c68",
"displayOrder": 0,
"label": "Subscribe for unlimited access",
"targetType": "ti_item",
"targetId": "7a6b5c4d-3e2f-1a0b-9c8d-7e6f5a4b3c2d",
"targetUrl": null,
"targetUrlTitle": null,
"hidePrimaryCta": false,
"createdAt": "2026-09-15T14:22:01.000Z",
"updatedAt": "2026-09-15T14:22:01.000Z"
}
}Errors
Cross-field validation (wrong target pairing, empty label after sanitize, or a
non-purchasable targetId) returns 400:
{
"code": "VALIDATION_ERROR",
"message": "Invalid alternate purchase path",
"category": "VALIDATION",
"timestamp": "2026-09-15T14:22:01.000Z",
"statusCode": 400,
"metadata": {
"errors": [
"targetId 7a6b5c4d-3e2f-1a0b-9c8d-7e6f5a4b3c2d does not resolve to a purchasable item (an active, priced course, learning path, subscription, or collection)"
]
}
}An unknown or cross-company contentId returns 404 with
message: "Content item with identifier \"…\" was not found" — see
List alternate purchase paths.
Malformed DTO values — a non-enum targetType, a non-UUID targetId, a non-http(s)
targetUrl, an over-length field, or an unrecognized property (whitelist +
forbidNonWhitelisted are on) — are rejected by the global ValidationPipe as 400
with code: "BAD_REQUEST" and category: "CLIENT_ERROR". The class-validator messages
are surfaced: message is the failed constraints joined with ; , and
metadata.errors lists them individually.
{
"code": "BAD_REQUEST",
"message": "targetType must be one of the following values: ti_item, external_url",
"category": "CLIENT_ERROR",
"timestamp": "2026-09-15T14:22:01.000Z",
"statusCode": 400,
"metadata": {
"errors": [
"targetType must be one of the following values: ti_item, external_url"
]
}
}A missing or invalid API key returns 401 with code: "UNAUTHENTICATED". A manager
session without settings.alternatePurchasePaths returns 403 with code: "FORBIDDEN"
and message: "Insufficient permissions: settings.alternatePurchasePaths required".
Rate limit
60 requests per 60 seconds, counted per instance. This limit is specific to this endpoint
and is independent of the per-family rate limits that apply to /incoming/v2 endpoints.