# Audit API V3

The **Audit API** provides the audit changelog for a Sprinklr asset — a record of modifications made over time, showing **who changed what, and when**. It is a governance component that offers visibility into changes across the platform. The same information is visible in the Sprinklr platform under the **Activity** tab.

For each change the API returns the **old and new values** of the affected field, so it supports compliance review, change forensics, and building an external audit trail.

## Overview

Audit V3 exposes two operations:

| Operation | Method | Path | Purpose |
|  --- | --- | --- | --- |
| Search the audit trail | `POST` | `/api/v3/audit/search` | Initial fetch: audit records for one or more asset IDs |
| Fetch the next page | `GET` | `/api/v3/audit?id={cursorId}` | Continue a result set that exceeded `limit` |


These are two halves of one workflow, not two independent features. You always start with the search call; you call the cursor endpoint only if the search response returned a `cursor`.

### Asset classes

An **asset class** is the kind of object being audited — features present on Sprinklr's platform, such as Account, Outbound Message, Case, or Profile.

Every request pairs an asset class with one or more asset IDs belonging to that class. The API then returns the change history for those assets.

| Asset class | Description |
|  --- | --- |
| `UNIVERSAL_CASE` | Cases |
| `MESSAGE_WORKFLOW` | Message workflow records |
| `PROFILE_WORKFLOW` | Profile workflow records |
| `MEDIA_ASSET` | Media assets |
| `OUTBOUND_MESSAGE` | Outbound messages |
| `USER` | Users |
| `SPR_TASK` | Tasks |


> **Asset class and asset IDs must correspond.** The IDs you send must belong to the class you name. Start with `UNIVERSAL_CASE` when validating your integration — it is the value used in the worked examples below.


### The cursor pagination model

Audit uses **cursor pagination**, not offset pagination:

1. Call `POST /api/v3/audit/search` with a `limit`.
2. If more records exist beyond that limit, the response includes a **`cursor`** string.
3. Call `GET /api/v3/audit?id={cursor}` to fetch the next page.
4. Each page returns its own `cursor`. Repeat until the result set is exhausted.


The cursor is an opaque server-side handle. You do not construct it, and you cannot skip to an arbitrary page.

**The cursor request carries no asset context.** `GET /api/v3/audit?id=…` takes only the cursor — the server already knows which asset IDs, asset class, order, and limit the original search used. The cursor fully encodes the query, which is why you must never mix a cursor from one search into another.

> **Write your pagination loop defensively.** Treat an absent `cursor`, a `null` cursor, **and** an empty-string cursor as terminal, and add a maximum-iteration guard.


## Base URL

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

The two audit resources are:

```
https://api3.sprinklr.com/{env}/api/v3/audit/search
https://api3.sprinklr.com/{env}/api/v3/audit?id={cursorId}
```

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 so promotion between environments is a configuration change, not a code change.

> **Environment-scoped credentials.** API keys and access tokens are scoped to a single environment. A key generated for `prod0` returns `401 Unauthorized` against `prod19` even when the request is otherwise perfectly formed. When you get an unexpected `401`, verify the environment before debugging the payload.


## 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 | `POST /audit/search` |
| `Accept` | `application/json` | Declares the acceptable response type | All requests |


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

`Content-Type` is only meaningful on the `POST`, since the `GET` has no body.

> **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. If two services share one key and both refresh, they will knock each other offline. Give each service its own key.


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


## Search the audit trail

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

### Request body parameters

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `assetIds` | Array of strings | **Yes** | List of asset IDs whose audit trail you want |
| `assetClass` | String | **Yes** | The asset class the IDs belong to |
| `limit` | Integer | No | Number of results in the response. Default `50`, maximum `200` |
| `order` | String | No | Sorting order — `ASC` or `DESC`. Defaults to `DESC` |
| `from` | Integer | No | Time after which audits are required, in epoch milliseconds |
| `upto` | Integer | No | Time before which audits are required, in epoch milliseconds |


### `assetIds` is plural, and an array of strings

The field is **`assetIds`** — plural — and it takes an array. Send the IDs as **quoted strings**:

```json
"assetIds": [ "10520954" ]
```

Batch multiple IDs into a single call rather than looping one asset at a time.

> **Do not confuse `assetIds` with `assetId`.** The *request* field is `assetIds` (plural, array). The *response* field on each record is `assetId` (singular, string). They are different fields.


### `from` and `upto` — bound the query to a time window

