# Sprinklr

> Send the messages matched by your Sprinklr Listening Topics to Dimension Labs with the universal API.

This brings in the messages your Sprinklr Listening Topics already collect: public posts, comments and reviews about your brand across the channels you listen to, each one as a customer message. You run it on your side: a small script pulls from Sprinklr's Listening API and posts each message to the [universal API](/api/tracker).

## What you need

- A Sprinklr account with **Listening**, and Listening API access switched on. Sprinklr says Listening is not enabled for the API by default: ask Sprinklr Support, when Listening is part of the use case on your License Order Form. Without it, the API answers "Streaming in listening is not enabled for you. Please contact support".
- A Sprinklr user who can see the Topics you want. The API key inherits that user's permissions, and a Topic the user can't see returns nothing rather than an error.
- Your Sprinklr environment (`{env}`, for example `prod2`). Ask your Sprinklr admin if you don't know it.
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

**Text Sprinklr can't pass on.** Some channels' resyndication policies, X in particular, stop Sprinklr from returning the text over the API. Those messages come back with an empty `content.text` and `"apiStatus": "Removed fields because of resyndication policy"`. Sprinklr says enabling full X (Twitter) syndication needs a request to Sprinklr Support. The code below skips messages with no text.

## 1. Get API access in Sprinklr

Sprinklr's steps are in [Generate an API Key and Secret](https://developer.sprinklr.com/guides/authentication).

1. Sign in to your Sprinklr environment and open **Developer Tools**. (Key generation has moved there from the Developer Portal.)
2. Select **Create App**, or open an existing app. In its **Credentials** section, select **Generate API Key** and copy the API key and secret.
3. Generate an access token from the same screen. Store the key, secret and token in a secrets manager.
4. Send every request with `Authorization: Bearer ACCESS_TOKEN` and `Key: API_KEY`. Check your setup with `GET https://api3.sprinklr.com/{env}/api/v2/me`.
5. Find your Topic ids with the Listening Topic APIs. The query takes ids, never names.

## 2. Pull new messages

```bash
curl -X POST "https://api3.sprinklr.com/$SPRINKLR_ENV/api/v3/listening/query/stream" \
  -H "Authorization: Bearer $SPRINKLR_ACCESS_TOKEN" -H "Key: $SPRINKLR_API_KEY" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"sinceTime":1759276800000,"untilTime":1761436800000,"timeField":"SYSTEM_CREATED_TIME",
       "details":{"widgetType":"STREAM"},"filters":[{"dimension":"TOPIC","filterValues":["TOPIC_ID"]}],
       "metric":"MENTIONS","sortKey":"SYSTEM_CREATED_TIME","messageFormatOptions":"strip_html","rows":1000,"start":0}'
```

- `POST /api/v3/listening/query/stream` returns the messages themselves. `sinceTime` and `untilTime` are epoch **milliseconds**.
- **Incremental:** Sprinklr recommends `timeField: "SYSTEM_CREATED_TIME"` for a 15-minute poll. It windows on when Sprinklr took the message in, so a post from three days ago that Sprinklr picked up five minutes ago is not missed. The cursor is the newest `createdTime` (Sprinklr's ingestion time).
- **Pagination:** page with `start` and `rows` while `data.hasMore` is `true`. One request returns at most 1,000 messages, and you can't fetch more than 10,000 messages in the same time window, so the code splits a busier window in two.
- `messageFormatOptions: "strip_html"` strips HTML from the text.
- Sprinklr can report errors in the `errors` array of a `200`, so the code checks it on every answer.
- **Rate limits (Sprinklr's):** a sliding window set by your account tier: Standard 60 requests a minute (3,000 an hour, 72,000 a day), Professional 120, Enterprise 300, or a custom limit. A `429` comes with `Retry-After` and `X-RateLimit-Reset`.

## 3. Map the fields

Each message is one incoming message under its own `sessionId`. Messages with `brandPost: true` come from your own accounts and are skipped.

| Dimension Labs | Sprinklr | Notes |
|---|---|---|
| `text` | `content.text` | Skip empty text; check `apiStatus` to see why it is empty. |
| `userId` | `messageId` | No author id is documented on Listening messages, so use the message's id. |
| `incoming` | | Always `true`. |
| `sessionId` | `messageId` | Sprinklr's unique message id, prefixed: `sprinklr-ACCOUNT_600049002_...`. |
| `conversationId` | `messageId` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `channelCreatedTime` | When the message was created on its channel, in milliseconds. Falls back to `createdTime`. |
| `platformJson` | `channelType`, `subType`, `accountType`, `language`, `channelMessageId`, `sourceId` | `channelType` tells the networks apart; `subType` tells a post from a comment, reply or mention. |

**Personal data.** Don't send `senderProfile` fields or `permalink`, which can hold handles. Message text can hold names and order details: redact it before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// sprinklr.mjs. Environment: SPRINKLR_ENV (for example prod2), SPRINKLR_API_KEY, SPRINKLR_ACCESS_TOKEN,
// SPRINKLR_TOPIC_IDS (comma-separated Listening Topic ids), DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = `https://api3.sprinklr.com/${process.env.SPRINKLR_ENV}/api/v3`;
const ROWS = 1000; // Sprinklr's maximum per request
const id = (x) => String(x).replace(/:/g, '-');

// Retries Sprinklr's rate limits (429) and server errors (5xx), honoring Retry-After or X-RateLimit-Reset.
async function post(path, body) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${API}${path}`, { method: 'POST', body: JSON.stringify(body), headers: {
      Authorization: `Bearer ${process.env.SPRINKLR_ACCESS_TOKEN}`, Key: process.env.SPRINKLR_API_KEY,
      'Content-Type': 'application/json', Accept: 'application/json' } });
    if (res.ok) {
      const json = await res.json();
      if (json.errors?.length) throw new Error(`Sprinklr returned errors: ${JSON.stringify(json.errors)}`); // Sprinklr can report errors on a 200
      return json;
    }
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Sprinklr returned ${res.status}: ${await res.text()}`);
    const reset = Number(res.headers.get('x-ratelimit-reset')) * 1000 - Date.now();
    await sleep(Number(res.headers.get('retry-after')) * 1000 || (reset > 0 ? reset + 1000 : 2 ** attempt * 1000));
  }
}

