# Sprout Social

> Send the comments, mentions and messages in your Sprout Social inbox to Dimension Labs with the universal API.

This brings in the messages your Sprout Social profiles receive (comments, mentions, direct messages and reviews across your connected networks) and your team's replies within Sprout cases. You run it on your side: a small script pulls from the Sprout Social API and posts each message to the [universal API](/api/tracker).

## What you need

- **A Sprout plan that enables API access.** Sprout says to ask your sales representative or support if you aren't sure your plan includes it.
- A Sprout user with the **API Permissions** permission, who has accepted Sprout's Analytics API Terms of Service under **Settings > Global Features > API**.
- **For X data:** every Sprout user with API Permissions has to accept the Sprout API X Content End User License Agreement (shown at the top of the same API page), and Sprout says customers go through a brief X review and approval before they can reach X data through the API.
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

**What the API leaves out.** Sprout's API doesn't include paid (ad account) data, Listening data from X, or Listening message-level data from Reddit. It can't pass on Yelp, Trustpilot, Tripadvisor or Glassdoor reviews, and Google Business Profile (`GOOGLE_MY_BUSINESS_*`) messages are limited to the last 30 days. For reviews, use the [Trustpilot](/integrations/trustpilot) or [Google reviews](/integrations/google-reviews) guide instead.

## 1. Get API access in Sprout Social

Sprout's steps are in its [API documentation](https://api.sproutsocial.com/docs/).

1. Log in to Sprout and go to **Settings > Global Features > API**. Accept the Analytics API Terms of Service if you haven't.
2. In **API Token Management**, click **Generate API Token**, enter a name and copy the token. Store it securely. (Sprout also supports OAuth 2.0 machine-to-machine clients, which it recommends; the token is simpler for one script.)
3. Send it as `Authorization: Bearer YOUR_TOKEN` with `Accept` and `Content-Type` set to `application/json`.
4. Find your ids once: `GET https://api.sproutsocial.com/v1/metadata/client` returns your `customer_id`, and `GET https://api.sproutsocial.com/v1/CUSTOMER_ID/metadata/customer` lists each profile's `customer_profile_id` and the `groups` it belongs to.

## 2. Pull new messages

```bash
curl -X POST "https://api.sproutsocial.com/v1/CUSTOMER_ID/messages" \
  -H "Authorization: Bearer $SPROUT_API_TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"filters":["group_id.eq(78910)","customer_profile_id.eq(1234, 5678)","created_time.in(2026-10-01T00:00:00..2026-10-26T00:00:00)"],
       "fields":["guid","created_time","text","post_type","from.guid","profile_guid","case_id"],
       "limit":100,"sort":["created_time:asc"],"timezone":"Etc/UTC"}'
```

