# Data Ingestion and Media Upload API V3

**Data Ingestion** loads external data — profiles and messages — into Sprinklr in bulk from Excel files. Once your first-party data is inside Sprinklr, it becomes subject to the same AI, listening, and automation as natively collected data, and it appears in the same unified dashboards.

**Media Upload** is the supporting API that gets your spreadsheet to a public URL in the first place. Data Ingestion does not accept a file — it accepts a **`fileUrl`**. Media Upload is how you produce one.

## The two-call workflow

```
1. POST /api/v3/media/upload          → upload the .csv/.xlsx, receive { url }
2. POST /api/v3/dataIngestion/ingest  → submit { fileUrl: <that url>, ... }, receive { taskId }
3. (asynchronous)                     → Sprinklr POSTs a completion payload to your callbackUrl
```

Data ingestion is **asynchronous**. The `200` you get back is an acknowledgement carrying a `taskId`, not a result. The actual outcome — how many records were created, how many failed, and where to download each set — arrives later at your `callbackUrl`.

> **If you do not implement a callback receiver, you will not learn whether the import succeeded.** The synchronous response contains no record-level information at all.


### One endpoint, two ingestion types

Bulk Profile and Bulk Messages are **not separate endpoints**. They are the same endpoint, differentiated by the `type` field in the request body.

