# User APIs

The User APIs manage platform users in Sprinklr: the people who log into the product, hold roles and permissions, occupy licensed seats, and are routed work by the assignment engine.

User management controls access. The roles and permissions attached to a user determine what that user can see and do — including what their own API key can do, because a Sprinklr API key inherits the roles and permissions of the user it was created for. Provisioning a user through these APIs is a governance action, not merely a data-entry action.

## Overview

Sprinklr recognises four user types, in descending order of access:

| User type | Description |
|  --- | --- |
| Partner Admin | Highest level of access. Visibility across the entire brand. Can manage users, accounts, workflow structures, and client (local division) accesses and structures. |
| Partner User | Second highest level of access. Visibility across the entire brand, but cannot manage client (local division) accesses. |
| Client Admin | Highest level of access within a single Client environment. Can manage users, accounts, and workflow structures within their local Client. |
| Client User | Lowest level of access. Cannot manage users or accounts, and cannot manage workflow structures within any local Client environment. |


Within a client environment, Client-level roles and permissions override Partner-level roles and permissions.

### Two surfaces, one user

API 3.0 exposes two parallel surfaces for the same user record:

- **The `/user` surface.** A conventional REST surface: one path with five methods, plus an asynchronous bulk family with job-status polling. This is the surface that matches the rest of API 3.0.
- **The `/scim` surface.** A SCIM 2.0-flavoured surface that preserves the API 2.0 URL shapes.