// Every message Sprinklr took into the Topics between `since` and `until` (Sprinklr's own ingestion time).
// One time window can't page past 10,000 messages, so a busier window is split in two.
async function* stream(since, until) {
  const topics = process.env.SPRINKLR_TOPIC_IDS.split(',').map((x) => x.trim()).filter(Boolean);
  const all = [];
  for (let start = 0; ; start += ROWS) {
    if (start >= 10000) {
      const mid = since + Math.floor((until - since) / 2);
      if (mid === since) throw new Error('Too many Sprinklr messages in one millisecond to page through.');
      yield* stream(since, mid);
      yield* stream(mid, until);
      return;
    }
    const { data } = await post('/listening/query/stream', { sinceTime: since, untilTime: until, timeField: 'SYSTEM_CREATED_TIME',
      details: { widgetType: 'STREAM' }, filters: [{ dimension: 'TOPIC', filterValues: topics }], metric: 'MENTIONS',
      sortKey: 'SYSTEM_CREATED_TIME', messageFormatOptions: 'strip_html', rows: ROWS, start });
    all.push(...(data?.result ?? []));
    if (!data?.hasMore) break;
  }
  yield all;
}

// Sends every Listening message Sprinklr took in at or after `since` (Unix ms). Returns the new `since`.
export async function syncSprinklr({ since, historical = false }) {
  let newest = since;
  for await (const batch of stream(since, Date.now())) {
    for (const m of batch) {
      newest = Math.max(newest, m.createdTime ?? since);
      const text = m.content?.text?.trim();
      if (!text || m.brandPost) continue; // empty text (see apiStatus), or a post by one of your own accounts
      const ts = toMillis(m.channelCreatedTime || m.createdTime);
      const sessionId = `sprinklr-${id(m.messageId)}`;
      await sendToDimensionLabs({
        text,
        userId: sessionId,
        incoming: true,
        sessionId,
        conversationId: sessionId,
        dimensionlabs_timestamp: ts,
        platformJson: { channelType: m.channelType, subType: m.subType, accountType: m.accountType, language: m.language,
          channelMessageId: m.channelMessageId, sourceId: m.sourceId },
      }, { historical: historical || ts < Date.now() - HOUR });
    }
  }
  return newest;
}
```

## 5. Run it

Save the code as `sprinklr.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/scheduled-sync) runner: `import { syncSprinklr as sync } from './sprinklr.mjs';`.

- **History first:** `node run.mjs 2026-01-01` sends every message Sprinklr took into the Topics since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends new messages to `/track`, and they are enriched automatically every day. Anything more than an hour old when the job sees it goes to `/trackhistorical`, and needs a backfill.
- **Re-runs are safe:** duplicates are removed automatically.

If you have messages in a file instead, [upload it](/data/upload-a-file) and check it first with the [file checker](/data/check-a-file), then run a backfill.

## Sprinklr reference

- [Generate an API Key and Secret](https://developer.sprinklr.com/guides/authentication)
- [Listening Query API V3](https://developer.sprinklr.com/guides/developer-guides-v3/listening-api-v3-developer-guide)
- [Listening Topic APIs V3](https://developer.sprinklr.com/guides/topics/listening-topics)
- [Rate limiting](https://developer.sprinklr.com/guides/rate-limiting)
- [Message API V3](https://developer.sprinklr.com/guides/developer-guides-v3/message-api-v3-developer-guide)

Checked against Sprinklr's documentation on 2026-10-26.
