Skip to main content
https://.thoughtindustries.com

List Content

Lists content. Omit types[] to get courses and learningPaths only. Pass types[]=courseGroups for catalog rows.

Use it to list catalog rows (types[]=courseGroups) or to search courses and sellable types.

Do not use types[]=courseGroups to sync currency-level prices[]. Price rows are stored by course id (and by sellable id). A catalog row’s id is the course-group id, so that type always returns prices: []. Use top-level priceInCents on catalog rows, or request types[]=courses / a sellable type when you need prices[].

url is not always null on catalog rows. It is null when kind is courseGroup, shareableContentObject, or xApiObject. Other kinds (for example article, video, webinar) build a learn URL from the slug. Course sessions copy kind from their parent group, so a types[]=courses hit can have kind: courseGroup and url: null even though prices[] on that same hit can be populated — id is the course id.

Panorama

A Panorama (client) API key authenticates as the parent company with that client attached (same rate-limit bucket as the company key). This route does not filter on the indexed panorama field. Search filters are built as a client admin, so results are limited to that client’s allocated courses, tags, and licenses. Non-null url values include ?client={clientSlug}. url is still null when kind is courseGroup. Extra scoping is skipped for some client eCommerce settings; that skip was not the case for the Panorama key used to retrieve the sample below.

Pagination

Page size is always 100. Do not pass perPage. The query-string value is forwarded as a GraphQL Int without parsing, so "50" and "100" fail Int coercion and return HTTP 400. The body is the masked processing-error envelope (see Example responses). A server-side clamp that looks like it would cap perPage at 100 is not reachable from this route.

pageInfo is { total, cursor, hasMore }. pageInfo.perPage is computed in the GraphQL resolver but is not selected, so it is absent from the HTTP body. When hasMore is true, pass pageInfo.cursor back as ?cursor=. Cursors are opaque base64 encodings of the next page number (page 2 is Mg). Echo them; do not construct them. On the last page the cursor key is still present, with value null. Branch on hasMore, not on key presence.

Sort

sort is field:direction. Default when omitted: updatedAt:desc. Direction is asc or desc; any other value is treated as desc. An unrecognized field name falls back to updatedAt keeping the direction you passed (sort=bogus:asc sorts updatedAt:asc).

Fields on the content index mapping: updatedAt, createdAt, title, courseStartDate, publishDate, displayDate, meetingStartDate, meetingEndDate. The parser also accepts lastActiveAt, name, label, and parentName, but those names are not on the mapping, so they sort as missing. displayDate is on the mapping but is not written onto courseGroup documents.

Rate limits

Default: 20 requests per 60-second window, counted per company, not per key or IP. Inspect X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix epoch) on every throttled response — X-RateLimit-Limit is the effective cap, which can be higher than 20. Retry-After is set on 429. A 429 body is {"errors": ["Rate limit exceeded, retry in N seconds"]} (N is seconds until the window resets, or the words a few if that duration is missing). Seed backoff from Retry-After.

types[]

Only these seven values are valid. Any other value (types[]=assets) returns HTTP 400 with the same processing-error body as perPage.

ValueWhat it is
coursesCourses (sessions). kind is copied from the parent group.
courseGroupsCatalog grouping for parent/child and session content
learningPathsLearning paths
bundlesSubscriptions in the UI
discountGroupsCollections in the UI
pickableGroupsÀ la carte collections in the UI
productsProducts

Repeat the param (types[]=courseGroups&types[]=learningPaths). If you omit it, the endpoint returns courses and learningPaths only.

To list catalog rows: types[]=courseGroups. Add query=status:published to exclude draft and archived. This route only excludes deleted. Catalog rows return prices: []. url follows kind as described above.

types (no brackets) also works. If you pass both, the bracket-less types value silently wins (types[]=bundles&types=courses searches courses). Use types[] only.

Query syntax

query is an OpenSearch query_string over the whole content document (no field restriction). Terms combine with AND unless you write OR. Leading wildcards do not match.

Bare field:value on keyword fields is case-sensitive. Use <field>.analyzed for a case-insensitive field query (standard analyzer). Custom-field slugs are dasherized. Write the slug directly (level:Intermediate). Do not add a customFields prefix plus a trailing dot; that prefix is stripped once per query, and a second copy is left in place and matches nothing.