Both surfaces read and write the same user record. The API 2.0 User APIs were the `/scim` paths, so for existing integrators `/scim` is the continuity path and `/user` is the new option. See [Migrating from API 2.0](#migrating-from-api-20).

### Capability summary

| Capability | `/user` surface | `/scim` surface |
|  --- | --- | --- |
| Create one user | `POST /user` | `POST /scim/Users` |
| Fetch one user by id | `GET /user?id=` | `GET /scim/Users/{userId}` |
| Fetch one user by email | `GET /user?email=` | `GET /scim/email/{emailId}` |
| List or search users | Not available | `GET /scim/Users` with filter, sort, and paging |
| Replace a user | `PUT /user?id=` | `PUT /scim/Users/{userId}` |
| Partially update a user | `PATCH /user?id=` | `PATCH /scim/Users/{userId}` and `PUT /scim/update/{userId}` |
| Delete a user | `DELETE /user?id=` | `DELETE /scim/Users/{userId}` |
| Upsert a user | `POST /user/upsert` | `POST /scim/upsert` |
| Bulk create, replace, or patch | `POST` / `PUT` / `PATCH /user/bulk` (asynchronous) | Not available |
| Bulk upsert | `POST /user/bulk/upsert` (asynchronous) | `POST /scim/bulk-upsert` |
| Poll a bulk job | `GET /user/bulk/status?processId=` | Not available |
| Fetch the calling user | `GET /me` | Not available |


Two capabilities exist on only one side. **List and search is available only on `/scim`.** **Asynchronous bulk with job polling is available only on `/user`.** A non-trivial integration will often use both.

## Base URL and environments

All API 3.0 calls follow this pattern:

```
https://api3.sprinklr.com/{env}/api/v3/{resource}
```

Replace `{env}` with your environment. The supported values are:

`prod`, `prod0`, `prod2`, `prod3`, `prod4`, `prod5`, `prod6`, `prod8`, `prod11`, `prod12`, `prod15`, `prod16`, `prod17`, `prod18`, `prod19`, `prod21`, `prod25`

Examples on `prod2`:

```
https://api3.sprinklr.com/prod2/api/v3/user
https://api3.sprinklr.com/prod2/api/v3/user/bulk/status
https://api3.sprinklr.com/prod2/api/v3/scim/Users
```

If you do not know your environment, read it from the URL you use to log into Sprinklr, or ask your Sprinklr account team. Calling the wrong environment returns an authentication or authorisation failure rather than a "wrong environment" message, so confirm this before debugging anything else.

## Authentication

Every request needs two credentials.

| Header | Value | Purpose |
|  --- | --- | --- |
| `Authorization` | `Bearer {{accessToken}}` | Authenticates the user with the server. See the Authorize article to generate a token. |
| `Key` | `{{apiKey}}` | Authenticates the application with the server. See the API Key and Secret Generation article. |


Sending only one produces `401 Unauthorized`.

### Standard header block

```http
Authorization: Bearer {{accessToken}}
Key: {{apiKey}}
Content-Type: application/json
Accept: application/json
```

> **Content-Type has changed on the SCIM surface.** API 2.0 SCIM endpoints used `Content-Type: application/scim+json`. In API 3.0, send `application/json`. If you are porting a 2.0 SCIM client, update this header — it is the most common cause of a failed first call.


### Governance applies to your key

Because an API key inherits the permissions of the user who created it, a key belonging to a Client User cannot provision users at all, and a key belonging to a Client Admin can only provision inside its own client environment. If a create call returns `403 Forbidden` with an apparently valid body, check the permissions of the key's owning user before you check the payload.

## Choosing between the two surfaces

| If you are… | Use | Why |
|  --- | --- | --- |
| Building a new integration from scratch | `/user` | Conventional REST shape, consistent with the rest of API 3.0, and the only surface with asynchronous bulk. |
| Porting an existing 2.0 integration with minimum change | `/scim` | The paths, path parameters, and request bodies are unchanged. Only the version segment and the `Content-Type` change. |
| Connecting a standards-compliant identity provider such as Okta, Entra ID, or OneLogin | `/scim` | It is the SCIM-shaped surface, with SCIM patch documents, list results, `startIndex`/`count` paging, and filter expressions. |
| Loading or reconciling users in large batches | `/user/bulk` | The only asynchronous bulk family, with callbacks and job polling. |
| Searching or listing users | `GET /scim/Users` | The only surface that can list. |


Do not mix the two surfaces for the same write within a single transaction. They resolve identity differently — `/scim` takes a numeric user id in the path, `/user` takes an `id` or `email` query parameter.

Two paths are retained for backward compatibility only and should not be used in new code:

| Legacy path | Use instead |
|  --- | --- |
| `POST /scim` | `POST /scim/Users` |
| `PUT /scim/{userId}` | `PUT /scim/Users/{userId}` |


## Operations on the user surface

### Fetch a user

```
GET /api/v3/user
```

| Parameter | In | Type | Description |
|  --- | --- | --- | --- |
| `id` | query | string | User id |
| `email` | query | string | User email |


**Send exactly one of `id` or `email` on every request.**

Returns an **array** of user objects, even when you fetch by a unique id or email. Take the first element.

### Create a user

```
POST /api/v3/user
```

Request body: a user object. Returns the created user.

> **A user you create is inactive by default.** Set `"active": true` in the create payload if you want the user to be able to log in immediately. Otherwise you must follow the create with an update. This is the most common surprise when provisioning for the first time.


### Replace a user

```
PUT /api/v3/user
```

| Parameter | In | Type |
|  --- | --- | --- |
| `id` | query | string |
| `email` | query | string |


Send exactly one of `id` or `email`. Request body: a user object.

This is a **full replacement**. Fields you omit from the body are not preserved. To change a single attribute, use `PATCH` instead.

### Partially update a user

```
PATCH /api/v3/user
```

| Parameter | In | Type |
|  --- | --- | --- |
| `id` | query | string |
| `email` | query | string |


Send exactly one of `id` or `email`. Request body: a partial update request containing only the fields you want to change.

This is the recommended way to make a targeted change. The partial update request is a purpose-built write model: it contains only writable fields, and its nested update objects support explicit add-and-remove semantics rather than whole-collection replacement.

It also carries `userAvailabilityStatus`, which is the field to use when changing an agent's availability. Confirm the permitted values for your tenant with your Sprinklr account team.

### Delete a user

```
DELETE /api/v3/user
```

| Parameter | In | Type |
|  --- | --- | --- |
| `id` | query | string |
| `email` | query | string |


**Always send `id` or `email`.**

> **Prefer deactivation to deletion.** Setting `"active": false` with `PATCH` blocks the user from logging in while preserving their history and attribution across cases, messages, and reports. Delete only when the record must genuinely be removed. If you do delete, `DELETE /scim/Users/{userId}` is the safer call, because the user id is part of the path and cannot be omitted.


### Upsert a user

```
POST /api/v3/user/upsert
```

Request body: a user object. Returns a user.

Creates the user if no match exists and updates it if one does. Use upsert for reconciliation jobs where you do not want to track whether a user already exists. Confirm the match key with your Sprinklr account team before relying on it for a large reconciliation.

### Bulk operations

Three methods on one path, all asynchronous:

| Method | Purpose |
|  --- | --- |
| `POST /api/v3/user/bulk` | Bulk create users |
| `PUT /api/v3/user/bulk` | Bulk replace users |
| `PATCH /api/v3/user/bulk` | Bulk partially update users |
| `POST /api/v3/user/bulk/upsert` | Bulk upsert users |


All four take the same request shape:

| Field | Type | Description |
|  --- | --- | --- |
| `records` | array | The users to process. Send full user objects for create, replace, and upsert; send partial update objects for patch. |
| `callbackUrl` | string | URL that Sprinklr calls when the job finishes. |
| `callbackUrlHeaders` | object | Optional HTTP headers to send with the callback request, as name/value string pairs. |
| `syncProcessing` | boolean | When true, process records synchronously in-request and return per-record results immediately. |


#### Two processing modes

- **Asynchronous** (default, `syncProcessing` absent or false). The call returns quickly with a process identifier. Wait for the callback, or poll for status.
- **Synchronous** (`syncProcessing: true`). The call blocks and returns per-record results directly. Suitable only for small batches — large synchronous batches risk gateway timeouts.


Confirm the maximum batch size for your tenant with your Sprinklr account team. Start at a few hundred records per batch.

#### Poll a bulk job

```
GET /api/v3/user/bulk/status
```

| Parameter | In | Type | Description |
|  --- | --- | --- | --- |
| `processId` | query | string | Identifier of the bulk job |


Always send `processId`.

Prefer the callback to polling wherever you can receive one. If you must poll, back off — start at 5 seconds and double up to a ceiling of about 60 seconds.

### Fetch the calling user

```
GET /api/v3/me
```

Returns the user that owns the credentials making the call. Useful for verifying which identity, environment, and permission set your key resolves to. This operation is documented in full in the Me API article.

## Operations on the SCIM surface

### Create a user

```
POST /api/v3/scim/Users
```

Request body: a user object. Returns the created user.

### List or search users

```
GET /api/v3/scim/Users
```

| Parameter | In | Type | Description |
|  --- | --- | --- | --- |
| `startIndex` | query | integer | 1-based index of the first result |
| `count` | query | integer | Maximum results per page |
| `filter` | query | string | SCIM filter expression |
| `sortBy` | query | string | Sort field |
| `sortOrder` | query | string | Sort order |


This is the only way to list or search users in API 3.0.

Paging is **1-based index-and-count**, following the SCIM convention. This differs from the cursor-based paging used elsewhere in API 3.0, so do not reuse a cursor-paging helper from another integration. To fetch the second page of 50, send `startIndex=51&count=50`.

Confirm which attributes are filterable and sortable, and the maximum permitted `count`, with your Sprinklr account team before building a synchronisation job.

### Read, replace, patch, and delete by id

```
/api/v3/scim/Users/{userId}
```

`userId` is a required path parameter.

| Method | Purpose | Body |
|  --- | --- | --- |
| `GET` | Get user by id | — |
| `PUT` | Replace user | User object |
| `PATCH` | Patch user | SCIM patch document |
| `DELETE` | Delete user | — |


`PATCH` here takes a standard SCIM patch document with a list of add, replace, and remove operations. If your identity provider emits SCIM patch documents, use this operation directly rather than translating them.

> **The user id is numeric on the SCIM paths and a string on the `/user` query parameter**, for the same identity. Store the id as a string in your own systems to avoid precision loss, and pass a numeric value on the SCIM paths.


### Read by email

```
GET /api/v3/scim/email/{emailId}
```

`emailId` is a required path parameter. Returns a single user object — note that this returns one object, whereas `GET /user?email=` returns an array.

URL-encode the email address in the path. An unencoded `+` in an address such as `jane.doe+test@example.com` is decoded as a space.

### Partial update by id

```
PUT /api/v3/scim/update/{userId}
```

`userId` is a required path parameter. Request body: a user update request. Returns a user.

This endpoint is preserved for compatibility with the API 2.0 Partial User Update endpoint. For new code, prefer `PATCH /scim/Users/{userId}` or `PATCH /user`.

### Upsert

```
POST /api/v3/scim/upsert
```

Request body: a user object. Returns a user. **Matches on email.**

### Bulk upsert

```
POST /api/v3/scim/bulk-upsert
```

Request body: a batch user import object. Returns a bulk response containing per-record results.

This is a different mechanism from `/user/bulk`: it returns per-record results in the response of the call itself, with no callback or job polling. Use it when you want results immediately. Use `POST /user/bulk/upsert` instead when you need to load a very large batch without holding a connection open.

## Field reference

### The user object

| Field | Type | Description |
|  --- | --- | --- |
| `schemas` | array of string | The SCIM schema URIs this resource conforms to. |
| `userName` | string | The unique identifier for the user in Sprinklr. This is the user's email address. |
| `name` | object | Name of the user. |
| `emails` | array | The user's email addresses. |
| `phoneNumbers` | array | The user's phone numbers. |
| `photos` | array | User photos. |
| `active` | boolean | Whether the user is active. **A user is created in an inactive state unless you set this to `true`.** |
| `locale` | string | Locale of the user. |
| `globalAttributes` | object | Partner-level attributes that cannot be mapped to SCIM attributes. |
| `clientAttributes` | array | Client-level attributes. |
| `userSeats` | array | The licensed seats assigned to the user. |
| `isSpaceUser` | boolean | Whether the user is a space user. |
| `enterpriseUser` | object | Enterprise user attributes. |
| `userAssignmentConfig` | object | User assignment configuration, including routing skills, languages, channels, and capacities. |
| `userVoiceConfig` | object | User voice configuration. |
| `currentLoginStatus` | string | Current login status. Set by the platform. |
| `upcomingStatus` | string | Upcoming status. Set by the platform. |
| `supervisorTeamIds` | array of string | Supervisor team ids. |
| `recordingShareConfigs` | array | Recording share configurations. |
| `customPropertiesMetadata` | array | Custom property details. |
| `assignmentData` | object | Assignment data. Set by the platform. |


Three practical notes:

1. **Send `userName` and `active` on every create.** `userName` establishes the identity; `active` determines whether the user can log in.
2. **The user object carries no id field.** Identity travels via `userName`, `emails`, or the id in the query string or path — never in the body.
3. **Keep `userName` and the primary entry of `emails` consistent.** `userName` is the user's email address, and `emails` lists their addresses; they should agree.


### Read and write with the same object

The user object is used both as a request body and as a response body. Some fields — `currentLoginStatus`, `upcomingStatus`, and `assignmentData` — are set by the platform.

**When writing, send only the fields you intend to set.** Do not round-trip a user you fetched straight back into a `PUT`; strip the platform-set status and metadata fields first. When you want to change one attribute and are unsure what is safe to send, use `PATCH /user`, whose partial update object contains only writable fields.

### The partial update object

Used by `PATCH /user`.

| Field | Type | Description |
|  --- | --- | --- |
| `id` | string | User id |
| `externalId` | string | External id from the SCIM client |
| `lyearnId` | string | Learning platform id |
| `userName` | string | Email id / SCIM userName |
| `name` | object | Display name |
| `emails` | array | Emails |
| `phoneNumbers` | array | Phone numbers |
| `photos` | array | Photos |
| `active` | boolean | Active flag |
| `locale` | string | Locale |
| `globalAttributes` | object | Global attributes update |
| `clientAttributes` | array | Client attributes update |
| `isSpaceUser` | boolean | Space user flag |
| `meta` | object | SCIM meta |
| `enterpriseUser` | object | Enterprise user attributes |
| `userSeats` | array | User seats |
| `userAssignmentConfigUpdateRequest` | object | Assignment config update |
| `userVoiceConfigUpdateRequest` | object | Voice config update |
| `userAvailabilityStatus` | string | Agent availability status |


`externalId` and `lyearnId` are available only here. If you need to set the external identity-provider id for a user, `PATCH /user` is the way to do it.

### Assignment updates add and remove explicitly

The assignment config update object pairs each collection with a removal list — `capacities` with `capacitiesToRemove`, and likewise `skillsToRemove`, `languagesToRemove`, and `channelsToRemove`.

This matters. On these nested objects you are **not** replacing the whole collection. Sending `skills: ["french"]` adds French; it does not remove the skills already there. To remove a skill, name it in `skillsToRemove`. This is the opposite of the whole-object replacement semantics of `PUT /user`.

## Worked examples

All examples use placeholder credentials and identifiers. Replace `{{accessToken}}`, `{{apiKey}}`, and `prod2` with your own values.

### Fetch a user by email

```bash
curl -X GET \
  'https://api3.sprinklr.com/prod2/api/v3/user?email=jane.doe%40example.com' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

### Create an active user

```bash
curl -X POST \
  'https://api3.sprinklr.com/prod2/api/v3/user' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -d '{
    "userName": "jane.doe@example.com",
    "active": true,
    "name": {
      "givenName": "Jane",
      "familyName": "Doe"
    },
    "emails": [
      { "value": "jane.doe@example.com", "primary": true }
    ],
    "locale": "en_US"
  }'