These optional fields let you bound an audit query to a time range instead of pulling the entire history of an asset:

- `from` — time after which audits are required
- `upto` — time before which audits are required


Both are epoch milliseconds, matching the `auditDate` values in the response.

This is the most efficient way to run an incremental export. A daily compliance job that currently pulls a full history and filters client-side can push the window server-side instead.

### `limit`

|  | Value |
|  --- | --- |
| Default | `50` |
| Maximum per call | `200` |


Set `limit` explicitly on every call rather than relying on the default.

### `order`

| Value | Meaning |
|  --- | --- |
| `DESC` | Newest first (default) |
| `ASC` | Oldest first |


Define these as constants in your own code — the values are case-sensitive.

### Example request

```bash
curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/audit/search' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "assetIds": [
      "10520954"
    ],
    "assetClass": "UNIVERSAL_CASE",
    "order": "DESC",
    "limit": 2
  }'
```

### Example response

```json
{
  "data": {
    "results": [
      {
        "assetClass": "UNIVERSAL_CASE",
        "assetId": "10520954",
        "auditDate": 1650461298712,
        "userId": 0,
        "changes": [
          {
            "fieldName": "Messages",
            "oldValues": [],
            "newValues": [
              "New value 1"
            ]
          }
        ]
      },
      {
        "assetClass": "UNIVERSAL_CASE",
        "assetId": "10520954",
        "auditDate": 1650461298594,
        "userId": 0,
        "changes": [
          {
            "fieldName": "Message Sentiment",
            "oldValues": [],
            "newValues": [
              "New value 2"
            ]
          }
        ]
      }
    ],
    "cursor": "638e12816d414169dc5ad755"
  },
  "errors": []
}
```

`limit` was `2` and a `cursor` came back, so more data is available.

Note the ordering: `1650461298712` precedes `1650461298594`, which is **descending** by `auditDate` — consistent with the requested `"order": "DESC"`.

## Fetch the next page by cursor

```
GET https://api3.sprinklr.com/{env}/api/v3/audit?id={cursorId}
```

### Query parameters

| Parameter | In | Type | Description |
|  --- | --- | --- | --- |
| `id` | query | String | Cursor ID from a previous audit search response |


> **The parameter is named `id`, not `cursorId`.** Always send it — a request with no cursor has no defined result.


### Example request

