Bulk Operations
Efficiently manage users, enrollments, and data at scale
When migrating from another LMS, onboarding a large cohort, or syncing with an HRIS, you need to process hundreds or thousands of records efficiently. This guide covers batch endpoints, rate limit strategies, and resilient error handling.
Prerequisites
REST API key with write permissions
Understanding of your source data format
A way to track external IDs for idempotency
Familiarity with rate limit headers
Batch User Creation
Create multiple users in a single request. The batch endpoint accepts up to 100 users per call and returns per-record success/failure details:
POST /incoming/v2/users/bulk
Authorization: Bearer {API_KEY}
Content-Type: application/json
{
"users": [
{
"email": "[email protected]",
"firstName": "Alice",
"lastName": "Smith",
"externalId": "emp_001"
},
{
"email": "[email protected]",
"firstName": "Bob",
"lastName": "Jones",
"externalId": "emp_002"
}
],
"options": {
"skipExisting": true,
"sendWelcomeEmail": false
}
}Use externalId to link Thought Industries users to your system. This makes future updates and deduplication trivial.
Batch Enrollments
Enroll many users into courses or learning paths at once. This is ideal for onboarding new hires, rolling out mandatory training, or granting access after external purchases:
POST /incoming/v2/enrollments/bulk
Authorization: Bearer {API_KEY}
Content-Type: application/json
{
"enrollments": [
{
"userId": "usr_alice_001",
"courseId": "crs_intro_api",
"source": "bulk_import"
},
{
"userId": "usr_bob_002",
"courseId": "crs_intro_api",
"source": "bulk_import"
}
]
}Rate Limits
The API returns rate limit headers with every response. Monitor these to avoid throttling during large imports:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1699900000
Retry-After: 45async function batchWithBackoff(items, operation, batchSize = 50) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const response = await operation(batch);
results.push(response);
// Respect rate limits
const remaining = parseInt(response.headers['x-ratelimit-remaining']);
if (remaining < 5) {
const resetAt = parseInt(response.headers['x-ratelimit-reset']) * 1000;
await delay(resetAt - Date.now());
}
}
return results;
}Error Handling
Batch responses include per-item error details so partial failures don't block the entire operation:
{
"successful": 48,
"failed": 2,
"errors": [
{
"index": 3,
"email": "invalid@",
"error": "Invalid email format",
"code": "VALIDATION_ERROR"
},
{
"index": 17,
"email": "[email protected]",
"error": "User already exists",
"code": "CONFLICT"
}
]
}VALIDATION_ERROR
A field failed format or constraint checks. Fix the data and retry.
CONFLICT
The record already exists. Use skipExisting or update instead.
NOT_FOUND
A referenced course or user doesn't exist. Verify IDs first.
RATE_LIMITED
Too many requests. Back off and retry with exponential delay.
Best Practices
Batch size of 50–100
Larger batches risk timeouts. Smaller ones create unnecessary overhead.
Log external IDs
Maintain a mapping table so you can retry or update records later.
Pre-validate data
Check email formats and required fields before sending to reduce validation errors.
Process asynchronously
For imports >1,000 records, use a background job with progress tracking.