```

`"active": true` is deliberate. Omit it and the user is created but cannot log in.

### Deactivate a user without touching anything else

```bash
curl -X PATCH \
  'https://api3.sprinklr.com/prod2/api/v3/user?id=600000000' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -d '{ "active": false }'
```

This is the recommended way to offboard a user while preserving their history.

### List users

```bash
curl -X GET \
  'https://api3.sprinklr.com/prod2/api/v3/scim/Users?startIndex=1&count=50&sortBy=userName&sortOrder=ascending' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

Paging is 1-based. To fetch the second page of 50, send `startIndex=51`.

### Bulk create with a callback, then poll

Submit the job:

```bash
curl -X POST \
  'https://api3.sprinklr.com/prod2/api/v3/user/bulk' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -d '{
    "callbackUrl": "https://your-host.example.com/hooks/user-bulk",
    "callbackUrlHeaders": {
      "X-Signature": "{{sharedSecret}}"
    },
    "syncProcessing": false,
    "records": [
      {
        "userName": "jane.doe@example.com",
        "active": true,
        "name": { "givenName": "Jane", "familyName": "Doe" }
      },
      {
        "userName": "john.smith@example.com",
        "active": true,
        "name": { "givenName": "John", "familyName": "Smith" }
      }
    ]
  }'
```

