Skip to main content
Back to Guides

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
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
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: 45
backoff.js
async 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.