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

Bulk Panorama Migration

Enqueue an asynchronous bulk Panorama (client) migration for a list of users, each with their own destination Panorama and sublicenses.

Moves a heterogeneous set of users between Panoramas in a single request. Each entry in users carries its own destination Panorama and sublicenses, so one call can move different learners to different Panoramas. 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-panorama-assignment

This endpoint is served by the v3 API. Its full path is https://{instance}.thoughtindustries.com/v3/users/bulk-panorama-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 — move users to a Panorama with sublicenses

curl -X POST "https://{instance}.thoughtindustries.com/v3/users/bulk-panorama-assignment" \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "users": [
    {
      "id": "3f9a2b18-7c4d-4e2a-9b1f-0c5d6e7f8a90",
      "targetClientId": "c0ffee00-1111-2222-3333-444455556666",
      "targetLicenseIds": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
    },
    {
      "email": "[email protected]",
      "targetClientId": "c0ffee00-1111-2222-3333-444455556666",
      "targetLicenseIds": []
    }
  ],
  "enrollmentOptions": {
    "preserveEnrollments": true,
    "preserveProgress": true
  }
}'

Example request — move users to the main site

Set targetClientId to null to move a user off Panoramas and back to the main site. A main-site move must not carry any targetLicenseIds.

curl -X POST "https://{instance}.thoughtindustries.com/v3/users/bulk-panorama-assignment" \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "users": [
    {
      "externalCustomerId": "CRM-88213",
      "targetClientId": null
    }
  ]
}'

Parameters

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

NameTypeRequiredLocationDescription
usersBulkPanoramaUser[]YesbodyUsers to move; each carries its own destination Panorama and sublicenses. 1–5000 entries.
enrollmentOptionsEnrollmentOptionsNobodyControls what happens to enrollments the move would otherwise drop. Applies to the whole batch. Defaults to preserve-everything. See Enrollment options.

Each object in users (BulkPanoramaUser):

FieldTypeRequiredDescription
iduuidOne of id / email / externalCustomerIdUser to move, by ID.
emailstringOne of id / email / externalCustomerIdUser to move, by email.
externalCustomerIdstringOne of id / email / externalCustomerIdUser to move, by external customer ID.
targetClientIduuid | nullYesDestination Panorama (client) ID. Pass an explicit null to move the user to the main site (which must carry no targetLicenseIds).
targetLicenseIdsuuid[]NoSublicenses to grant in the destination Panorama. Defaults to [].

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 a user's existing enrollments when the move would otherwise remove them (for example, license-derived enrollments the destination Panorama does not grant). 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 (for example a license that does not belong to the destination Panorama) 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.
No identifier resolves to a userIf every entry is unresolvable the request fails rather than enqueuing an empty job.
A main-site move carries licensesAn entry with targetClientId: null must not include targetLicenseIds.
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.