Poll for status:

```bash
curl -X GET \
  'https://api3.sprinklr.com/prod2/api/v3/user/bulk/status?processId={{processId}}' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

### End-to-end in Python

```python
import time
import requests

ENV = "prod2"
BASE = f"https://api3.sprinklr.com/{ENV}/api/v3"

HEADERS = {
    "Authorization": "Bearer {{accessToken}}",
    "Key": "{{apiKey}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
}


def get_user_by_email(email):
    """Fetch a user by email. The /user surface returns an array, so take
    the first element."""
    resp = requests.get(f"{BASE}/user", headers=HEADERS, params={"email": email})
    resp.raise_for_status()
    users = resp.json().get("data") or []
    return users[0] if users else None


def create_user(user_name, given_name, family_name, locale="en_US"):
    """Create an ACTIVE user. Without active=True the user cannot log in."""
    payload = {
        "userName": user_name,
        "active": True,
        "name": {"givenName": given_name, "familyName": family_name},
        "emails": [{"value": user_name, "primary": True}],
        "locale": locale,
    }
    resp = requests.post(f"{BASE}/user", headers=HEADERS, json=payload)
    resp.raise_for_status()
    return resp.json()


def deactivate_user(user_id):
    """Deactivate rather than delete, to preserve the user's history."""
    resp = requests.patch(
        f"{BASE}/user",
        headers=HEADERS,
        params={"id": user_id},
        json={"active": False},
    )
    resp.raise_for_status()
    return resp.json()