- The Messages endpoint returns messages published by or received by your profiles. Without `fields`, it returns only `guid`.
- **Incremental:** filter on `created_time.in(start..end)`. Dates without a time are read as midnight, in the `timezone` you send.
- **Pagination:** `limit` goes up to 100. Pass `paging.next_cursor` as `page_cursor` until a response has no `paging`. Only the next page can be fetched.
- `profile_guid` equals `from.guid` when your profile wrote the message. That is how the code tells your replies from the public's.
- The Messages endpoint returns no thread id. `case_id` is filled when a message belongs to a Sprout case, so cases are what group a customer's message with your reply.
- **Rate limits (Sprout's):** 60 requests per minute and 250,000 requests per month. A `202` means the answer isn't ready yet: retry it.

## 3. Map the fields

A message in a case joins that case's conversation. A message from the public outside a case is a conversation of its own.

| Dimension Labs | Sprout Social | Notes |
|---|---|---|
| `text` | `title` and `text` | Join them with a line break when both are present. |
| `userId` | `from.guid` | The author's native profile id with Sprout's prefix, colons replaced: `sprout-fbpr-1112345`. Fall back to the `sessionId`. |
| `incoming` | `from.guid`, `profile_guid` | `false` when they are equal (your profile wrote it), `true` otherwise. |
| `sessionId` | `case_id`, else `guid` | `sprout-case-CASEID`, or `sprout-GUID`. Sprout's ids contain colons: replace them with `-`. |
| `conversationId` | same as `sessionId` | |
| `dimensionlabs_timestamp` | `created_time` | ISO 8601 in UTC. Convert with `toMillis`. |
| `platformJson` | `guid`, `network`, `post_type`, `post_category`, `customer_profile_id`, `case_id`, `language_code` | `post_type` tells a comment (`FBCR_COMMENT`, `INSTAGRAM_GRAPH_COMMENT`, `YOUTUBE_COMMENT`) from a DM or a review. |

**Personal data.** Don't request `from.name`, `from.screen_name` or the `internal.sent_by` name and email fields. Messages can hold names, phone numbers and order details: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// sprout-social.mjs. Environment: SPROUT_API_TOKEN, SPROUT_CUSTOMER_ID, SPROUT_GROUP_ID,
// SPROUT_PROFILE_IDS (comma-separated customer_profile_id values in that group), DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = `https://api.sproutsocial.com/v1/${process.env.SPROUT_CUSTOMER_ID}`;
const id = (x) => String(x).replace(/:/g, '-'); // Sprout ids look like "fbpr:123"; Dimension Labs ids can't hold colons
const FIELDS = ['guid', 'created_time', 'text', 'title', 'post_type', 'post_category', 'network', 'from.guid', 'profile_guid',
  'customer_profile_id', 'case_id', 'language_code'];

// Retries Sprout's rate limits (429), "not ready" answers (202) and server errors (5xx), honoring Retry-After when it is sent.
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.SPROUT_API_TOKEN}`, Accept: 'application/json', 'Content-Type': 'application/json' } });
    if (res.status === 200) return res.json();
    if ((res.status !== 202 && res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Sprout returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt * 2) * 1000);
  }
}

const sproutTime = (ms) => new Date(ms).toISOString().slice(0, 19); // 2026-10-01T00:00:00, read in the timezone below

// Sends every inbox message on your profiles created at or after `since` (Unix ms). Returns the new `since`.
export async function syncSproutSocial({ since, historical = false }) {
  let newest = since, cursor;
  const profiles = process.env.SPROUT_PROFILE_IDS.split(',').map((x) => x.trim()).filter(Boolean).join(', ');
  do {
    const page = await post('/messages', {
      filters: [`group_id.eq(${process.env.SPROUT_GROUP_ID})`, `customer_profile_id.eq(${profiles})`,
        `created_time.in(${sproutTime(since)}..${sproutTime(Date.now())})`],
      fields: FIELDS, limit: 100, sort: ['created_time:asc'], timezone: 'Etc/UTC', ...(cursor && { page_cursor: cursor }) });
    for (const m of page.data ?? []) {
      const ts = toMillis(m.created_time);
      newest = Math.max(newest, ts);
      const text = [m.title, m.text].filter((x) => x?.trim()).join('\n');
      if (!text) continue;
      const mine = Boolean(m.from?.guid) && m.from.guid === m.profile_guid; // Sprout: equal when your profile wrote it
      if (mine && !m.case_id) continue; // no thread id outside a case, so your post or reply has nothing to join
      const sessionId = m.case_id ? `sprout-case-${id(m.case_id)}` : `sprout-${id(m.guid)}`;
      await sendToDimensionLabs({
        text,
        userId: mine ? `sprout-${id(m.profile_guid)}` : m.from?.guid ? `sprout-${id(m.from.guid)}` : sessionId,
        incoming: !mine,
        sessionId,
        conversationId: sessionId,
        dimensionlabs_timestamp: ts,
        platformJson: { messageId: m.guid, network: m.network, postType: m.post_type, postCategory: m.post_category,
          customerProfileId: m.customer_profile_id, caseId: m.case_id, language: m.language_code },
      }, { historical: historical || ts < Date.now() - HOUR });
    }
    cursor = page.paging?.next_cursor;
  } while (cursor);
  return newest;
}
```

Your own posts and replies outside a case are skipped: with no thread id they have no conversation to join, and on their own they hold no customer words.

## 5. Run it

Save the code as `sprout-social.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 { syncSproutSocial as sync } from './sprout-social.mjs';`.

- **History first:** `node run.mjs 2026-01-01` sends every message 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.

Each page holds up to 100 messages and Sprout allows 60 requests a minute, so a large history takes a while, and every call counts toward the 250,000 a month. If your plan doesn't include API access, use a file instead. Sprout's [release notes](https://sproutsocial.com/insights/release/february-1-2018/) describe an **Export Inbox** option in the Smart Inbox gear menu, on its Advanced plan: filter the inbox to the messages you want, pick a date range, and Sprout emails a CSV. Map its columns as in step 3 with the [converter](/data/convert-a-file), check it with the [file checker](/data/check-a-file), [upload it](/data/upload-a-file), then run a backfill.

## Sprout Social reference

- [Sprout Social API documentation](https://api.sproutsocial.com/docs/) (Accessing the APIs, Messages Endpoint, Message Fields, Post Types, Rate Limits, Limitations)
- [Export Inbox message data (release note)](https://sproutsocial.com/insights/release/february-1-2018/)

Checked against Sprout Social's documentation on 2026-10-26.
