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

Bulk Sublicense Change

Enqueue an asynchronous bulk add/remove of sublicenses for a list of users, without changing their Panorama.

Adds and/or removes sublicenses for a set of users without changing their Panorama. Each entry in users carries its own licenseIdsToAdd / licenseIdsToRemove, so one call can apply a different license delta to each learner. The work runs asynchronously as a background job — the request returns 202 Accepted with a job ID to poll.

POSThttps://example.thoughtindustries.com/v3/users/bulk-sublicense-assignment

This endpoint is served by the v3 API. Its full path is https://{instance}.thoughtindustries.com/v3/users/bulk-sublicense-assignment, not the /incoming/v2 base URL used by most of this reference. Use the full URL shown in the examples below.

Feature availability. This endpoint is part of the Panorama migration feature set and requires Advanced Clients (Panorama) to be enabled on your instance. If it returns 403/404, the feature is not yet enabled — contact your Thought Industries representative.

Example request — add and remove sublicenses

curl -X POST "https://{instance}.thoughtindustries.com/v3/users/bulk-sublicense-assignment" \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "users": [
    {
      "id": "3f9a2b18-7c4d-4e2a-9b1f-0c5d6e7f8a90",
      "licenseIdsToAdd": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
      "licenseIdsToRemove": ["b2c3d4e5-f6a7-8901-bcde-f23456789012"]
    },
    {
      "email": "[email protected]",
      "licenseIdsToAdd": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
    }
  ],
  "enrollmentOptions": {
    "preserveEnrollments": true,
    "preserveProgress": true
  }
}'

Example request — remove a sublicense (enrollmentOptions required)

When any user in the batch removes a license, enrollmentOptions is required so that license-derived enrollments are not silently converted to direct enrollments. Omitting it returns 400.

curl -X POST "https://{instance}.thoughtindustries.com/v3/users/bulk-sublicense-assignment" \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "users": [
    {
      "id": "3f9a2b18-7c4d-4e2a-9b1f-0c5d6e7f8a90",
      "licenseIdsToRemove": ["b2c3d4e5-f6a7-8901-bcde-f23456789012"]
    }
  ],
  "enrollmentOptions": {
    "preserveEnrollments": false,
    "preserveProgress": false
  }
}'

Parameters

Requires the users.edit permission. A standard company API key satisfies this.

NameTypeRequiredLocationDescription
usersBulkSublicenseUser[]YesbodyUsers to update; each carries its own license add/remove sets. 1–5000 entries.
enrollmentOptionsEnrollmentOptionsConditionalbodyRequired when any user removes a license; optional otherwise. Applies to the whole batch. See Enrollment options.

Each object in users (BulkSublicenseUser):

FieldTypeRequiredDescription
iduuidOne of id / email / externalCustomerIdUser to update, by ID.
emailstringOne of id / email / externalCustomerIdUser to update, by email.
externalCustomerIdstringOne of id / email / externalCustomerIdUser to update, by external customer ID.
licenseIdsToAdduuid[]NoSublicenses to add for this user. Defaults to [].
licenseIdsToRemoveuuid[]NoSublicenses to remove for this user. Defaults to [].

At least one of licenseIdsToAdd / licenseIdsToRemove must be non-empty for each user. The user's Panorama is never changed. When more than one identifier is supplied for a user, they are resolved in priority order id > email > externalCustomerId.

Batch limit: 5000 users per request. Split larger sets into multiple requests.

Enrollment options

enrollmentOptions decides what happens to enrollments that a license removal would otherwise drop. A single enrollmentOptions object applies to every user in the batch.

FieldTypeDefaultDescription
preserveEnrollmentsbooleantrueKeep affected enrollments as direct enrollments (true) or remove that access (false).
preserveProgressbooleantrueKeep progress for removed enrollments (true) or reset it (false). Must be true when preserveEnrollments is true.

The combination { "preserveEnrollments": true, "preserveProgress": false } is invalid — you cannot reset progress while keeping the enrollment — and is rejected with 400. To remove access and wipe progress, send { "preserveEnrollments": false, "preserveProgress": false }.

Example response

Returns 202 Accepted. The payload is nested under data (v3 convention).

{
  "data": {
    "jobId": "22222222-2222-2222-2222-222222222222",
    "totalCount": 2,
    "unresolvedIdentifiers": []
  }
}

Response fields

FieldTypeDescription
data.jobIduuidID of the enqueued background job. Poll it via Get job by ID.
data.totalCountintegerNumber of users enqueued — the entries whose identifier resolved to a user.
data.unresolvedIdentifiersstring[]Caller-supplied identifiers that matched no user in your instance. These users were not enqueued. Empty when every user resolved.

Partial success

The 202 confirms the job was accepted, not that every user succeeded. Results arrive in two layers:

  • Synchronously, at submit time: identifiers that match no user in your instance are returned in unresolvedIdentifiers and are not enqueued. Users that did resolve are still enqueued, so a request can be partially accepted.
  • Asynchronously, in the job: per-user failures that happen while the job runs are recorded on the job, not in this response. Poll Get job by ID to read per-user outcomes, including the downloadable error report for failed rows.

A 202 with a non-empty unresolvedIdentifiers is normal — always inspect it rather than relying on the status code alone.

Errors

The whole request is rejected with 400 (code: "VALIDATION_ERROR") when:

ConditionNotes
users is emptyAt least one user is required.
users exceeds 5000 entriesSplit into multiple requests.
A user has neither licenseIdsToAdd nor licenseIdsToRemoveEach user must change at least one license.
A license is removed without enrollmentOptionsSend enrollmentOptions whenever any user removes a license.
No identifier resolves to a userIf every entry is unresolvable the request fails rather than enqueuing an empty job.
Invalid enrollmentOptions combination{ "preserveEnrollments": true, "preserveProgress": false } is rejected.

Malformed values — a non-UUID ID, an unrecognized property, or a body that violates the schema — are rejected before reaching the endpoint and return an opaque 400 with code: "BAD_REQUEST" and message: "Bad Request Exception", with no indication of which field was at fault. Validate UUIDs and field spelling client-side.

A missing or invalid API key returns 401 (code: "UNAUTHENTICATED"); a key without the users.edit permission returns 403 (code: "FORBIDDEN").

Rate limit

20 requests per 60 seconds, counted per instance. Because each request can carry up to 5000 users, this is rarely a constraint — size your batches up rather than issuing many small calls.