def bulk_create(records, callback_url=None):
    """Submit an asynchronous bulk create."""
    payload = {"records": records, "syncProcessing": False}
    if callback_url:
        payload["callbackUrl"] = callback_url
    resp = requests.post(f"{BASE}/user/bulk", headers=HEADERS, json=payload)
    resp.raise_for_status()
    return resp.json()


def poll_bulk_status(process_id, timeout=900):
    """Poll a bulk job with exponential backoff, capped at 60s per attempt."""
    delay = 5
    waited = 0
    while waited < timeout:
        resp = requests.get(
            f"{BASE}/user/bulk/status",
            headers=HEADERS,
            params={"processId": process_id},
        )
        resp.raise_for_status()
        print("status:", resp.json())
        time.sleep(delay)
        waited += delay
        delay = min(delay * 2, 60)
    raise TimeoutError(f"Bulk job {process_id} did not finish within {timeout}s")


def list_users(page_size=50):
    """Page through all users. Paging is 1-based index-and-count."""
    start = 1
    while True:
        resp = requests.get(
            f"{BASE}/scim/Users",
            headers=HEADERS,
            params={"startIndex": start, "count": page_size, "sortBy": "userName"},
        )
        resp.raise_for_status()
        result = resp.json()
        resources = result.get("Resources") or result.get("data") or []
        if not resources:
            return
        for user in resources:
            yield user
        if len(resources) < page_size:
            return
        start += page_size
