Bulk Operations
Import users in bulk and assign content at scale
When onboarding a cohort or syncing with an HRIS, you have two bulk-oriented endpoints to choose from. Both live under the user-API rate-limit bucket and both are wired into the platform's background-job infrastructure.
There is no POST /incoming/v2/users/bulk or POST /incoming/v2/enrollments/bulk endpoint. Those paths appeared in earlier drafts of this guide and never existed in the API — use the endpoints described below.
Which endpoint to use
POST /v2/users/bulkImport
Import users from a CSV that you have already uploaded to accessible storage. Enqueues a background job — the HTTP response returns the job identifier, not the imported records.
POST /v2/users/bulkContentAssignment
Grant or update content access for a list of existing users, inline in the request body. Returns an updatedRecordCount plus per-row errors synchronously.
Bulk user import (CSV)
Upload the CSV to a URL the platform can fetch (for example, an asset uploaded via the platform's asset service), then submit the url along with the content and license to grant on import.
POST /incoming/v2/users/bulkImport
Authorization: Bearer {API_KEY}
Content-Type: application/json
{
"url": "https://your-bucket.s3.amazonaws.com/uploads/users-2026-07.csv",
"inviteMessage": "Welcome to Acme University",
"courseIds": ["<course-uuid>"],
"learningPathIds": [],
"licenseIds": [],
"clientId": "<client-uuid>",
"role": "learner",
"dryRun": false
}The response is the background-job record — poll the job endpoint for progress and terminal status:
{
"id": "job-uuid",
"description": "Bulk user import",
"errorMessage": null,
"status": "pending"
}async function pollJob(jobId, apiKey) {
while (true) {
const res = await fetch(
`https://api.thoughtindustries.com/incoming/v2/backgroundJobs/${jobId}`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
const job = await res.json();
if (job.status === 'complete' || job.status === 'failed') return job;
await new Promise(r => setTimeout(r, 5000));
}
}Bulk content assignment (JSON)
When you already have the user list in hand, submit each user by id or identifier along with the courses and learning paths to grant. The response is synchronous and reports per-row errors alongside the total successful count.
POST /incoming/v2/users/bulkContentAssignment
Authorization: Bearer {API_KEY}
Content-Type: application/json
{
"users": [
{
"id": "usr-uuid",
"courseIds": ["<course-uuid-a>", "<course-uuid-b>"],
"learningPathIds": []
},
{
"identifier": "emp_002",
"courseIds": ["<course-uuid-a>"],
"learningPathIds": []
}
]
}{
"updatedRecordCount": 47,
"errors": [
{
"id": null,
"identifier": "emp_ghost",
"error": "User not found",
"record": { "identifier": "emp_ghost", "courseIds": ["..."] }
}
]
}Rate limits
Bulk endpoints are metered under the user-API bucket. Every response includes standard rate-limit headers; back off on Retry-After when you see a 429.
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 17
X-RateLimit-Reset: 1699900000
Retry-After: 45Error handling
bulkContentAssignment returns a per-row errors array — each entry echoes the offending record so you can retry just the failures.
errors[].id
The user UUID that failed, when supplied in the request. `null` when the row was identified by `identifier`.
errors[].identifier
The external identifier that was submitted, when applicable.
errors[].error
Human-readable reason for the failure.
errors[].record
The original request row, so you can retry it after fixing the underlying issue.
Best practices
Prefer bulkImport for large user creation
The background-job model isolates the work from your API request lifetime and is the intended path for CSV-scale onboarding.
Use bulkContentAssignment for existing users
When you already have users in the platform, the JSON path avoids the CSV round-trip and gives you a synchronous report.
Retry only failed rows
Combine errors[].record entries into a follow-up request rather than resending the whole batch.
Watch the userAPI rate limits
Bulk endpoints share the userAPI throttle with individual user operations — pace requests accordingly.