```bash
curl -X GET \
  'https://api3.sprinklr.com/{env}/api/v3/audit?id=638e12816d414169dc5ad755' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

### Example response

```json
{
  "data": {
    "results": [
      {
        "assetClass": "UNIVERSAL_CASE",
        "assetId": "10520954",
        "auditDate": 1650452106126,
        "userId": 0,
        "changes": [
          {
            "fieldName": "Messages",
            "oldValues": [],
            "newValues": [
              "Test1"
            ]
          }
        ]
      }
    ],
    "cursor": "627e334a25033713a3cb8887"
  },
  "errors": []
}
```

The response shape is identical to the search response, including its own `cursor` for the page after this one.

**Send the cursor you last received.** Cursors are single-use and scoped to one query — never persist them across runs or reuse one from a different search.

## Response format

```json
{
  "data": {
    "results": [ ... ],
    "cursor": "..."
  },
  "errors": []
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `data.results` | Array | Array listing the details of the response |
| `data.results[].assetClass` | String | Asset class for which the request was made |
| `data.results[].assetId` | String | The asset ID from the request |
| `data.results[].auditDate` | Epoch milliseconds | The date on which the change was recorded |
| `data.results[].userId` | Integer | The ID of the user who made the changes |
| `data.results[].changes` | Array | Lists the change details |
| `data.results[].changes[].fieldName` | String | The name of the field that was modified |
| `data.results[].changes[].oldValues` | Array of strings | The old value of the field |
| `data.results[].changes[].newValues` | Array of strings | The new or changed value of the field |
| `data.cursor` | String | Returned when the response has more data than the requested `limit`. Use it to fetch the next set of data |
| `errors` | Array | Array of error objects. Empty on success |


### Reading `auditDate`

`auditDate` is **epoch milliseconds** — not seconds, and not an ISO-8601 string:

| Raw value | UTC |
|  --- | --- |
| `1650461298712` | 2022-04-20T13:28:18.712Z |
| `1650461298594` | 2022-04-20T13:28:18.594Z |
| `1650452106126` | 2022-04-20T10:55:06.126Z |


> **Parsing `auditDate` as seconds yields a date in 1970.** This is the most common integration error against this API.


Consecutive records can be **milliseconds apart** — the two values above differ by 118 ms. A single user action can produce several audit records, so do not assume one record per user interaction, and **do not use `auditDate` as a unique key**.

### Reading `changes`

`changes` is an array — one entry per field modified in that audit event. Each entry has `fieldName`, `oldValues`, and `newValues`. Both value fields are **arrays**, because a field can hold multiple values.

An empty `oldValues` with a populated `newValues` indicates a value being **set for the first time** rather than replaced. A modification of an existing value populates both.

### The error object

When `errors` is non-empty, each entry has this shape:

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


Error responses return `data` as `null`.

> **Always inspect the `errors` array, not just the HTTP status.** The `message` field is a dotted key suitable for branching. Do not branch on human-readable text.


## Status codes

Both operations return the same set:

| HTTP code | Scenario |
|  --- | --- |
| `200 OK` | Success. Audit records returned |
| `400 Bad Request` | Malformed body, missing `assetIds` or `assetClass`, invalid `assetClass`, or a malformed cursor |
| `401 Unauthorized` | Invalid or missing `Authorization` token or `Key` header |
| `403 Forbidden` | Caller lacks permission to read the audit trail for that asset |
| `404 Not Found` | The asset or the cursor does not exist |


### Troubleshooting

| Symptom | Likely cause | Fix |
|  --- | --- | --- |
| `401` with all headers present | Credentials belong to a different environment | Confirm the key and token were generated for the environment in the base URL |
| `401` after previously working | Access token expired (30-day default), or a second token was generated for the same API key | Refresh the token; give each service its own key |
| `401` and only one auth header sent | Both `Authorization` and `Key` are required | Send both |
| `400` citing `assetIds` | Sent `assetId` (singular) instead of `assetIds` | Use `assetIds`, and send it as an array |
| Client-side type error on `assetIds` | Sent integers where strings are expected | Quote the IDs: `["10520954"]` |
| `400` on `assetClass` | The asset class does not match the IDs sent | Confirm the IDs belong to the class you named |
| `404` on the cursor call | Cursor expired, already consumed, or copied from a different search | Re-run the search from the beginning; do not persist cursors |
| Pagination never terminates | Your loop checks for only one terminal form | Treat absent, `null`, and `""` as terminal; add a max-iteration guard |
| Dates decode to 1970 | `auditDate` is epoch **milliseconds**, parsed as seconds | Parse as milliseconds |
| Duplicate-looking records | Multiple audit records can share one user action, milliseconds apart | Do not use `auditDate` as a unique key |
| More records than expected | `limit` defaults to `50` | Set `limit` explicitly on every call |


## Migrating from V2

### Endpoint mapping

| V2 | V3 |
|  --- | --- |
| `POST /api/v2/audit/fetch` | `POST /api/v3/audit/search` |
| `GET /api/v2/audit?id={cursorId}` | `GET /api/v3/audit?id={cursorId}` |


```diff
- POST https://api3.sprinklr.com/{env}/api/v2/audit/fetch
+ POST https://api3.sprinklr.com/{env}/api/v3/audit/search

- GET  https://api3.sprinklr.com/{env}/api/v2/audit?id={cursorId}
+ GET  https://api3.sprinklr.com/{env}/api/v3/audit?id={cursorId}
```

### Field mapping

| V2 | V3 | Change |
|  --- | --- | --- |
| `assetIds` (body) | `assetIds` (body) | None |
| `assetClass` (body) | `assetClass` (body) | None |
| `order` (body) | `order` (body) | None |
| `limit` (body) | `limit` (body) | None |
| — | `from` (body) | **New in V3** — lower time bound |
| — | `upto` (body) | **New in V3** — upper time bound |
| `id` (query, cursor) | `id` (query, cursor) | None |


**The request and response contracts are otherwise unchanged.** Apart from the path and the two new optional time-window fields, a working V2 integration becomes a working V3 integration by changing `v2` to `v3` and `fetch` to `search`.

### Migration checklist

1. **Update the paths** — `/api/v2/audit/fetch` becomes `/api/v3/audit/search`, and change `v2` to `v3` on the cursor call.
2. **Keep sending `assetIds` as an array.**
3. **Quote your asset IDs** as strings.
4. **Set `limit` explicitly.**
5. **Define `order` as a constant set** (`ASC` / `DESC`) in your own code.
6. **Adopt `from` and `upto`** if you currently pull full histories and filter client-side.
7. **Harden the pagination loop** to treat absent, `null`, and empty-string cursors as terminal.
8. **Leave response parsing alone** — `results`, `cursor`, and the record shape are unchanged from V2.


## Common tasks

### Find who changed a case, and when

A case was resolved incorrectly and you need the change history.

```json
{
  "assetIds": ["10520954"],
  "assetClass": "UNIVERSAL_CASE",
  "order": "DESC",
  "limit": 50
}
```

`DESC` puts the most recent change first, which is usually what you want when investigating a recent incident.

### Export a full history for a compliance archive

Send up to the maximum `limit` of `200`, then follow `cursor` until the result set is exhausted. Use `ASC` so the archive reads chronologically:

```json
{
  "assetIds": ["10520954", "10520955", "10520956"],
  "assetClass": "UNIVERSAL_CASE",
  "order": "ASC",
  "limit": 200
}
```

`assetIds` accepts multiple IDs in one call — batch them rather than looping one asset at a time.

### Run an incremental nightly sync

You mirror the audit trail into a data warehouse and only want what is new. Store the previous run's high-water mark and pass it as `from`:

```json
{
  "assetIds": ["10520954"],
  "assetClass": "UNIVERSAL_CASE",
  "from": 1650452106126,
  "upto": 1650461298712,
  "order": "ASC",
  "limit": 200
}
```

This is materially cheaper than pulling the full history and filtering client-side. Prefer a slightly overlapping window with de-duplication so that records written exactly on a boundary are never missed.

### Detect changes to a specific field

There is **no server-side field filter**. Fetch the audit trail for the asset and filter `changes[].fieldName` client-side. Narrow the volume first with `from` and `upto` rather than filtering a full history.

### Reconstruct an asset's state at a point in time

Fetch with `ASC` order and `upto` set to your target timestamp, then replay `changes[].newValues` forward. Because `oldValues` is also returned, you can equally walk backward from the current state.

### Page through a large result set

```
POST /api/v3/audit/search   { "assetIds": ["10520954"], "assetClass": "UNIVERSAL_CASE", "limit": 200 }
  → results[…200], cursor "638e12816d414169dc5ad755"

GET  /api/v3/audit?id=638e12816d414169dc5ad755
  → results[…200], cursor "627e334a25033713a3cb8887"

GET  /api/v3/audit?id=627e334a25033713a3cb8887
  → results[…], cursor absent/null/empty  → stop
```

Send the cursor you last received, not one from a previous run. Guard the loop with a maximum iteration count.

### Feed a governance dashboard

The same information is available in the platform under the **Activity** tab. Use this API when you need that data **outside** Sprinklr — in a SIEM, a compliance warehouse, or an internal audit tool. If your users are already working in Sprinklr, the Activity tab needs no integration at all.

## Best practices

**Request fields**

- The request field is `assetIds` — plural, and an array.
- Quote asset IDs as strings.
- Batch multiple IDs into one call rather than looping.
- Always set `limit` explicitly. Maximum is `200`; default is `50`.
- `order` accepts `ASC` and `DESC` and defaults to `DESC`.


**Asset classes**

- Asset class and asset IDs must correspond — the IDs must belong to the class you name.


**Time windows**

- `from` and `upto` are epoch milliseconds, matching `auditDate`.
- Use an overlapping window with de-duplication for incremental ingestion.


**Cursors**

- Treat the cursor as opaque. Do not parse, construct, or increment it.
- Cursors are single-use and query-scoped. Never mix a cursor from one search into another.
- Do not persist cursors across runs; restart the search instead.
- Handle absent, `null`, and empty-string cursors as terminal, and add a max-iteration guard.


**Timestamps**

- `auditDate` is epoch **milliseconds**. Parsing as seconds yields 1970.
- Records can be milliseconds apart. `auditDate` is not a unique key.


**Rate limiting**

- Cursor loops are the most likely trigger for rate limiting. Add backoff and avoid tight paging loops.


**Permissions**

- Audit data is governance data. A `403` is a permission gap, not a malformed request.


## Related

- [Audit API (V2)](https://dev.sprinklr.com/audit-api)
- [Fetch Audit Details by Cursor (V2)](https://dev.sprinklr.com/fetch-audit-details-by-cursor)
- [API Overview](https://dev.sprinklr.com/api-overview)