```

### End-to-end in JavaScript

```javascript
const ENV = "prod2";
const BASE = `https://api3.sprinklr.com/${ENV}/api/v3`;

const HEADERS = {
  Authorization: "Bearer {{accessToken}}",
  Key: "{{apiKey}}",
  "Content-Type": "application/json",
  Accept: "application/json",
};

async function request(path, options = {}) {
  const response = await fetch(`${BASE}${path}`, { headers: HEADERS, ...options });
  if (!response.ok) {
    const body = await response.text();
    throw new Error(`${response.status} ${response.statusText}: ${body}`);
  }
  return response.json();
}

// The /user surface returns an array even for a unique lookup.
async function getUserByEmail(email) {
  const query = new URLSearchParams({ email });
  const body = await request(`/user?${query}`);
  const users = body.data || [];
  return users.length ? users[0] : null;
}

// Create an ACTIVE user. Omitting active leaves the user unable to log in.
async function createUser({ userName, givenName, familyName, locale = "en_US" }) {
  return request("/user", {
    method: "POST",
    body: JSON.stringify({
      userName,
      active: true,
      name: { givenName, familyName },
      emails: [{ value: userName, primary: true }],
      locale,
    }),
  });
}

// Deactivate rather than delete, to preserve the user's history.
async function deactivateUser(userId) {
  const query = new URLSearchParams({ id: userId });
  return request(`/user?${query}`, {
    method: "PATCH",
    body: JSON.stringify({ active: false }),
  });
}

// Submit an asynchronous bulk create.
async function bulkCreate(records, callbackUrl) {
  const payload = { records, syncProcessing: false };
  if (callbackUrl) payload.callbackUrl = callbackUrl;
  return request("/user/bulk", {
    method: "POST",
    body: JSON.stringify(payload),
  });
}

// Poll a bulk job with exponential backoff, capped at 60s.
async function pollBulkStatus(processId, timeoutMs = 900000) {
  let delay = 5000;
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const query = new URLSearchParams({ processId });
    console.log("status:", await request(`/user/bulk/status?${query}`));
    await new Promise((r) => setTimeout(r, delay));
    delay = Math.min(delay * 2, 60000);
  }
  throw new Error(`Bulk job ${processId} did not finish in time`);
}

