# Profile API V3 — Developer Guide

- **Applies to:** Sprinklr Profile API V3 (`/api/v3/profile`)
- **V2 API reference:** [Profile | Sprinklr Developer Portal](https://dev.sprinklr.com/profile)


## 1. Overview

A **Sprinklr Global Profile** (also called a Universal Profile) is a single database record for a user who enters the Sprinklr Platform. It prevents duplicate or conflicting records for users who interact with brand accounts, and it lets the platform display threaded conversations across channels. Profile data is stored as standard and custom fields and is visible in the **Audience Profiles** module in the Sprinklr UI.

Profile V3 exposes full CRUD on a **single resource path** — `/api/v3/profile` — differentiated by HTTP method:

| Operation | Method | Path |
|  --- | --- | --- |
| Create profile | `POST` | `/api/v3/profile` |
| Get profile | `GET` | `/api/v3/profile?id=` or `?channelTypeChannelId=` or paginated search |
| Update profile (full replace) | `PUT` | `/api/v3/profile?channelTypeChannelId=` |
| Update profile (partial) | `PATCH` | `/api/v3/profile?channelTypeChannelId=` |
| Delete profile | — | Not exposed in V3. See [§4.6](#46-deleting-profile-data-gdpr). |


This is the central design change from V2, which spread the same capabilities across `POST /api/v2/profile`, `POST /api/v2/profile/bulk-update`, and several distinct fetch endpoints.

For more details refer to [Profile API Reference](/apis/sprinklr-v3/profile-v3).

### 1.1 The profile data model

Profile data is modeled in three layers. Understanding this model is the fastest way to understand every endpoint in this guide.

| Layer | Object | What it holds | Scope |
|  --- | --- | --- | --- |
| Person | `contact`, `demographics` | `firstName`, `lastName`, `fullName`, `email`, `phoneNo`, `website`, `emailDetails`, `phoneDetails`, `location`, `gender` | One per Universal Profile |
| Social identity | `profiles[]` | `channelType`, `channelId`, `name`, `permalink`, `avatarUrl`, `profileImageUrl`, `bio`, `followers`, `following`, `verified`, `unSubscribed`, `accountSpecificInfos` | One entry per social account the person owns |
| Workflow | `profileWorkflow` | `profileLists`, `customProperties`, `profileSpaceWorkflows` | Global (partner) level plus per-workspace level |


One Universal Profile can hold **multiple social identities**. The create example in [§4.1](#41-create-a-profile) attaches two YouTube channels (`webDLabs70` and `webDLabs50`) to a single person record — this is the mechanism that produces the unified customer view.

### 1.2 Addressing a profile

A profile is addressed in two ways:

- **By Sprinklr ID** — `id`, for example `69c17f4c82492af653b27af2`
- **By profile key** — `channelTypeChannelId`, a `channelType~channelId` pair, for example `YOUTUBE~webDLabs70`


`GET` accepts both. `PUT` and `PATCH` are documented against `channelTypeChannelId`.

## 2. Base URLs and environments

**All API calls are sent to the production endpoint:**

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

So the profile resource in production is:

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

Replace `{env}` with your assigned environment identifier (`prod0`, `prod2`, `prod11`, and so on — see [APIs | Sprinklr Developer Portal](https://dev.sprinklr.com/apis) for the environment list).

## 3. Authentication and common headers

All Profile API calls are authenticated with OAuth 2.0. See [Developer Tools in Sprinklr](https://www.sprinklr.com/help/articles/developer-tools/developer-tools-in-sprinklr/692e8b39f0afa271d18a5929) for API key and secret generation, and the Authorize flow.

| Header | Value | Purpose | Required on |
|  --- | --- | --- | --- |
| `Authorization` | `Bearer {{accessToken}}` | Authenticates the user with the server | All requests |
| `Key` | `{{apiKey}}` | Authenticates the application with the server | All requests |
| `Content-Type` | `application/json` | Declares the request body media type | `POST`, `PUT`, `PATCH` |
| `Accept` | `application/json` | Declares the acceptable response type | All requests |


**Permissions:** the caller needs **`VIEW` permission on the `AUDIENCE_PROFILE` entity** for read operations. Missing permission returns `403 Forbidden`.

## 4. Write operations

### 4.1 Create a profile

**`POST /api/v3/profile`**

Creates a new Universal Profile with its contact details, demographics, and one or more attached social identities.

#### Request body

| Parameter | Sub-parameter | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `contact` |  | Required | Object | Contact details of the customer |
|  | `firstName` | Optional | String | First name. Recommended — acts as a unique identifier for the customer profile. |
|  | `lastName` | Optional | String | Last name. Recommended — acts as a unique identifier. |
|  | `fullName` | Optional | String | Full name. Recommended — acts as a unique identifier. |
|  | `email` | Optional | String | Email address of the customer |
|  | `phoneNo` | Optional | String | Contact number of the customer |
|  | `website` | Optional | List[String] | Websites where the customer has been identified |
| `demographics` |  | Optional | Object | Demographic details of the customer |
|  | `gender` | Optional | String | Gender of the customer |
|  | `location` | Optional | String | Country the customer belongs to |
| `profiles` |  | Required | Array | Social profile level details. One entry per social identity. |
|  | `name` | Required | String | Display name of the customer on that channel |
|  | `channelType` | Required | String | Channel type. **Case-sensitive, uppercase** — `YOUTUBE`, `TWITTER`, `FACEBOOK`, `EMAIL`, `SMS`, `WHATSAPP`. |
|  | `channelId` | Required | String | Native channel user ID of the customer |
|  | `permalink` | Optional | String | Link to the social profile |
|  | `avatarUrl` | Optional | String | Image link for the display picture |
|  | `bio` | Optional | String | Bio of the customer on that social profile |
|  | `followers` | Optional | Integer | Reach of the customer on that social profile |
|  | `following` | Optional | Integer | Number of accounts the customer follows |
|  | `verified` | Optional | Boolean | Whether the native channel marks the account as verified |
|  | `unSubscribed` | Optional | Boolean | If `true`, the profile is unsubscribed from receiving email notifications |
|  | `deleted` | Optional | Boolean | Soft-delete marker on the social identity |
|  | `statusCount` | Optional | Integer | Number of posts/statuses on the native channel |
|  | `accountSpecificInfos` | Optional | Object / Array | Per-social-account state. See [§4.1.1](#411-accountspecificinfos). |
|  | `additional` | Optional | Object | Free-form additional attributes |


#### Request

```bash
curl --location 'https://api3.sprinklr.com/{env}/api/v3/profile' \
--header 'Authorization: Bearer {{accessToken}}' \
--header 'Key: {{apiKey}}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data-raw '{
    "contact": {
        "firstName": "Shivangi",
        "lastName": "Singh",
        "fullName": "Shivangi Singh",
        "email": "shivi_test@gmail.com",
        "phoneNo": "+917205963027",
        "website": [
            "https://youtube.com"
        ]
    },
    "demographics": {
        "gender": "female",
        "location": "INDIA"
    },
    "profiles": [
        {
            "name": "webDLabs",
            "channelType": "YOUTUBE",
            "channelId": "webDLabs70",
            "permalink": "https://www.youtube.com/@webDLabs",
            "followers": 5700,
            "bio": "testing 70",
            "avatarUrl": "https://yt3.googleusercontent.com/ytc/AIdro_ny6e4KcJV8GqQri4-3Wp6t1pm2Vo3NkZ21igveFM0dnB8=s160-c-k-c0x00ffffff-no-rj",
            "following": 10,
            "unSubscribed": false,
            "deleted": false,
            "statusCount": 0,
            "accountSpecificInfos": {},
            "additional": {},
            "verified": true
        },
        {
            "name": "webDLabs",
            "channelType": "YOUTUBE",
            "channelId": "webDLabs50",
            "permalink": "https://www.youtube.com/@webDLabs",
            "followers": 5700,
            "bio": "testing 50",
            "avatarUrl": "https://yt3.googleusercontent.com/ytc/AIdro_ny6e4KcJV8GqQri4-3Wp6t1pm2Vo3NkZ21igveFM0dnB8=s160-c-k-c0x00ffffff-no-rj",
            "following": 10,
            "unSubscribed": false,
            "deleted": false,
            "statusCount": 0,
            "accountSpecificInfos": {},
            "additional": {},
            "verified": true
        }
    ]
}'
```

Note that both `profiles[]` entries carry the **same `name`** (`webDLabs`) but **distinct `channelId` values** (`webDLabs70`, `webDLabs50`). `channelId` is the identity key within a channel; `name` is only a display label.

#### 4.1.1 `accountSpecificInfos`

Carries per-social-account state. The create example passes an empty object `{}`; the full-update example passes an array of objects. Supported fields:

| Parameter | Required | Type | Description |
|  --- | --- | --- | --- |
| `accountId` | Optional | Integer | Social account ID to associate the profile with |
| `externalId` | Optional | String | External ID of the profile, if any |
| `lastBrandEngagedTime` | Optional | Integer | Last brand engagement time |
| `lastFanEngagedTime` | Optional | Integer | Last fan engagement time |
| `optIn` | Optional | Boolean | Opt-in state. Default: `false` |
| `fanSubscriptionState` | Optional | String | Subscription state of the fan |
| `activeUser` | Optional | Boolean | Whether the user is active. Default: `false` |
| `invited` | Optional | Boolean | Whether the user was invited. Default: `false` |


> **Open item.** The create example types `accountSpecificInfos` as an object (`{}`) while the full-update example types it as an array (`[{"accountId": 0}]`). Confirm the canonical type with the API owner — see [§11](#11-questions-for-the-api-owner).


### 4.2 Update a profile — full update

**`PUT /api/v3/profile?channelTypeChannelId={channelType}~{channelId}`**

Replaces the profile document. Use this when your system is the source of truth and you are sending the complete, authoritative state of the profile.

#### Query parameters

| Parameter | Type | Required | Description | Example |
|  --- | --- | --- | --- | --- |
| `channelTypeChannelId` | String | Required | Profile key in `channelType~channelId` form | `YOUTUBE~webDLabs70` |


#### Additional body fields available on `PUT`

Beyond the create fields, the full update accepts:

| Parameter | Type | Description |
|  --- | --- | --- |
| `restricted` | Boolean | Restriction flag on the profile |
| `contact.emailDetails` | Array[Object] | Structured email records, for example `[{"email":"abcd@gmail.com"}]` |
| `contact.phoneDetails` | Array | Structured phone records |
| `profiles[].profileImageUrl` | String | Profile image URL (the V3 name; V2 called this `imageUrl`) |
| `profiles[].url` | String | Canonical URL of the social profile |
| `profiles[].snCreatedTime` | Long | Creation time on the native channel (epoch ms) |
| `profiles[].snModifiedTime` | Long | Last modification time on the native channel (epoch ms) |
| `profileWorkflow.profileLists` | List[Integer] | Global (partner) level profile list IDs |
| `profileWorkflow.customProperties` | Object | Global level custom properties, keyed by custom field ID |
| `profileWorkflow.profileSpaceWorkflows` | Array | Per-workspace properties: `spaceId` (String, required), `profileLists` (List[Integer]), `tags` (List[String]) |


#### Request

```bash
curl --location --request PUT 'https://api3.sprinklr.com/{env}/api/v3/profile?channelTypeChannelId=YOUTUBE~webDLabs70' \
--header 'Authorization: Bearer {{accessToken}}' \
--header 'Key: {{apiKey}}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data-raw '{
    "restricted": false,
    "contact": {
        "firstName": "Dinesh",
        "lastName": "Sisodiya 2",
        "fullName": "Dinesh Sisodiya 2",
        "email": "test-profile@gmail.com",
        "phoneNo": "+917205963028",
        "website": [
            "https://youtube.com"
        ],
        "phoneDetails": [],
        "emailDetails": [{"email":"abcd@gmail.com"}]
    },
    "demographics": {
        "location": "UK",
        "gender": "male"
    },
    "profiles": [
        {
            "name": "webDLabs",
            "channelType": "YOUTUBE",
            "channelId": "webDLabs90",
            "permalink": "https://www.youtube.com/@webDLabs",
            "avatarUrl": "https://yt3.googleusercontent.com/ytc/AIdro_ny6e4KcJV8GqQri4-3Wp6t1pm2Vo3NkZ21igveFM0dnB8=s160-c-k-c0x00ffffff-no-rj",
            "profileImageUrl": "https://yt3.googleusercontent.com/ytc/AIdro_ny6e4KcJV8GqQri4-3Wp6t1pm2Vo3NkZ21igveFM0dnB8=s160-c-k-c0x00ffffff-no-rj",
            "bio": "testing profile with workflow",
            "followers": 5700,
            "following": 11,
            "verified": true,
            "unSubscribed": false,
            "deleted": false,
            "snCreatedTime": 0,
            "snModifiedTime": 1774288713717,
            "statusCount": 0,
            "accountSpecificInfos": [
                {
                    "accountId": 0
                }
            ],
            "additional": {},
            "url": "https://www.youtube.com/@webDLabs"
        },
        {
            "name": "webDLabs",
            "channelType": "YOUTUBE",
            "channelId": "webDLabs901",
            "permalink": "https://www.youtube.com/@webDLabs",
            "avatarUrl": "https://yt3.googleusercontent.com/ytc/AIdro_ny6e4KcJV8GqQri4-3Wp6t1pm2Vo3NkZ21igveFM0dnB8=s160-c-k-c0x00ffffff-no-rj",
            "profileImageUrl": "https://yt3.googleusercontent.com/ytc/AIdro_ny6e4KcJV8GqQri4-3Wp6t1pm2Vo3NkZ21igveFM0dnB8=s160-c-k-c0x00ffffff-no-rj",
            "bio": "testing profile with workflow",
            "followers": 5700,
            "following": 901,
            "verified": true,
            "unSubscribed": false,
            "deleted": false,
            "snCreatedTime": 0,
            "snModifiedTime": 1774288713717,
            "statusCount": 0,
            "accountSpecificInfos": [
                {
                    "accountId": 0
                }
            ],
            "additional": {},
            "url": "https://www.youtube.com/@webDLabs"
        }
    ],
    "profileWorkflow": {
        "profileLists": [],
        "customProperties": {
            "_c_64dcb892e32de6530b5a8dbf": [
                "IND"
            ],
            "_c_6512721b83353e6f3e80c1c5": [
                "Updated"
            ],
            "_c_651ed978c84c6928d762f976": [
                "N/A"
            ]
        },
        "profileSpaceWorkflows": []
    }
}'
```

#### ⚠️ `PUT` replaces — it does not merge

This example illustrates the risk directly. The profile is addressed by `channelTypeChannelId=YOUTUBE~webDLabs70`, but the `profiles[]` array in the body contains `webDLabs90` and `webDLabs901` — **not** `webDLabs70`. The body also sends `"profileLists": []` and `"profileSpaceWorkflows": []`.

Under replace semantics that discards the original social identity and clears all list membership and workspace workflow state. Before issuing a `PUT`:

1. `GET` the current profile.
2. Merge your changes into the retrieved document.
3. `PUT` the merged result back.


If you only intend to change a few fields, use `PATCH` ([§4.3](#43-update-a-profile--partial-update)) instead. `PATCH` exists precisely so you do not have to do a read-modify-write cycle.

### 4.3 Update a profile — partial update

**`PATCH /api/v3/profile?channelTypeChannelId={channelType}~{channelId}`**

Changes a subset of fields without resending the whole profile document. This is the safe default for incremental syncs, enrichment jobs, and any integration that is not the sole source of truth for the profile.

#### Request body

| Parameter | Sub-parameter | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `profileUpdateRequestDTO` |  | Optional | Object | Profile details to update |
|  | `contactInfo` | Optional | Object | Contact fields: `firstName`, `lastName`, `fullName`, `emailDetails`, `phoneNo` |
| `profileWorkflowUpdateRequestDTO` |  | Optional | Object | Custom property update operations |
|  | `addedPartnerCustomProperties` | Optional | Object | **Adds** values to existing custom property values without removing current ones |
|  | `selectivePartnerCustomProperties` | Optional | Object | **Sets** the listed custom properties to the supplied values, leaving unlisted properties untouched |


Custom properties are keyed by custom field ID (for example `_c_64dcb892e32de6530b5a8dbf`) and always take a **list** of values, even for single-valued fields.

#### Request

```bash
curl --location --request PATCH 'https://api3.sprinklr.com/{env}/api/v3/profile?channelTypeChannelId=YOUTUBE~webDLabs70' \
--header 'Authorization: Bearer {{accessToken}}' \
--header 'Key: {{apiKey}}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
    "profileUpdateRequestDTO": {
        "contactInfo": {
            "phoneNo": "99909880891"
        }
    },
    "profileWorkflowUpdateRequestDTO": {
        "addedPartnerCustomProperties": {
            "_c_64dcb892e32de6530b5a8dbf": ["IRAN"]
        },
        "selectivePartnerCustomProperties": {
            "_c_6512721b83353e6f3e80c1c5": ["updated via patch api"]
        }
    }
}'
```

#### Choosing the right custom-property operation

| Intent | Operation |
|  --- | --- |
| Append a new value to a multi-valued property (for example, add a product interest) | `addedPartnerCustomProperties` |
| Set a property that must hold exactly the supplied values (for example, lifecycle stage) | `selectivePartnerCustomProperties` |


Using `selectivePartnerCustomProperties` where you meant `addedPartnerCustomProperties` silently discards every other value on that property.

### 4.4 Method comparison — when to use which

|  | `POST` | `PUT` | `PATCH` |
|  --- | --- | --- | --- |
| Purpose | Create a new profile | Replace the full profile document | Change selected fields |
| Addressing | None — identity comes from the body | `?channelTypeChannelId=` | `?channelTypeChannelId=` |
| Body shape | `contact` / `demographics` / `profiles[]` | Same as `POST`, plus `profileWorkflow`, `restricted`, extended `profiles[]` fields | `profileUpdateRequestDTO` / `profileWorkflowUpdateRequestDTO` |
| Unspecified fields | N/A | **Replaced/cleared** | **Preserved** |
| Custom-property semantics | Set at creation | Whole `customProperties` map replaced | Per-operation: add or selectively set |
| Safe for incremental sync | — | No, without read-modify-write | Yes |


Note that `PUT` and `PATCH` take **different body schemas**. `PUT` takes the profile document directly; `PATCH` takes DTO wrappers. You cannot reuse one payload for both methods.

## 5. Read operations

### 5.1 `GET /api/v3/profile` — Fetch profiles

This is a **dual-mode** endpoint. It replaces three separate V2 reads:

| V2 endpoint | V3 equivalent |
|  --- | --- |
| [Fetch Profile by Profile Id](https://dev.sprinklr.com/profile) | `GET /api/v3/profile?id=` |
| [Fetch Profile by sntype and snUserId](https://dev.sprinklr.com/profile) | `GET /api/v3/profile?channelTypeChannelId=` |
| [Fetch Profile by sntype and username](https://dev.sprinklr.com/profile) | `GET /api/v3/profile?pageNumber=&channelType=&username=` |


#### Mode 1 — Direct fetch by identifier

Use this mode when you already hold identifiers, typically from a create response, a webhook, an export, or a prior search.

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `id` | String | Optional | Comma-separated profile IDs (mutually exclusive with `channelTypeChannelId`) |
| `channelTypeChannelId` | String | Optional | Comma-separated `channelType~channelId` values (mutually exclusive with `id`) |


```bash
curl --location 'https://api3.sprinklr.com/{env}/api/v3/profile?id=69c17f4c82492af653b27af2' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{accessToken}}' \
--header 'Key: {{apiKey}}'
```

```bash
# Multiple IDs in one call
curl --location 'https://api3.sprinklr.com/{env}/api/v3/profile?id=69c17f4c82492af653b27af2,64fed24d53edd22031e97322' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{accessToken}}' \
--header 'Key: {{apiKey}}'

# By profile key
curl --location 'https://api3.sprinklr.com/{env}/api/v3/profile?channelTypeChannelId=YOUTUBE~webDLabs70' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{accessToken}}' \
--header 'Key: {{apiKey}}'
```

#### Mode 2 — Paginated search by channel type and username

Use this mode when you know **who** you are looking for but not their identifier.

| Parameter | Type | Required | Description | Example values |
|  --- | --- | --- | --- | --- |
| `pageNumber` | Integer | Required for paginated mode | 0-based page index. First page is 0, then 1, 2, etc. | `0`, `1`, `2` |
| `pageSize` | Integer | Optional | Number of profiles per page. Default: 20, Maximum: 100 | `10`, `20`, `50`, `100` |
| `channelType` | String | Required when `pageNumber` provided | Social network channel type | `TWITTER`, `FACEBOOK`, `INSTAGRAM`, `YOUTUBE`, `LINKEDIN` |
| `username` | String | Required when `pageNumber` provided | Username to search for on the specified channel | `NicoleJohn41342`, `john_doe`, `company_official` |


```bash
curl --location 'https://api3.sprinklr.com/{env}/api/v3/profile?pageNumber=0&pageSize=10&channelType=TWITTER&username=NicoleJohn41342' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{accessToken}}' \
--header 'Key: {{apiKey}}'
```

**Response** *(illustrative example, generated from the documented schema)*

```json
{
  "data": [
    {
      "id": "69e7b1b172f9219650974ffa",
      "channelType": "TWITTER",
      "channelId": "1952565946465304576",
      "username": "NicoleJohn41342",
      "displayName": "Nicole John",
      "description": "Digital Marketing Specialist | Content Creator",
      "profileImageUrl": "https://pbs.twimg.com/profile_images/...",
      "followersCount": 1250,
      "followingCount": 890,
      "location": "San Francisco, CA",
      "verified": false,
      "createdTime": 1690876800000,
      "modifiedTime": 1690876800000,
      "customProperties": {
        "industry": "Technology",
        "engagement_score": "high"
      }
    }
  ],
  "errors": [],
  "metadata": {
    "totalCount": null,
    "hasMore": true,
    "pageNumber": 0,
    "pageSize": 10
  }
}
```

> **Do not mix modes.** Do not send direct-fetch parameters (`id`, `channelTypeChannelId`) together with pagination parameters (`pageNumber`, `pageSize`). Mixed parameter sets return `400 Bad Request`. `id` and `channelTypeChannelId` are also mutually exclusive with each other.


### 5.2 `GET /api/v3/conversation` — Fetch a profile's conversations

Replaces [Fetch Profile Conversations](https://dev.sprinklr.com/fetch-profile-conversations) (`POST /api/v2/profile/conversations`) and [Message Conversations](https://dev.sprinklr.com/message-conversations) (`POST /api/v2/message/conversations`).

Once you have resolved a profile, this is how you retrieve everything that profile has said. **Mode 1 (Profile Conversations)** is the profile-relevant mode.

| Parameter | Type | Required | Description | Example |
|  --- | --- | --- | --- | --- |
| `channelType` | String | Yes | Social network channel type | `TWITTER`, `FACEBOOK`, `INSTAGRAM` |
| `channelId` | String | Yes | ID of the social account/profile | `1647140850` |
| `sinceTime` | Long | Optional | Messages at or after this time (epoch ms) | `1649437654240` |
| `untilTime` | Long | Optional | Messages at or before this time (epoch ms) | `1649826760000` |
| `pageNumber` | Integer | Optional | 0-based page index (default: 0) | `0`, `1`, `2` |
| `pageSize` | Integer | Optional | Rows per page (default: 20, max: 100) | `20`, `50`, `100` |
| `sourceType` | Enum | Optional | Filter by message source | `ACCOUNT`, `LISTENING` |
| `sortKey` | String | Required* | Message field for sorting | `createdTime`, `modifiedTime` |
| `sortOrder` | String | Optional | Sort direction (default: `DESC`) | `ASC`, `DESC` |


Supported `sortKey` values: `createdTime` (default), `modifiedTime`, `channelCreatedTime`, `channelMessageId`, `parentMessageId`, `associatedCaseNumber`.

```bash
curl -X GET \
  'https://api3.sprinklr.com/{env}/api/v3/conversation?channelType=YOUTUBE&channelId=webDLabs70&pageNumber=0&pageSize=20&sortKey=createdTime&sortOrder=DESC' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

The conversation response uses the same envelope, with `metadata.hasMore` as the only metadata field:

```json
{
  "data": [ { "messageId": "...", "content": {}, "senderProfile": {} } ],
  "errors": [],
  "metadata": { "hasMore": true }
}
```

Key V2 → V3 parameter renames: `sinceDate` → `sinceTime`, `untilDate` → `untilTime`, `start` → `pageNumber` (`pageNumber = start / rows`), `rows` → `pageSize`, `profileKey.channelType` → `channelType`, `profileKey.channelId` → `channelId`, `sort.key` → `sortKey`, `sort.order` → `sortOrder`. All parameters move from the POST body to the query string, and the method changes from `POST` to `GET`.

**Mutual exclusivity:** provide either (`channelType` + `channelId`) OR `messageId`, but not both.

## 6. Response format and status codes

### 6.1 The V3 envelope

Every V3 endpoint returns the same three-part envelope:

```json
{
  "data": [ ... ],
  "errors": [],
  "metadata": {
    "totalCount": null,
    "hasMore": true,
    "pageNumber": 0,
    "pageSize": 10
  }
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `data` | Array[UnifiedProfile] | Array of profile objects matching the search criteria |
| `errors` | Array[Error] | Array of error objects (empty if no errors) |
| `metadata` | Object | Pagination and response metadata |
| `metadata.totalCount` | Integer/null | Total number of results (`null` for performance reasons) |
| `metadata.hasMore` | Boolean | Indicates if more pages are available |
| `metadata.pageNumber` | Integer | Current page number (0-based) |
| `metadata.pageSize` | Integer | Number of items per page |


### 6.2 Response codes

| HTTP Code | Scenario | Description |
|  --- | --- | --- |
| `200 OK` | Success | Profiles found and returned successfully |
| `201 OK` | Success | Profiles created |
| `400 Bad Request` | Invalid Parameters | Missing required parameters or invalid parameter combinations |
| `401 Unauthorized` | Authentication Failed | Invalid or missing `Authorization` token |
| `403 Forbidden` | Insufficient Permissions | User lacks `VIEW` permission for `AUDIENCE_PROFILE` |
| `404 Not Found` | No Profiles Found | No profiles match the search criteria |
| `500 Internal Server Error` | Server Error | Unexpected server-side error occurred |


## 7. V2 → V3 migration

### 7.1 Endpoint mapping

| V2 | V3 |
|  --- | --- |
| `POST /api/v2/profile` (create) | `POST /api/v3/profile` |
| `POST /api/v2/profile` (full update / upsert) | `PUT /api/v3/profile?channelTypeChannelId=` |
| `POST /api/v2/profile` (partial update) | `PATCH /api/v3/profile?channelTypeChannelId=` |
| Fetch by profile ID | `GET /api/v3/profile?id=` |
| Fetch by snType + snUserId | `GET /api/v3/profile?channelTypeChannelId=` |
| Fetch by snType + username | `GET /api/v3/profile?pageNumber=&channelType=&username=` |
| `POST /api/v2/profile/conversations` | `GET /api/v3/conversation?channelType=&channelId=` |
| `POST /api/v2/profile/bulk-update` | No V3 equivalent documented — continue on V2 |


The single biggest structural change: V2 overloaded one `POST` for create, full update, and partial update, disambiguated by payload shape and query flags. V3 splits these across `POST`, `PUT`, and `PATCH`.

### 7.2 Field mapping

| Aspect | API V2 | API V3 | Impact |
|  --- | --- | --- | --- |
| Response structure | Flat response with `message`/`data`/`count` | Structured with `data`/`errors`/`metadata` | More consistent error handling |
| Field names | `snType`, `snId`, `snUserName`, `name` | `channelType`, `channelId`, `username`, `displayName` | More descriptive and consistent naming |
| Channel type | Numeric enum (`snType: 1`) | String enum (`channelType: "YOUTUBE"`) | More readable and maintainable |
| Error handling | HTTP status + `message` field | Structured `errors` array + HTTP status | Better error granularity |
| Pagination metadata | `hasMore`, `count` fields at root | Nested `metadata` object | Cleaner separation of concerns |
| Image field | `imageUrl` | `profileImageUrl` | More specific field naming |


Because V2 encoded `snType` as a numeric enum and V3 uses string enums, any persisted `snType` integers in your datastore need a translation table during the cutover.

### 7.3 Custom-property operation mapping

| V2 (bulk-update) | V3 (`PATCH`) | Semantics |
|  --- | --- | --- |
| `partnerCustomPropertiesAdded` | `addedPartnerCustomProperties` | Add values without removing existing ones |
| `partnerCustomProperties` | `selectivePartnerCustomProperties` | Set the listed properties to the supplied values |
| `partnerCustomPropertiesRemoved` | *No documented V3 equivalent* | Remove specific values |


Note the naming inversion: V2 used a `partnerCustomProperties…` suffix pattern, V3 uses an `…PartnerCustomProperties` prefix pattern. A find-and-replace migration will not work.

### 7.4 Migration steps

1. **Update the base URL.** Point at `https://api3.sprinklr.com/{env}/api/v3`.
2. **Split your write path by method.** Route creates to `POST`, authoritative full syncs to `PUT`, and incremental changes to `PATCH`.
3. **Reshape `PATCH` payloads.** Wrap contact changes in `profileUpdateRequestDTO.contactInfo` and workflow changes in `profileWorkflowUpdateRequestDTO`.
4. **Rename custom-property operations** per [§7.3](#73-custom-property-operation-mapping).
5. **Update parameter names.** Use `channelType` instead of `snType`, `username` instead of `snUserName`, `channelTypeChannelId` instead of separate `snType`/`snUserId` params.
6. **Update response parsing.** Access data through `response.data` instead of the response root.
7. **Handle the new error format.** Check the `response.errors` array for detailed error information.
8. **Update pagination logic.** Use `metadata.hasMore` instead of root-level `hasMore`; convert `start`/`rows` to `pageNumber = start / rows` and `pageSize = rows`.
9. **Update field mappings** per [§7.2](#72-field-mapping).


## 8. Supported channel types

| Channel Type | Description | Username format |
|  --- | --- | --- |
| `TWITTER` | Twitter/X profiles | `@handle` without the `@` symbol |
| `FACEBOOK` | Facebook profiles and pages | Username or page name |
| `INSTAGRAM` | Instagram profiles | Username without `@` |
| `YOUTUBE` | YouTube channels | Channel handle or custom URL |
| `LINKEDIN` | LinkedIn profiles and company pages | Profile username or company identifier |


Additional channel types (`EMAIL`, `SMS`, `WHATSAPP`, and others) are supported on the write endpoints. **`channelType` values are always uppercase and case-sensitive.** A truncated or lowercase value — for example `youtube` instead of `YOUTUBE` — will not resolve to a profile.

## 9. Use cases

### 9.1 Onboard a new customer with multiple social identities

**Scenario:** a customer signs up and you already know two of their YouTube channels.

Issue a single `POST /api/v3/profile` with both identities in `profiles[]`, as in [§4.1](#41-create-a-profile). One call, one Universal Profile, two linked social identities — no post-hoc merge step. Capture the returned profile `id` and store it; it is the cheapest addressing key for every later call.

### 9.2 Nightly CRM sync where the CRM is authoritative

**Scenario:** your CRM owns contact data and pushes the complete record each night.

Use `PUT /api/v3/profile?channelTypeChannelId=…` with the full document. Because the CRM is authoritative, replace semantics are correct here. Still `GET` first if Sprinklr-side agents can edit profile lists or workspace tags, otherwise the nightly `PUT` will erase their work.

### 9.3 Incremental enrichment from a scoring service

**Scenario:** a model scores customers hourly and writes an engagement tier plus any newly detected interests.

Use `PATCH`:

- Engagement tier must hold exactly one value → `selectivePartnerCustomProperties`
- Newly detected interests append to a list → `addedPartnerCustomProperties`


```json
{
  "profileWorkflowUpdateRequestDTO": {
    "selectivePartnerCustomProperties": {
      "_c_6512721b83353e6f3e80c1c5": ["tier-2"]
    },
    "addedPartnerCustomProperties": {
      "_c_64dcb892e32de6530b5a8dbf": ["outdoor-gear"]
    }
  }
}
```

No `GET` is needed and no unrelated field is at risk.

### 9.4 Correct a phone number reported by an agent

**Scenario:** an agent notes the phone number on file is wrong.

`PATCH` with only the changed field:

```json
{ "profileUpdateRequestDTO": { "contactInfo": { "phoneNo": "99909880891" } } }
```

Using `PUT` here would require reconstructing the entire profile document to change one string.

### 9.5 Resolve an inbound social handle to a Sprinklr profile

**Scenario:** an agent tool receives a mention from `@NicoleJohn41342` and needs the full customer record.

1. `GET /api/v3/profile?pageNumber=0&pageSize=10&channelType=TWITTER&username=NicoleJohn41342`
2. Read `data[0].id` — this is the Sprinklr profile ID.
3. `GET /api/v3/conversation?channelType=TWITTER&channelId={data[0].channelId}&sortKey=createdTime&sortOrder=DESC&pageSize=50` for the interaction history.


Username matching is **case-insensitive but requires an exact username match**. A partial handle returns `404`.

### 9.6 Hydrate a batch of profiles from a webhook feed

**Scenario:** a webhook delivers 40 profile IDs; you need display names and follower counts for a dashboard.

Use direct-fetch mode with comma-separated IDs rather than 40 individual calls:

```
GET /api/v3/profile?id=id1,id2,id3,...
```

This avoids the pagination path entirely and reduces round trips. Do not add `pageNumber` or `pageSize` to a direct-fetch call.

### 9.7 Page through a large result set

```javascript
let pageNumber = 0;
const pageSize = 100;               // maximum permitted
let all = [];

while (true) {
  const res = await get(
    `/api/v3/profile?pageNumber=${pageNumber}&pageSize=${pageSize}` +
    `&channelType=YOUTUBE&username=${username}`
  );

  if (res.errors && res.errors.length) {
    handleErrors(res.errors);       // V3 returns structured errors, not a message string
    break;
  }

  all = all.concat(res.data);

  if (!res.metadata.hasMore) break; // never test totalCount — it is always null
  pageNumber += 1;
}
```

### 9.8 Migrate thousands of profiles from a legacy platform

For a one-time backfill, do not loop `POST /api/v3/profile`. Use [`POST /api/v2/data-ingestion/ingest`](https://dev.sprinklr.com/bulk-profile) with `type: "PROFILE"` and a `.csv`/`.xlsx` at a public `fileUrl`. Register a `callbackUrl` with `callbackType: "PUBLIC_URL"` and reconcile against the `failedFileUrl` from the callback payload. If your source of truth is a legacy CRM ID rather than a social handle, set `identifierFields` to `PARTNER_CUSTOM_PROPERTIES` so matching happens on your custom field.

### 9.9 Audience segmentation and journey triggers

Per the [Update Profile List](https://dev.sprinklr.com/update-profile-list) reference, that API is particularly useful for:

- **Audience segmentation.** Profiles can belong to multiple profile lists, so you can segment by demographics, behavior, and similar criteria for targeted marketing or personalized engagement.
- **Triggering a journey based on profile lists.** Add profiles to a list configured as a journey trigger — for example, adding all users who recently made a purchase to a list that starts a post-purchase journey.


Resolve list IDs first via the Bootstrap endpoint; the API takes numeric IDs, not names.

## 10. Caveats and best practices

**Method selection**

- Default to `PATCH`. Reach for `PUT` only when you hold the complete authoritative document.
- `PUT` replaces. Any `profiles[]` entry, profile list, or workspace workflow absent from the body is at risk.
- `PUT` and `PATCH` take different body schemas. Payloads are not interchangeable.


**Identifiers**

- `channelType` is uppercase and case-sensitive.
- The `channelTypeChannelId` separator is a tilde: `YOUTUBE~webDLabs70`.
- URL-encode `channelId` values that contain reserved characters.
- Prefer the Sprinklr `id` where you have it — it is stable across channel handle changes.


**Pagination**

- Pages are **0-based**. The first page is `0`.
- Use `metadata.hasMore` to determine whether additional pages exist. Do not rely on `totalCount`.
- `totalCount` is `null` to optimize response time for large datasets.
- Maximum `pageSize` is **100**. Default is **20** if not specified.
- Use `pageSize` 20–50 for interactive UI pagination and larger values for bulk processing.


**Matching and data freshness**

- Username search is **case-insensitive but requires an exact username match**.
- Profile data is **periodically updated**. Real-time changes on the native channel may not be immediately reflected.


**Permissions**

- Users need `VIEW` permission for the `AUDIENCE_PROFILE` entity. A `403` almost always means a permission gap, not a malformed request.


**Rate limiting**

- The Profile endpoints are **subject to rate limiting**. Implement retry logic with exponential backoff.


**Parameter hygiene**

- Do not mix direct-fetch parameters (`id`, `channelTypeChannelId`) with pagination parameters.
- `id` and `channelTypeChannelId` are mutually exclusive.


**Error handling**

- Always inspect the `errors` array even on a `200` response — bulk endpoints report per-record failures inside `data[].success` while returning `200` overall.


*All JSON payloads in this guide are illustrative examples derived from the supplied specifications and published references. They are not real customer data and are not guaranteed production responses. All credentials are placeholders (`{{accessToken}}`, `{{apiKey}}`) and must never be committed or logged.*