Summary and recommendation
Thought Industries exposes both a REST API (versioned at v1 under /incoming/api/v1/) and a GraphQL API at /incoming/api/graphql, with authentication via API key passed as the apiKey query parameter or X-API-Key header.
The API key is tenant-scoped and tied to the customer subdomain - there is no shared multi-tenant hostname.
Core user lifecycle operations (create, read, update, delete/deactivate) are available over REST;
some operations may only be available in one interface, so confirm coverage against the GraphQL schema via introspection before committing to an implementation path.
This API surface integrates cleanly into an identity graph that maps users across HR systems, SSO providers, and downstream SaaS tools, enabling authoritative lifecycle events to propagate to Thought Industries without manual intervention.
API quick reference
| Has user API | Yes |
| Auth method | API Key (passed as a query parameter or HTTP header) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Enterprise |
Authentication
Auth method: API Key (passed as a query parameter or HTTP header)
Setup steps
- Log in to your Thought Industries admin portal.
- Navigate to Settings > Integrations > API.
- Generate or copy your API key.
- Include the key in requests as the query parameter
apiKeyor in theX-API-Keyheader.
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string | Unique internal user identifier | system-generated | immutable | |
| string | User's email address (login identifier) | required | supported | Must be unique per tenant | |
| firstName | string | User's first name | optional | supported | |
| lastName | string | User's last name | optional | supported | |
| password | string | User's password (write-only) | optional | supported | Never returned in responses |
| role | string | User role within the platform (e.g., learner, admin) | optional | supported | |
| groups | array | Group memberships for the user | optional | supported | Array of group identifiers |
| customFields | object | Key-value pairs for custom user profile attributes | optional | supported | Field keys defined in admin portal |
| createdAt | datetime | Timestamp of user creation | system-generated | immutable | ISO 8601 format |
| updatedAt | datetime | Timestamp of last user update | system-generated | system-generated | ISO 8601 format |
| active | boolean | Whether the user account is active | optional | supported | Deactivating prevents login |
| ssoId | string | External SSO identifier for the user | optional | supported | Used for SSO-linked accounts |
| avatarUrl | string | URL to user's profile avatar | optional | supported | |
| language | string | Preferred language/locale for the user | optional | supported | BCP 47 language tag |
Core endpoints
List Users
- Method: GET
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/v1/users - Watch out for: Pagination is required for large user sets; default page size is 25.
Request example
GET /incoming/api/v1/users?apiKey=YOUR_KEY&page=1&per_page=25
Response example
{
"users": [
{"id": "abc123", "email": "user@example.com", "firstName": "Jane", "lastName": "Doe", "active": true}
],
"total": 150
}
Get User by ID
- Method: GET
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/v1/users/{id} - Watch out for: Returns 404 if user ID does not exist in the tenant.
Request example
GET /incoming/api/v1/users/abc123?apiKey=YOUR_KEY
Response example
{
"user": {
"id": "abc123",
"email": "user@example.com",
"firstName": "Jane",
"active": true
}
}
Create User
- Method: POST
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/v1/users - Watch out for: Email must be unique; duplicate email returns an error. Password is optional; if omitted, user may need to set one via email invite.
Request example
POST /incoming/api/v1/users?apiKey=YOUR_KEY
Content-Type: application/json
{
"email": "newuser@example.com",
"firstName": "John",
"lastName": "Smith"
}
Response example
{
"user": {
"id": "xyz789",
"email": "newuser@example.com",
"firstName": "John",
"active": true
}
}
Update User
- Method: PUT
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/v1/users/{id} - Watch out for: Full object replacement semantics may apply; confirm with official docs whether PATCH partial update is supported.
Request example
PUT /incoming/api/v1/users/xyz789?apiKey=YOUR_KEY
Content-Type: application/json
{
"firstName": "Jonathan",
"active": false
}
Response example
{
"user": {
"id": "xyz789",
"email": "newuser@example.com",
"firstName": "Jonathan",
"active": false
}
}
Delete User
- Method: DELETE
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/v1/users/{id} - Watch out for: Deletion is typically permanent; deactivating (active: false) is preferred for audit trail retention.
Request example
DELETE /incoming/api/v1/users/xyz789?apiKey=YOUR_KEY
Response example
{
"success": true
}
Add User to Group
- Method: POST
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/v1/groups/{groupId}/users - Watch out for: Group ID must exist; adding a user to a non-existent group returns an error.
Request example
POST /incoming/api/v1/groups/grp001/users?apiKey=YOUR_KEY
Content-Type: application/json
{
"userId": "xyz789"
}
Response example
{
"success": true
}
Get User Enrollments
- Method: GET
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/v1/users/{id}/enrollments - Watch out for: Enrollment data may be paginated for users with many courses.
Request example
GET /incoming/api/v1/users/xyz789/enrollments?apiKey=YOUR_KEY
Response example
{
"enrollments": [
{"courseId": "crs001", "status": "in_progress", "completedAt": null}
]
}
GraphQL User Query
- Method: POST
- URL:
https://{subdomain}.thoughtindustries.com/incoming/api/graphql - Watch out for: GraphQL API is available alongside REST; schema introspection should be used to confirm current field availability.
Request example
POST /incoming/api/graphql?apiKey=YOUR_KEY
Content-Type: application/json
{
"query": "{ users(page: 1) { id email firstName active } }"
}
Response example
{
"data": {
"users": [
{"id": "abc123", "email": "user@example.com", "firstName": "Jane", "active": true}
]
}
}
Rate limits, pagination, and events
Rate limits: Rate limit specifics are not publicly documented in official developer docs.
Rate-limit headers: No
Retry-After header: No
Rate-limit notes: No explicit rate limit tiers, headers, or Retry-After behavior documented publicly. Contact Thought Industries support for current limits.
Pagination method: offset
Default page size: 25
Max page size: 100
Pagination pointer: page / per_page
Webhooks available: Yes
Webhook notes: Thought Industries supports outbound webhooks that fire on platform events. Webhooks are configured in the admin portal under Integrations.
Alternative event strategy: Polling the REST API or GraphQL API for user and enrollment changes if webhooks are not available on a given plan.
Webhook events: user.created, user.updated, user.enrolled, course.completed, assessment.completed
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Enterprise
- Endpoint: Not documented
Limitations:
- Native SCIM provisioning is not documented in official Thought Industries developer docs.
- Enterprise plan may be required for advanced provisioning integrations.
- SSO-based provisioning (JIT) may be available as an alternative via SAML.
Common scenarios
Three scenarios are well-supported by the documented endpoints.
First, provisioning a new learner: POST to /v1/users, capture the returned id, then POST to /v1/courses/{courseId}/enrollments - the course must be published and the user active before enrollment succeeds.
Second, deactivating a departed employee: resolve the user id via GET /v1/users?email={email}, then PUT /v1/users/{id} with {"active": false};
this preserves completion history, whereas DELETE is permanent and removes historical records.
Third, syncing profile updates from an HR system: resolve the user by email, PUT updated fields including customFields, and fall back to POST if the user does not yet exist
note that custom field keys must be pre-configured in the admin portal or writes may be silently ignored.
Pagination defaults to 25 records per page with a maximum of 100;
use page and per_page params for full roster traversal.
Rate limit behavior is not publicly documented - implement exponential backoff as a baseline.
Provision a new learner and enroll in a course
- POST /incoming/api/v1/users with email, firstName, lastName to create the user.
- Capture the returned user id from the response.
- POST /incoming/api/v1/courses/{courseId}/enrollments with the userId to enroll the learner.
- Optionally POST /incoming/api/v1/groups/{groupId}/users to assign the user to the appropriate group.
Watch out for: Course enrollment endpoint requires the course to be published and the user to be active; verify both before calling.
Deactivate a departed employee
- GET /incoming/api/v1/users?email={email} to look up the user by email and retrieve their id.
- PUT /incoming/api/v1/users/{id} with body {"active": false} to deactivate the account.
- Confirm the response returns active: false.
Watch out for: Deactivation prevents login but preserves completion records. Permanent deletion removes historical data.
Sync user profile updates from an external HR system
- On HR system change event, call GET /incoming/api/v1/users?email={email} to resolve the Thought Industries user id.
- If user exists, call PUT /incoming/api/v1/users/{id} with updated fields (firstName, lastName, customFields).
- If user does not exist (404), call POST /incoming/api/v1/users to create a new record.
- Log the API response and handle errors with exponential backoff retry.
Watch out for: Custom fields must match keys pre-configured in the admin portal; unknown keys may be silently ignored or return an error.
Why building this yourself is a trap
The absence of native SCIM is the primary integration caveat: automated provisioning requires direct API orchestration rather than a standards-based SCIM 2.0 handshake, which increases implementation surface and ongoing maintenance burden.
The PUT endpoint for user updates may apply full object replacement semantics - partial PATCH support is unconfirmed, so callers should retrieve the current user object before writing to avoid inadvertently nulling fields. Webhook payload schemas are not fully documented publicly; validate in a sandbox before wiring into production pipelines.
No rate limit headers or Retry-After signals are documented, so there is no programmatic signal to back off - conservative retry logic is mandatory. Finally, the GraphQL and REST APIs coexist but are not feature-equivalent; audit both surfaces before assuming an operation is unavailable.
Automate Thought Industries workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.