// Page through all users. Paging is 1-based index-and-count.
async function* listUsers(pageSize = 50) {
  let start = 1;
  for (;;) {
    const query = new URLSearchParams({
      startIndex: String(start),
      count: String(pageSize),
      sortBy: "userName",
    });
    const result = await request(`/scim/Users?${query}`);
    const resources = result.Resources || result.data || [];
    if (resources.length === 0) return;
    for (const user of resources) yield user;
    if (resources.length < pageSize) return;
    start += pageSize;
  }
}
```

## Response format and status codes

### Envelope

API 3.0 responses are wrapped in a standard envelope:

```json
{
  "data": {},
  "errors": [],
  "metadata": {}
}
```

Each entry in `errors` carries an `id`, a `code`, and a `message`.

### Status codes

| Code | Meaning | What to do |
|  --- | --- | --- |
| `200` | Success | Read `data`. |
| `400` | Bad Request | Malformed payload, invalid field value, or a missing identifier. Check the `errors` array. |
| `401` | Unauthorized | Missing, expired, or superseded credentials. Both `Authorization` and `Key` must be present. Generating a new token for an API key invalidates the previous one. |
| `403` | Forbidden | Credentials are valid but the owning user lacks permission. Check the key owner's role and whether they can act in the target client environment. |
| `404` | Not Found | No user matches the supplied id or email. |


Handle `429` and `5xx` defensively with retry and exponential backoff, particularly around bulk submission and polling. For the rate limits that apply to your tenant, contact your Sprinklr account team.

## Migrating from API 2.0

### The headline

**The API 2.0 User APIs were the SCIM APIs.** Every 2.0 endpoint was `https://api3.sprinklr.com/{env}/api/v2/scim/...`, and API 3.0 keeps all of those paths under `/api/v3/scim/...`.

For many integrations the minimum viable migration is:

1. Change `v2` to `v3` in the URL.
2. Change `Content-Type` from `application/scim+json` to `application/json`.


That is the recommended first step. Port to `/scim` on 3.0, verify in a lower environment, and only then decide whether to adopt the `/user` surface.

### Endpoint mapping

| API 2.0 endpoint | Direct 3.0 equivalent | Modern 3.0 alternative |
|  --- | --- | --- |
| Create User | `POST /api/v3/scim/Users` | `POST /api/v3/user` |
| Create/Update User | `POST /api/v3/scim/upsert` | `POST /api/v3/user/upsert` |
| Bulk Create/Update User | `POST /api/v3/scim/bulk-upsert` | `POST /api/v3/user/bulk/upsert` |
| Read User | `GET /api/v3/scim/Users/{userId}` | `GET /api/v3/user?id=` |
| Read User by Email ID | `GET /api/v3/scim/email/{emailId}` | `GET /api/v3/user?email=` |
| User Update | `PUT /api/v3/scim/Users/{userId}` | `PUT /api/v3/user?id=` |
| Delete User | `DELETE /api/v3/scim/Users/{userId}` | `DELETE /api/v3/user?id=` |
| Partial User Update | `PUT /api/v3/scim/update/{userId}` | `PATCH /api/v3/user` |
| Update Agent Status | Set `userAvailabilityStatus` via `PATCH /api/v3/user` | Same |


### What is new in 3.0

| Area | API 2.0 | API 3.0 |
|  --- | --- | --- |
| Version segment | `/api/v2/` | `/api/v3/` |
| Content-Type | `application/scim+json` | `application/json` |
| Alternative surface | None | The `/user` REST surface |
| Asynchronous bulk | Not available | `POST`, `PUT`, and `PATCH /user/bulk` with callbacks and job polling |
| Sparse update model | User update request via `PUT` | Partial update request via `PATCH`, plus SCIM patch documents |


### What to watch for

1. **Change the `Content-Type` header.** This is the most likely cause of a failed first 3.0 SCIM call.
2. **Do not switch surfaces and versions in the same change.** Port `v2` to `v3` on `/scim` first, confirm parity, then migrate to `/user` as separate work.
3. **Re-test permissions.** Governance behaviour is unchanged, but a newly issued API key inherits the permissions of *its* owner, which may differ from the old key's owner.
4. **`GET /user` returns an array**, whereas the SCIM read-by-id returns a single object. A client ported from 2.0 SCIM to `/user` must change its response handling.
5. **If you use Update Agent Status**, confirm the permitted values of `userAvailabilityStatus` with your Sprinklr account team before cutting over.


## Common tasks

### Provision a new agent so they can log in immediately

`POST /user` with `"active": true`. Include `userName`, `name`, `emails`, and `locale`. Then set roles, seats, and assignment configuration — either via `userSeats` and `userAssignmentConfig` in the same payload, or via a follow-up `PATCH /user`.

### Offboard a user

`PATCH /user?id=...` with `{"active": false}`. Prefer this to deletion: deactivation blocks login while preserving the user's history and attribution.