| Use case | `type` value | V2 page |
|  --- | --- | --- |
| Import profiles | `PROFILE` or `INFLUENCER_PROFILE` | [Bulk Profile](https://dev.sprinklr.com/bulk-profile) |
| Import messages | `LISTENING_MESSAGE` | [Bulk Messages](https://dev.sprinklr.com/bulk-messages) |


`type` values are **case-sensitive**. Define them as constants in your own code.

## Before you start

### The template must exist in the Sprinklr UI

Neither ingestion type works from a bare spreadsheet. You must first establish a **template** in the UI that maps your file's column headers to Sprinklr fields. This is a one-time, per-template manual step and it cannot be automated through these APIs.

**For profiles:**

1. Log in to the Sprinklr UI and click **Profile** within **Modern Engagement**.
2. Click the **Import** icon on the top right.
3. Click **Download Excel Template** to download the template needed for this API.
4. Fill in the profile details in the template and upload the file onto the web.


**For messages:**

1. Log in to the Sprinklr UI and click **First Party Data Ingestion** within **Modern Research**.
2. Click **Import from Excel** on the top right.
3. Upload the Excel file from your local machine.
4. Enter the unique file name and map the Sprinklr fields to the headers used in the Excel file.


Once you map the file headers within Sprinklr and save the mapping under a unique name, you can reuse the same file shape to import more data. You reference that template by `templateName` in every subsequent API call and never repeat the UI step for that file shape.

### Profile ingestion is an upsert

Each profile has a unique ID. If the system finds a profile with that ID in the spreadsheet it updates the profile; otherwise it creates one. There is no separate update endpoint.

By default the key is the Sprinklr identifier. You can override this with [`identifierFields`](#identifierfields--matching-on-your-own-keys).

## Base URL

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

The resources covered by this guide are:

```
https://api3.sprinklr.com/{env}/api/v3/media/upload
https://api3.sprinklr.com/{env}/api/v3/dataIngestion/ingest
```

Replace `{env}` with your assigned environment identifier: `prod`, `prod0`, `prod2`, `prod3`, `prod4`, `prod5`, `prod6`, `prod8`, `prod11`, `prod12`, `prod15`, `prod16`, `prod17`, `prod18`, `prod19`, `prod21`, or `prod25`.

See [APIs | Sprinklr Developer Portal](https://dev.sprinklr.com/apis) for the published environment list. Make the base URL a single configuration value rather than hard-coding the host.

> **Environment-scoped credentials.** API keys and access tokens are scoped to a single environment. A key generated for one environment returns `401` against another even when the request is otherwise perfect.


> **Content-store URLs are environment-specific too.** The `url` returned by Media Upload belongs to the environment you uploaded to. You must feed it to the ingestion endpoint **in that same environment**.


## Authentication

All calls are authenticated with OAuth 2.0. See [API Overview](https://dev.sprinklr.com/api-overview) for portal registration, API key and secret generation, and the Authorize flow.

### Required headers

| 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 | `dataIngestion/ingest` |
| `Content-Type` | `multipart/form-data; boundary=<calculated when request is sent>` | Declares a file upload body | `media/upload` |
| `Accept` | `application/json` | Declares the acceptable response type | All requests |


Both `Authorization` and `Key` are required on every call. Sending only one returns `401`.

> **Do not set `Content-Type` manually on multipart uploads.** The boundary token is computed by your HTTP client at send time. If you hard-code the header without a matching boundary, the request body cannot be parsed and you will get a `400`. In curl, `-F` sets the header for you; in most SDKs, attaching a file does the same.


> **One token pair per API key.** Generating a new access token for an API key invalidates the previously issued token and refresh token for that key. Two services sharing one key will knock each other offline. Give each service its own key.


> **Credential hygiene.** Never commit an access token or API key, never paste one into a ticket, and never log the `Authorization` or `Key` header values. Every credential on this page is a placeholder.


## Upload a file

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

Uploads a media asset as an input stream into the content store and returns a URL you can pass to the ingestion endpoint.

> **Content store is not Asset Manager.** Uploading gives you a stored file and a URL. It does **not** create a managed asset in the Sprinklr Social Asset Manager. For ingestion that is exactly what you want — you only need the URL. If you want the file to appear in Asset Manager, make a separate [Asset Create](https://dev.sprinklr.com/create-asset) call.


### Query parameters

| Parameter | Required | Type | Description |
|  --- | --- | --- | --- |
| `assetType` | **Yes** | String | The type of asset being uploaded, for example `file`, `image`, or `video` |
| `fileName` | **Yes** | String | The name of the file to upload, **including its extension** — for example `customers.xlsx` |


### Form-data body

| Field | Required | Description |
|  --- | --- | --- |
| `file` | **Yes** | Source of the uploaded media content |


### Example request

```bash
curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/media/upload?assetType=file&fileName=customers.xlsx' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json' \
  -F 'file=@"/path/to/customers.xlsx"'
```

### Example response

```json
{
  "data": {
    "id": "608676dbc2952f59abc797af",
    "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "name": "customers.xlsx",
    "url": "https://{env}-assets.sprinklr.com/{env}-cdata/DAM/50001/00000000-0000-0000-0000-000000000000-0000000000/customers.xlsx",
    "previewUrl": "",
    "size": 15511
  },
  "errors": []
}
```

**`data.url` is the value you pass as `fileUrl` to the ingestion endpoint.** That is the join between the two APIs.

### Response fields

| Field | Type | Description |
|  --- | --- | --- |
| `id` | String | Unique identifier for the uploaded content |
| `mimeType` | String | The asset type and format, for example `image/png` |
| `name` | String | The file name |
| `url` | URL | The URL of the uploaded content on the Sprinklr platform |
| `previewUrl` | URL | The preview URL of the uploaded content |
| `size` | Integer | The size of the uploaded content in **bytes** |
| `width` | Integer | Width of the uploaded content. Returned for image and video asset types |
| `height` | Integer | Height of the uploaded content. Returned for image and video asset types |
| `previewWidth` | Integer | Preview width. Returned for the image asset type |
| `previewHeight` | Integer | Preview height. Returned for the image asset type |


`previewUrl` is an empty string for non-previewable types such as spreadsheets, and populated for images.

> **Uploading multiple files.** V3 exposes a single upload path. Call `POST /api/v3/media/upload` once per file rather than using the V2 `/media/upload/multiple` path.


## Submit an ingestion task

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

> **The path segment is `dataIngestion` — camelCase, not `data-ingestion`.** This is the change most likely to break a V2 integration. See [Migrating from V2](#migrating-from-v2).


### Request body

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `type` | String | **Yes** | Type of ingestion — `PROFILE`, `INFLUENCER_PROFILE`, or `LISTENING_MESSAGE` |
| `name` | String | **Yes** | Unique task name |
| `fileName` | String | **Yes** | Name of the file |
| `fileUrl` | String | **Yes** | URL of the file to load data from — the `data.url` returned by Media Upload |
| `templateName` | String | **Yes** | Name of the template to use for ingestion, as saved in the UI |
| `callbackUrl` | String | **Yes** | Callback URL to receive notification on completion |
| `importTag` | String | **Yes** | Tag for filtering |
| `callbackType` | String | Required when `callbackUrl` is provided | Type of callback to use |
| `callbackUrlHeaders` | Object (string → string) | No | Headers sent with the callback request, for authenticating the call into your receiver |
| `identifierFields` | Object (string → array of strings) | No | Your own unique identifiers, used to match existing data instead of Sprinklr identifiers. `PROFILE` only |


> **Send all eight of the fields marked Required above on every call.** `fileName` and `importTag` are easy to omit if you are porting a V2 payload — include both.


### `callbackType` values

| Value | Notes |
|  --- | --- |
| `PUBLIC_URL` | Used in the examples below |
| `AUDIENCE_DATA_CONNECTOR` |  |
| `OAUTH2_FAILED_FILE_UPLO` |  |


### `identifierFields` — matching on your own keys

Use this when your system of record has its own customer key and you do not want to store Sprinklr IDs on your side:

```json
"identifierFields": {
  "PARTNER_CUSTOM_PROPERTIES": [
    "crm_customer_id"
  ]
}
```

Constraints: supported only when `"type": "PROFILE"`, and `PARTNER_CUSTOM_PROPERTIES` is the only supported key.

### Example — profile ingestion

```bash
curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/dataIngestion/ingest' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "type": "PROFILE",
    "name": "crm-sync-2026-09-02",
    "fileName": "customers.xlsx",
    "fileUrl": "https://{env}-assets.sprinklr.com/{env}-cdata/DAM/50001/00000000-0000-0000-0000-000000000000-0000000000/customers.xlsx",
    "importTag": "crm-nightly",
    "templateName": "crm-profile-template",
    "callbackUrl": "https://your-host.example.com/hooks/sprinklr-ingest",
    "callbackType": "PUBLIC_URL",
    "callbackUrlHeaders": {
      "X-Webhook-Token": "{{callbackSecret}}"
    },
    "identifierFields": {
      "PARTNER_CUSTOM_PROPERTIES": [
        "crm_customer_id"
      ]
    }
  }'
```

### Example — message ingestion

```bash
curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/dataIngestion/ingest' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "type": "LISTENING_MESSAGE",
    "name": "reviews-import-2026-09-02",
    "fileName": "messages.xlsx",
    "fileUrl": "https://{env}-assets.sprinklr.com/{env}-cdata/DAM/50001/00000000-0000-0000-0000-000000000000-0000000000/messages.xlsx",
    "importTag": "reviews-archive",
    "templateName": "reviews-template",
    "callbackUrl": "https://your-host.example.com/hooks/sprinklr-ingest",
    "callbackType": "PUBLIC_URL"
  }'
```

The only differences from the profile call are `type` and the absence of `identifierFields`.

### Example response

```json
{
  "data": "{\"taskId\":\"6033961866934b5708a5f90a\"}",
  "errors": []
}
```

> **`data` is a JSON-encoded *string*, not an object.** The value is `"{\"taskId\":\"...\"}"` — a string containing escaped JSON. **Parse it twice**: once for the response envelope, then again for the inner payload.


```python
envelope = response.json()
inner = json.loads(envelope["data"])   # second parse — required
task_id = inner["taskId"]
```

Store the `taskId`. It is your only correlation handle, and it reappears as the `id` on the callback payload.

## The callback contract

`callbackUrl` is how you learn the outcome of an ingestion task.

> **Your endpoint must return `200` to an empty POST.** Sprinklr verifies the URL before using it. A receiver that requires a well-formed body, or returns `400`/`422` on an empty one, fails verification and you will never receive results.


> **TLS certificates must be valid.** If callback URL verification is failing, check with the Sprinklr team that valid certificates are installed.


### The callback payload

```json
{
  "id": "6033961866934b5708a5f40a",
  "clientId": 108,
  "userId": 1312,
  "totalRecords": 3,
  "failedRecordsCount": 0,
  "createdRecordsCount": 3,
  "updatedRecordsCount": 0,
  "successfulFileUrl": "https://{env}-assets.sprinklr.com/EXPORT/5/00000000-0000-0000-0000-000000000000-0000000000/Records_6033963f66934b5708a5f4.csv",
  "failedFileUrl": "https://{env}-assets.sprinklr.com/EXPORT/5/00000000-0000-0000-0000-000000000000-0000000000/Records_6033963f66934b5708a5f4.csv",
  "startTime": 1613993498264,
  "completionTime": 1613993535613
}
```

| Field | Meaning |
|  --- | --- |
| `id` | The same value as the `taskId` from the synchronous response |
| `clientId` / `userId` | The Sprinklr client and user under which the task ran |
| `totalRecords` | Rows processed |
| `createdRecordsCount` | New records created |
| `updatedRecordsCount` | Existing records updated |
| `failedRecordsCount` | Rows that failed |
| `successfulFileUrl` | Contains rows successfully created, with the Sprinklr ID in the last column |
| `failedFileUrl` | Contains rows that failed, with the failure reason in the last column |
| `startTime` / `completionTime` | Epoch **milliseconds** |


**`successfulFileUrl` is how you map your rows back to Sprinklr IDs** — the ID is appended as the last column. `failedFileUrl` gives you the per-row failure reason in the same position. **These two URLs are the only per-record diagnostics the API offers.**

> **Do not put a long-lived secret in `callbackUrlHeaders` without rotation.** It is stored with the task and replayed on every callback.


## Status codes

| HTTP code | Scenario |
|  --- | --- |
| `200 OK` | Success. Ingestion returns a `taskId`; upload returns content metadata |
| `400 Bad Request` | Missing required field, unknown `type`, malformed multipart body, or unreachable `fileUrl` |
| `401 Unauthorized` | Invalid or missing `Authorization` token or `Key` header |
| `403 Forbidden` | Caller lacks permission to ingest data or upload media |
| `404 Not Found` | Wrong path — most often `data-ingestion` instead of `dataIngestion`. Also an unknown `templateName` |


### The error object

| Field | Type | Description |
|  --- | --- | --- |
| `id` | String | Error identifier |
| `code` | Integer | Numeric error code |
| `message` | String | Error key, for example `account.not.found` |


### Troubleshooting

| Symptom | Likely cause | Fix |
|  --- | --- | --- |
| `404` on every ingestion call | Using `data-ingestion` (kebab) instead of `dataIngestion` (camelCase) | Change the path. **This is the number-one V3 migration failure** |
| `400` naming `fileName` | `fileName` is required in V3 | Add `fileName` |
| `400` naming `importTag` | `importTag` is required | Add `importTag` |
| `400` on `type` | Value not one of `PROFILE`, `INFLUENCER_PROFILE`, `LISTENING_MESSAGE`, or wrong case | Use one of the three exactly |
| `400` on `templateName` | Template does not exist, or the name does not match exactly | Re-check the name saved in the UI |
| Client fails deserializing `data` | Ingestion returns `data` as a JSON-**encoded string**, not an object | Parse twice |
| `400` on multipart upload | `Content-Type` hard-coded without a matching boundary | Let the HTTP client set the header |
| Upload succeeds, ingestion `400`s on `fileUrl` | The content-store URL is environment-specific | Upload and ingest in the same `{env}` |
| Callback never arrives | Receiver does not return `200` to an empty POST, or the TLS certificate is invalid | Verify both |
| Callback arrives, results look wrong | You are reading the synchronous response, not the callback payload | Per-record counts are callback-only |
| Cannot tell which rows failed | The synchronous response has no per-record data | Download `failedFileUrl` — the reason is in the last column |
| `401` with all headers present | Credentials belong to a different environment | Regenerate for the target environment |
| `401` after previously working | Token expired (30-day default), or a second token was issued for the same key | Refresh; one key per service |
| Uploaded file not in Asset Manager | The content store is not Asset Manager | Call `Asset Create` separately |


## Migrating from V2

### The namespace change

**This is the one change that will break your integration.**

```diff
- POST https://api3.sprinklr.com/{env}/api/v2/data-ingestion/ingest
+ POST https://api3.sprinklr.com/{env}/api/v3/dataIngestion/ingest
```

The `data-ingestion` kebab-case prefix is replaced with the `dataIngestion` camelCase namespace. If your V3 calls are returning `404` while your V2 calls still work, this is almost certainly why.

### Endpoint mapping

| V2 | V3 |
|  --- | --- |
| `POST /api/v2/data-ingestion/ingest` | `POST /api/v3/dataIngestion/ingest` |
| `POST /api/v2/media/upload` | `POST /api/v3/media/upload` |
| `POST /api/v2/media/upload/multiple` | Call `POST /api/v3/media/upload` once per file |


### Field mapping

**No request field was renamed, removed, or retyped.** Every V2 field — `type`, `name`, `fileUrl`, `importTag`, `templateName`, `callbackUrl`, `callbackType`, `callbackUrlHeaders`, `identifierFields` — is present in V3 with the same name and type.

V3 adds **`fileName`**, which is required.

**So the migration is: change the path, add `fileName`.** Everything else is unchanged.

### Migration checklist

1. **Change `data-ingestion` to `dataIngestion` in the path.** The single most important step.
2. **Change `v2` to `v3` in the version segment.**
3. **Add `fileName`** to every request body.
4. **Keep sending `importTag`.**
5. **Verify your `data` parsing handles a JSON-encoded string**, not an object.
6. **Re-test the callback receiver** — it must return `200` to an empty POST with a valid TLS certificate.
7. **Confirm your media-upload path.** If you used `/media/upload/multiple`, switch to one call per file.
8. **Confirm upload and ingestion target the same `{env}`** — content-store URLs are environment-scoped.
9. **Leave templates alone.** Templates saved in the UI carry over; `templateName` semantics are unchanged.


## Common tasks

### Nightly CRM to Sprinklr profile sync

Export changed customers to `.xlsx`, then:

```
POST /api/v3/media/upload?assetType=file&fileName=customers-2026-09-02.xlsx
  → data.url

POST /api/v3/dataIngestion/ingest
  { "type": "PROFILE", "name": "crm-sync-2026-09-02", "fileName": "customers-2026-09-02.xlsx",
    "fileUrl": "<data.url>", "importTag": "crm-nightly",
    "templateName": "crm-profile-template",
    "callbackUrl": "https://your-host.example.com/hooks/sprinklr-ingest",
    "callbackType": "PUBLIC_URL" }
  → { "taskId": "..." }
```

Because profile ingestion upserts, the same job handles both new and changed customers. Use a dated `name` so tasks are traceable, and a stable `importTag` so all runs of this job can be filtered together.

### Match on your own customer key

If you do not want to store Sprinklr IDs, add `identifierFields` so matching happens on your key instead:

```json
"identifierFields": { "PARTNER_CUSTOM_PROPERTIES": ["crm_customer_id"] }
```

`PROFILE` only, and `PARTNER_CUSTOM_PROPERTIES` is the only supported key.

### Import historical messages for listening and AI

Load an archive of support tickets or reviews as `LISTENING_MESSAGE` so Sprinklr AI, listening, and automation apply to them and they appear alongside native data in the same dashboards.

Message ingestion requires a template mapped through **First Party Data Ingestion** in Modern Research.

### Reconcile results and retry failures

Do not treat the `200` as success. On callback:

```
if failedRecordsCount > 0:
    download failedFileUrl        # last column = failure reason
    fix rows, re-upload, re-ingest
download successfulFileUrl        # last column = Sprinklr ID → store the mapping
```

`successfulFileUrl` is the only way to map your rows back to Sprinklr IDs.

### Handle re-runs safely

Make `name` unique per run — a timestamp or UUID — and rely on the upsert key (the Sprinklr ID, or `identifierFields`) to prevent duplicate records if a job is replayed.

### Attach images or documents to a workflow

Media Upload is useful independently of ingestion. Upload once, then reference `data.url` wherever the platform accepts a media URL. Remember that this does **not** create an Asset Manager asset — call [Asset Create](https://dev.sprinklr.com/create-asset) if you need that.

## Best practices

**Path and namespace**

- The path is `dataIngestion`, camelCase. `data-ingestion` returns `404`.
- Keep the base path in one configuration constant.


**Request body**

- Send `type`, `name`, `fileName`, `fileUrl`, `templateName`, `callbackUrl`, and `importTag` on every call.
- `type` accepts `PROFILE`, `INFLUENCER_PROFILE`, and `LISTENING_MESSAGE`, and is case-sensitive.
- Never send an empty `fileUrl`.


**Asynchrony**

- The `200` is an acknowledgement, not a result. Per-record outcomes arrive only at the callback.
- Your callback receiver must return `200` to an empty POST and present a valid TLS certificate.
- Always download `failedFileUrl` when `failedRecordsCount > 0`; the reason is in the last column.
- Store the `taskId` — it is the `id` on the callback payload and your only correlation handle.


**Response parsing**

- Ingestion returns `data` as a **JSON-encoded string**. Parse twice.
- Media Upload returns `data` as a real object. The two endpoints differ.
- Always inspect the `errors` array, not just the HTTP status.
- `startTime` and `completionTime` are epoch **milliseconds**.


**Media upload**

- Let your HTTP client set the multipart `Content-Type` boundary.
- The form field name is `file`.
- Send `fileName` with its extension.
- Content-store URLs are environment-specific — upload and ingest in the same `{env}`.
- Uploading does not create an Asset Manager asset.


**Templates and permissions**

- The template must be created in the UI first; this cannot be automated through these APIs.
- `templateName` must match exactly.
- A `403` is a permission gap, not a malformed request.


**Volume**

- Add backoff around bulk jobs, and keep individual files to a size your own pipeline can re-drive on failure.


## Related

- [Data Ingestion (V2)](https://dev.sprinklr.com/data-ingestion)
- [Bulk Profile (V2)](https://dev.sprinklr.com/bulk-profile)
- [Bulk Messages (V2)](https://dev.sprinklr.com/bulk-messages)
- [Media Upload](https://dev.sprinklr.com/media-upload)
- [Create Asset](https://dev.sprinklr.com/create-asset)
- [API Overview](https://dev.sprinklr.com/api-overview)