sku is indexed on courses and the sellable types, not on courseGroups. Indexed prices are flattened CUR::amount strings: prefix prices:USD*, exact prices:"USD::6500". _prices-exists_:USD is not rewritten on this route (that rewrite needs a preferred currency this call does not pass).

sku:jump_sku, level:Intermediate, and status:published were retrieved on this route. The other rows are syntax, not a live hit.

FormSyntax
Free textcompliance
Field valuesku:jump_sku (not on courseGroups)
Case-insensitive fieldtitle.analyzed:scorm
Custom fieldlevel:Intermediate
Quoted valuelevel:"Very Hard"
Exists_exists_:level
Publishedstatus:published
Has currencyprices:USD*

Malformed queries do not return HTTP 400.

query=title:[ returns HTTP 200 with an empty list:

{
  "pageInfo": {
    "total": 0,
    "cursor": null,
    "hasMore": false
  },
  "contentItems": []
}

An unclosed quote (query=title:") returns HTTP 200 with the GraphQL error envelope (this is the one case that keeps { data: { APIContentSearch } }):

{
  "errors": [
    {
      "message": "A processing error occurred. Please refresh the page and try again.",
      "extensions": {}
    }
  ],
  "data": {
    "APIContentSearch": null
  }
}

URL-encode spaces, quotes, and OR.

Catalog widgets in the admin UI have a similar query box — see Writing a Search Query. That article is not this REST contract (the widget path also strips punctuation that this route does not).

GEThttps://example.thoughtindustries.com/incoming/v2/content

Authenticate with Authorization: Bearer YOUR_API_KEY. This endpoint requires the courses permission. Company keys and Panorama client keys run as admin and pass. A permissioned key whose role has no courses permission returns HTTP 200 with the same processing-error envelope as an unclosed quote (data.APIContentSearch is null). It is not HTTP 403, and the body does not say Forbidden.

Example request

curl "https://example.thoughtindustries.com/incoming/v2/content" \
  -H 'Authorization: Bearer YOUR_API_KEY'

Catalog rows (published course groups):

curl "https://example.thoughtindustries.com/incoming/v2/content?types[]=courseGroups&query=status:published" \
  -H 'Authorization: Bearer YOUR_API_KEY'

SKU search (courses, not course groups):

curl "https://example.thoughtindustries.com/incoming/v2/content?types[]=courses&query=sku:jump_sku" \
  -H 'Authorization: Bearer YOUR_API_KEY'

Parameters

NameTypeRequiredLocationDescription
types[]string[]NoquerySee types[]. Default when omitted: courses, learningPaths.
typesstringNoqueryIf both types and types[] are present, types wins. Use types[] only.
cursorstringNoqueryToken from pageInfo.cursor. On the last page that field is null.
perPagequeryDo not pass. Query-string values return HTTP 400. Size is always 100.
sortstringNoqueryfield:direction. See Sort. Default: updatedAt:desc.
querystringNoqueryOpenSearch query_string. See Query syntax.

A successful HTTP 200 is the unwrapped { pageInfo, contentItems } object. The bodies below were retrieved with GET /incoming/v2/content. Array examples that are not a complete HTTP body are labeled as a single contentItems element from that request.

Example responses

Permissioned key with no courses permission — HTTP 200, same envelope as query=title:":

{
  "errors": [
    {
      "message": "A processing error occurred. Please refresh the page and try again.",
      "extensions": {}
    }
  ],
  "data": {
    "APIContentSearch": null
  }
}

HTTP 400 for perPage=50, perPage=100, and types[]=assets (same body; no data wrapper):

{
  "errors": [
    {
      "message": "A processing error occurred. Please refresh the page and try again.",
      "extensions": {}
    }
  ]
}

Complete body for types[]=courses&query=sku:jump_sku (one hit; kind is video because sessions copy the parent group’s kind; priceInCents is 5500 while prices[].unitAmount is 1000):

{
  "pageInfo": {
    "total": 1,
    "cursor": null,
    "hasMore": false
  },
  "contentItems": [
    {
      "id": "c072e080-2bf8-475f-af04-d0fa66dbb477",
      "createdAt": "2020-04-20T18:20:12.568Z",
      "updatedAt": "2026-03-23T14:18:05.958Z",
      "hasChildren": false,
      "courseStartDate": "2020-04-20T18:20:12.399Z",
      "courseEndDate": null,
      "enrollmentStartDate": "2020-04-20T18:20:12.399Z",
      "enrollmentEndDate": null,
      "kind": "video",
      "language": "en",
      "contentTypeLabel": "Video",
      "asset": "https://d36ai2hkxl16us.cloudfront.net/thoughtindustries/image/upload/v1/course-uploads/e8ed2401-7fa3-40bc-9a2d-b80e2caa6d80/tunnroztexjb-photo-of-woman-wearing-black-printed-leggings-3758148.jpg",
      "assetAltText": "",
      "title": "Jump backs",
      "slug": "jump-backs",
      "description": "Learn how to properly jump back from Downward Dog to plank ",
      "metaTitle": null,
      "metaDescription": null,
      "sku": "jump_sku",
      "customFields": {
        "level": "Intermediate"
      },
      "authorsAndInstructors": [],
      "seatsLimit": null,
      "enrollmentCount": 8,
      "status": "published",
      "source": null,
      "freeWithRegistration": false,
      "priceInCents": 5500,
      "suggestedRetailPriceInCents": null,
      "waitlistingEnabled": false,
      "waitlistingTriggered": false,
      "waitlistCount": null,
      "url": "https://www.momopeach.fun/learn/video/jump-backs",
      "tags": [
        {
          "id": "fd19006e-b5f5-5bea-b681-3645bb00c36e",
          "label": "transitions"
        },
        {
          "id": "14c69a60-7d1e-53cb-af81-81b44abc2cb1",
          "label": "intermediate"
        }
      ],
      "prices": [
        {
          "currencyCode": "USD",
          "unitAmount": 1000,
          "suggestedRetailUnitAmount": null,
          "instructorAccessUnitAmount": null,
          "annualUnitAmount": null,
          "bulkPurchasingEnabled": false,
          "bulkPurchaseTiers": null,
          "seatTiers": [],
          "seatPackages": [],
          "locale": "en_US",
          "isDefault": true
        }
      ]
    }
  ]
}

pageInfo from types[]=courseGroups&query=status:published when that list fits in one page (cursor is present and null):

{
  "total": 17,
  "cursor": null,
  "hasMore": false
}

One contentItems element from that same catalog request (kind: courseGroup: prices is [], url is null, enrollment/seats/waitlist/sku are null, priceInCents is 15000):

{
  "id": "4f21c50e-0434-4b59-a720-e74cca43f3d7",
  "createdAt": "2020-04-16T16:24:10.058Z",
  "updatedAt": "2024-11-13T15:19:54.414Z",
  "hasChildren": false,
  "courseStartDate": "2020-04-16T16:24:10.044Z",
  "courseEndDate": null,
  "enrollmentStartDate": null,
  "enrollmentEndDate": null,
  "kind": "courseGroup",
  "language": "en",
  "contentTypeLabel": "Course",
  "asset": "https://d36ai2hkxl16us.cloudfront.net/thoughtindustries/image/upload/v1/course-uploads/e8ed2401-7fa3-40bc-9a2d-b80e2caa6d80/ogwcejr9aa5f-woman-with-arms-outstretched-against-blue-sky-317155.jpg",
  "assetAltText": null,
  "title": "A Step by Step Guide for Preparing for Handstands",
  "slug": "a-step-by-step-guide-for-preparing-for-handstands",
  "description": "Form a strong foundation to eventually achieve the full expression of the Handstand (Adho Mukha Vrksasana) pose.",
  "metaTitle": "yoga handstands",
  "metaDescription": null,
  "sku": null,
  "customFields": {
    "level": "Intermediate",
    "instructor": "Sarah Girard"
  },
  "authorsAndInstructors": [],
  "seatsLimit": null,
  "enrollmentCount": 0,
  "status": "published",
  "source": null,
  "freeWithRegistration": false,
  "priceInCents": 15000,
  "suggestedRetailPriceInCents": null,
  "waitlistingEnabled": false,
  "waitlistingTriggered": false,
  "waitlistCount": null,
  "url": null,
  "tags": [
    {
      "id": "b143bcb0-511b-5829-a747-d374223b0674",
      "label": "handstands"
    },
    {
      "id": "55deb92b-3f58-5299-aa36-6f96de4f845f",
      "label": "inversions"
    },
    {
      "id": "14c69a60-7d1e-53cb-af81-81b44abc2cb1",
      "label": "intermediate"
    }
  ],
  "prices": []
}

One contentItems element from the same catalog request with kind: article (url is set; prices is still []):

{
  "id": "3a2e5bb4-a961-4725-9824-e3008911cebd",
  "createdAt": "2020-04-16T16:40:02.895Z",
  "updatedAt": "2026-03-23T15:58:14.612Z",
  "hasChildren": false,
  "courseStartDate": "2020-04-16T16:40:02.867Z",
  "courseEndDate": null,
  "enrollmentStartDate": null,
  "enrollmentEndDate": null,
  "kind": "article",
  "language": "en",
  "contentTypeLabel": "Article",
  "asset": "https://d36ai2hkxl16us.cloudfront.net/thoughtindustries/image/upload/v1/course-uploads/e8ed2401-7fa3-40bc-9a2d-b80e2caa6d80/uaet42c8bj4k-woman-meditating-in-the-outdoors-29081751.jpg",
  "assetAltText": null,
  "title": "8 Seated Poses for Finding Your Focus",
  "slug": "8-seated-poses-for-finding-your-focus",
  "description": "Try this quick, centering sequence to heal a monkey mind and boost relaxation.",
  "metaTitle": null,
  "metaDescription": null,
  "sku": null,
  "customFields": {},
  "authorsAndInstructors": [],
  "seatsLimit": null,
  "enrollmentCount": 0,
  "status": "published",
  "source": null,
  "freeWithRegistration": false,
  "priceInCents": null,
  "suggestedRetailPriceInCents": null,
  "waitlistingEnabled": false,
  "waitlistingTriggered": false,
  "waitlistCount": null,
  "url": "https://www.momopeach.fun/learn/article/8-seated-poses-for-finding-your-focus",
  "tags": [
    {
      "id": "080df127-1d7a-5a74-b55f-c394ef46dde5",
      "label": "seated poses"
    },
    {
      "id": "d1c3e1e1-728e-5cb0-bc3c-b7c8f60df2b2",
      "label": "beginner"
    }
  ],
  "prices": []
}

One contentItems element from types[]=courses (kind: courseGroup because the session copied the parent group’s kind; url is null; prices[] is populated; enrollmentStartDate is set; assetAltText is ""):

{
  "id": "33a416e3-eb9f-474a-b553-1f5bba862a7b",
  "createdAt": "2025-05-12T18:48:41.913Z",
  "updatedAt": "2026-06-26T10:27:46.798Z",
  "hasChildren": false,
  "courseStartDate": "2026-04-27T04:00:00.000Z",
  "courseEndDate": null,
  "enrollmentStartDate": "2026-04-27T04:00:00.000Z",
  "enrollmentEndDate": null,
  "kind": "courseGroup",
  "language": "en",
  "contentTypeLabel": "Course",
  "asset": null,
  "assetAltText": "",
  "title": "Free Content ",
  "slug": "free-content",
  "description": null,
  "metaTitle": null,
  "metaDescription": null,
  "sku": null,
  "customFields": {},
  "authorsAndInstructors": [],
  "seatsLimit": null,
  "enrollmentCount": 5,
  "status": "published",
  "source": null,
  "freeWithRegistration": false,
  "priceInCents": null,
  "suggestedRetailPriceInCents": null,
  "waitlistingEnabled": false,
  "waitlistingTriggered": false,
  "waitlistCount": null,
  "url": null,
  "tags": [
    {
      "id": "215251a7-d5e4-4d0b-938d-194616e90f03",
      "label": "Group 1"
    }
  ],
  "prices": [
    {
      "currencyCode": "USD",
      "unitAmount": 2000,
      "suggestedRetailUnitAmount": null,
      "instructorAccessUnitAmount": null,
      "annualUnitAmount": null,
      "bulkPurchasingEnabled": true,
      "bulkPurchaseTiers": null,
      "seatTiers": [
        {
          "seats": 10,
          "unitAmount": 2000
        },
        {
          "seats": 20,
          "unitAmount": 1500
        },
        {
          "seats": 21,
          "unitAmount": 1000
        }
      ],
      "seatPackages": [
        {
          "seats": 10
        },
        {
          "seats": 20
        }
      ],
      "locale": "en_US",
      "isDefault": true
    }
  ]
}

One contentItems element from types[]=courses with a Panorama client key. url includes ?client=; kind: courseGroup on the same response still had url: null.

{
  "id": "2f080751-24dd-413e-879f-585127975f21",
  "createdAt": "2020-04-16T16:40:02.895Z",
  "updatedAt": "2026-03-23T15:58:14.612Z",
  "hasChildren": false,
  "courseStartDate": "2020-04-16T16:40:02.867Z",
  "courseEndDate": null,
  "enrollmentStartDate": "2020-04-16T16:40:02.867Z",
  "enrollmentEndDate": null,
  "kind": "article",
  "language": "en",
  "contentTypeLabel": "Article",
  "asset": "https://d36ai2hkxl16us.cloudfront.net/thoughtindustries/image/upload/v1/course-uploads/e8ed2401-7fa3-40bc-9a2d-b80e2caa6d80/uaet42c8bj4k-woman-meditating-in-the-outdoors-29081751.jpg",
  "assetAltText": "",
  "title": "8 Seated Poses for Finding Your Focus",
  "slug": "8-seated-poses-for-finding-your-focus",
  "description": "Try this quick, centering sequence to heal a monkey mind and boost relaxation.",
  "metaTitle": null,
  "metaDescription": null,
  "sku": null,
  "customFields": {},
  "authorsAndInstructors": [],
  "seatsLimit": null,
  "enrollmentCount": 6,
  "status": "published",
  "source": null,
  "freeWithRegistration": false,
  "priceInCents": null,
  "suggestedRetailPriceInCents": null,
  "waitlistingEnabled": false,
  "waitlistingTriggered": false,
  "waitlistCount": null,
  "url": "https://www.momopeach.fun/learn/article/8-seated-poses-for-finding-your-focus?client=sub-separation-test",
  "tags": [
    {
      "id": "080df127-1d7a-5a74-b55f-c394ef46dde5",
      "label": "seated poses"
    },
    {
      "id": "d1c3e1e1-728e-5cb0-bc3c-b7c8f60df2b2",
      "label": "beginner"
    }
  ],
  "prices": [
    {
      "currencyCode": "USD",
      "unitAmount": 3500,
      "suggestedRetailUnitAmount": null,
      "instructorAccessUnitAmount": null,
      "annualUnitAmount": null,
      "bulkPurchasingEnabled": false,
      "bulkPurchaseTiers": null,
      "seatTiers": [],
      "seatPackages": [],
      "locale": "en_US",
      "isDefault": true
    }
  ]
}

Response fields

Hits come from the content index, plus a SQL overlay (id, sku, assetAltText for every type; enrollment, seats, and waitlist fields only for courses). GraphQL Content resolvers then run. Selected fields are exactly the GraphQL selection on this route; fields on the GraphQL Content type that are not listed here are not returned.

FieldTypeBehavior
pageInfo.totalintegerHit count. 0 when there are no hits (including query=title:[).
pageInfo.cursorstring or nullNext-page token, or null when hasMore is false. The key is still present.
pageInfo.hasMorebooleanWhether another page exists.
contentItemsarrayMay be empty.
contentItems.iduuidIndex id, replaced by the SQL overlay when the row exists.
contentItems.createdAtISO 8601 or nullDate scalar.
contentItems.updatedAtISO 8601 or nullDate scalar.
contentItems.hasChildrenbooleanNever null (false if unset). Written on courseGroup documents when the group has more than one course.
contentItems.courseStartDateISO 8601 or nullOn courseGroups, taken from the display course.
contentItems.courseEndDateISO 8601 or nullOn courseGroups, taken from the display course.
contentItems.enrollmentStartDateISO 8601 or nullSQL overlay for courses only. Always null on courseGroups.
contentItems.enrollmentEndDateISO 8601 or nullSame. Always null on courseGroups.
contentItems.kindstringSingular. Course sessions copy the parent group’s kind (courseGroup, article, video, …). Sellable hits whose indexed kind is sellable are rewritten to the singular index type (bundle, learningPath, …). If missing, the resolver returns courseGroup. Not the same as the plural types[] filter.
contentItems.languagestring or nullNullable.
contentItems.contentTypeLabelstringIndexed contentType, or "Course" if missing. Never null.
contentItems.assetstring or nullNullable.
contentItems.assetAltTextstring or nullNullable. For courses, empty string when the parent course group has no alt text. On courseGroups, null or "".
contentItems.titlestring or nulltitle, else name, else null.
contentItems.slugstringGraphQL Slug! (a-z, 0-9, hyphen; empty string allowed).
contentItems.descriptionstring or nullNullable.
contentItems.metaTitlestring or nullNullable.
contentItems.metaDescriptionstring or nullNullable.
contentItems.skustring or nullIndexed on courses and sellable types, not on courseGroup documents. The SQL overlay still selects sku for every type, so a courseGroup row can return a value if that column is set on the course group.
contentItems.customFieldsobject or nullEach slug maps to a string, or to a string array when the field is configured as multiple (a single selected value is still an array). Some companies treat every custom field as multiple. Often {}.
contentItems.authorsAndInstructorsstring[]Never null. Indexed authors, plus organizer and co-organizer emails for webinar sessions.
contentItems.seatsLimitinteger or nullSQL overlay for courses only. Always null on courseGroups.
contentItems.enrollmentCountintegerseatsAllocatedCount or 0. Never null. On courseGroups this is 0 (the count is not on the group document).
contentItems.statusstringdraft, authoring, published, loginRestriction, archived, pending, or deleted. Deleted items are excluded from results.
contentItems.sourcestring or nullNullable.
contentItems.freeWithRegistrationboolean or nullOn courseGroups, from the display course (false when there is none).
contentItems.priceInCentsinteger or nullOn courseGroups, from the display course. This is the price catalog rows actually carry.
contentItems.suggestedRetailPriceInCentsinteger or nullOn courseGroups, from the display course.
contentItems.waitlistingEnabledbooleanNever null (false if unset). Written on course documents, not on courseGroup documents.
contentItems.waitlistingTriggeredbooleanNever null. SQL overlay for courses only; false on courseGroups.
contentItems.waitlistCountinteger or nullSQL overlay for courses only. Always null on courseGroups.
contentItems.urlstring or nullAlways null when kind is courseGroup, shareableContentObject, or xApiObject. Otherwise https://{instance}/learn/{path}/{slug} when slug is present. Path is learning-path, article, video, webinars, event, or course. A Panorama key adds ?client={clientSlug}.
contentItems.tagsarrayEmpty array if none. Selected subfields: id, label only.
contentItems.pricesarray[] when the company does not have eCommerce price objects enabled. [] for types[]=courseGroups (loader is the course-prices table, keyed by this row’s id). Can be populated for types[]=courses even when kind is courseGroup, because id is the course id. Populated for sellable kinds (learningPath, bundle, discountGroup, pickableGroup, product) when price rows exist. Entries whose currency is missing from the company’s eCommerce currency configuration are omitted.
contentItems.prices.currencyCodestringNon-null on a prices[] entry that survived the currency merge.
contentItems.prices.unitAmountinteger or nullDo not treat as the source of truth versus top-level priceInCents.
contentItems.prices.suggestedRetailUnitAmountinteger or nullNullable.
contentItems.prices.instructorAccessUnitAmountinteger or nullNullable.
contentItems.prices.annualUnitAmountinteger or nullNullable.
contentItems.prices.bulkPurchasingEnabledboolean or nullNullable.
contentItems.prices.bulkPurchaseTiersarray or nullArray when present, not a single object. Selected subfields only.
contentItems.prices.bulkPurchaseTiers.seatTiersarray or nullSelected: seats, unitAmount.
contentItems.prices.bulkPurchaseTiers.seatPackagesarray or nullSelected: seats.
contentItems.prices.bulkPurchaseTiers.lastTierPriceUnitAmountinteger or nullNullable.
contentItems.prices.seatTiersarray or nullnull, [], or populated. Selected: seats, unitAmount.
contentItems.prices.seatPackagesarray or nullnull, [], or populated. Selected: seats.
contentItems.prices.localestringNon-null on a prices[] entry that survived the currency merge.
contentItems.prices.isDefaultbooleanNon-null on a prices[] entry that survived the currency merge.