### Synchronise from an external identity provider

Use the `/scim` surface. Use `GET /scim/Users` with `filter` for reconciliation reads, `POST /scim/upsert` for individual writes (it matches on email), and `PATCH /scim/Users/{userId}` with a SCIM patch document if your provider emits one. Set `externalId` via `PATCH /user`.

### Load several thousand users

`POST /user/bulk` with `syncProcessing: false` and a `callbackUrl`. Chunk into batches, submit sequentially, and record each process identifier. Poll `GET /user/bulk/status` only as a fallback if the callback does not arrive.

### Change one agent's routing skills

`PATCH /user?id=...` with `userAssignmentConfigUpdateRequest`. Remember the add-and-remove semantics: to remove a skill, name it in `skillsToRemove`. Listing only the skills you want to keep will not remove the others.

### Find out which user your API key is

`GET /me`. Do this first whenever you are debugging a `403`.

## Best practices

- **Always set `active` explicitly on create.** Leaving it out creates a user who cannot log in.
- **Send exactly one identifier** — `id` or `email` — on every `GET`, `PUT`, `PATCH`, and `DELETE` against `/user`.
- **Deactivate rather than delete** unless the record must genuinely be removed.
- **Use `PATCH`, not `PUT`, for targeted changes.** `PUT` is a full replacement and will drop fields you omit.
- **Do not round-trip a fetched user into a `PUT`.** Strip platform-set fields such as `currentLoginStatus`, `upcomingStatus`, and `assignmentData` first.
- **Give each integration its own API key**, because only one token pair is active per key.
- **Prefer callbacks to polling** for bulk jobs, and back off exponentially when you must poll.
- **Verify your environment before debugging authentication**, since the wrong environment presents as an auth failure.
- **Store user ids as strings** in your own systems to avoid precision loss.
- **Confirm batch sizes, filterable attributes, and rate limits** with your Sprinklr account team before scaling a synchronisation job.


## Troubleshooting

| Symptom | Likely cause | Fix |
|  --- | --- | --- |
| `401 Unauthorized` on every call | One of the two credentials is missing, or the access token was superseded. | Send both `Authorization: Bearer …` and `Key: …`. Remember that generating a new token for a key invalidates the previous one — check whether another service shares the key. |
| `401` immediately after everything worked yesterday | The access token expired, or another service regenerated a token on the same key. | Access tokens last 30 days. Give each integration its own key. |
| `403 Forbidden` on an apparently valid create | The API key's owning user lacks permission to provision users, or is acting outside its client environment. | Call `GET /me` to confirm which identity your key resolves to, then check that user's role and client scope. |
| The user was created but cannot log in | `active` was not set to `true`. | `PATCH /user?id=...` with `{"active": false}` reversed — send `{"active": true}`. |
| A ported 2.0 SCIM call returns `400` | The `Content-Type` is still `application/scim+json`. | Change it to `application/json`. |
| Fields disappeared after an update | A `PUT` replaced the whole record. | Use `PATCH` for targeted changes. |
| Removing a routing skill has no effect | Assignment updates add rather than replace. | Name the skill in `skillsToRemove`. |
| `GET /user` seems to return the wrong shape | The `/user` surface returns an array even for a unique lookup. | Take the first element. |
| The second page of `GET /scim/Users` repeats the first | Paging is 1-based, not 0-based. | The second page of 50 starts at `startIndex=51`. |
| An email lookup by path fails for an address containing `+` | The `+` was decoded as a space. | URL-encode the email address. |
| A large synchronous bulk call times out | `syncProcessing: true` holds the connection open. | Set `syncProcessing: false` and use a callback or poll for status. |


## Related articles

- [API Overview](https://dev.sprinklr.com/api-overview)
- [API Key and Secret Generation](https://dev.sprinklr.com/api-key-and-secret-generation)
- [Authorize](https://dev.sprinklr.com/authorize)
- [REST API Errors and Status Codes](https://dev.sprinklr.com/rest-api-error-and-status-codes)
- [APIs](https://dev.sprinklr.com/apis)
- [FAQs](https://dev.sprinklr.com/faqs)
- [Help Center](https://dev.sprinklr.com/help-center)