Clone Client
Copies an existing Panorama (client) into a new client asynchronously, returning a background job to poll for completion
Copies an existing Panorama (client) into a new client within the same company. The copy runs asynchronously: the endpoint returns a BackgroundJob immediately, then you poll that job until it finishes.
https://example.thoughtindustries.com/incoming/v2/clients/:id/cloneRequires an API key whose role includes the clients.edit permission.
Example request
curl -X POST "https://example.thoughtindustries.com/incoming/v2/clients/:id/clone" \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "Acme West",
"slug": "acme-west"
}'Parameters
| Name | Type | Required | Location | Description |
|---|---|---|---|---|
id | uuid | Yes | path | ID of the source Panorama (client) to copy. |
name | string | Yes | body | Display name for the new client. |
slug | string | No | body | URL path for the new client. If omitted (or empty), a slug is derived from name. Must be unique within the company. |
Slug rules
- If you supply
slug, it must match^[a-z0-9-]{0,255}$— lowercase letters, digits, and hyphens only, up to 255 characters. A supplied slug is validated as-is (it is not auto-transformed). A value that does not match is rejected with an HTTP400. Note: the API does not currently return a slug-specific message for this case — the validation error is masked by a generic catch-all, so the response body is{ "errors": [{ "message": "A processing error occurred. Please refresh the page and try again." }] }(see Error responses below). - If you omit
slug(or send an empty/whitespace value), it is generated fromname: whitespace, underscores, and slashes become hyphens; all other non-alphanumeric characters are removed; consecutive hyphens are collapsed; leading and trailing hyphens are trimmed; the result is lowercased and truncated to 255 characters. - Slugs are unique per company. If the resulting slug already belongs to another client in the company, the request fails.
Example response
The response is a BackgroundJob. The clone is not finished when this returns — poll the job (see below) until status is complete.
{
"data": {
"APICloneClient": {
"id": "00000000-0000-0000-0000-000000000000",
"description": "Copying 3f9a2b18-7c4d-4e2a-9b1f-0c5d6e7f8a90 to new client Acme West.",
"errorMessage": null,
"status": "queued"
}
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | uuid | ID of the background job. Use it to poll for completion. |
description | string | Human-readable summary of the job. |
errorMessage | string | Error details if the job failed; null or absent on success. |
status | BackgroundJobStatus | One of queued, processing, complete, failed. |
Polling for completion
Poll the background job with GET /v2/jobs/:id, passing the id returned above. It returns the same BackgroundJob shape; status transitions queued → processing → complete (or failed, with errorMessage populated).
curl "https://example.thoughtindustries.com/incoming/v2/jobs/:id" \
-H 'Authorization: Bearer YOUR_API_KEY'Once the job is complete, find the new client by its slug:
curl "https://example.thoughtindustries.com/incoming/v2/clients?query=acme-west" \
-H 'Authorization: Bearer YOUR_API_KEY'Error responses
| Status | Body | When |
|---|---|---|
| 400 | { "valid": false, "errors": ["Missing required parameter."] } | name is missing or empty (or the derived slug is empty because name contains no slug-safe characters and no slug was supplied). |
| 400 | { "valid": false, "errors": ["A Client already exists with that path."] } | The slug is already used by another client in the company. |
| 400 | { "valid": false, "errors": ["A job for cloning this client with the specified path is already queued."] } | A clone job for this source client with the same slug is already in flight. |
| 400 | { "errors": [{ "message": "A processing error occurred. Please refresh the page and try again." }] } | The supplied slug does not match ^[a-z0-9-]{0,255}$. The Slug scalar's validation error is masked by a generic catch-all, so no slug-specific message is returned. Note this envelope differs from the other 400s above (it is { errors: [{ message }] }, not { valid, errors }). |
| 403 | { "error": "<message>" } | The API key's role lacks the clients.edit permission. |
What gets copied
- Name and slug from the request.
- The new client is flagged
generated: true. - Course tags (
courseTags). panoramaanddisabledflags.- Feature toggles:
enableSegmentation,enableDiscussions,enableCommunitiesSegmentation,enableBranding,enableEcommerce,enableOnboardingSurvey,enableGlobalLinks,enableNavLinks,enableRecommendationAssessment. - Language settings:
defaultLanguage,languageSelectorEnabled,languages,autoFilterForSelectedLanguage. - Provisioned content (for a Panorama source):
courses,learningPaths,purchasableContentIds,purchasableTagIds. - Licenses / sublicenses: every license on the source client is copied (with new IDs), including its course tags and expiration; parent-license relationships and the client's primary license are remapped to the copies.
- Blocks:
appearanceBlock,emailLayoutBlock,catalogBlock,ssoBlock(copied at both the client and per-license level); a freshappearanceHashis generated. - Layouts:
redemptionLayout,dashboardLayout(copied at both the client and per-license level); layout widget IDs are regenerated. - Seat usage is recomputed for the new client.
What is not copied
skuseatsLimit- Client-level
licenseEndDate notificationEmailsenableCustomEmailSettings— theemailLayoutBlockis copied, but because this toggle is not carried over, the copied email branding is inactive on the new client.enableContentDetailPageenableLicenseDashboardsenableCreditPurchasingcustomHost- Learners and their enrollments
- Onboarding survey responses (only the
enableOnboardingSurveytoggle is copied, not learner responses)