Skip to content

02 Get your data in /Integrations

Sprout Social

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

  1. SS Sprout SocialTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

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.

  • 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.

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 or Google reviews guide instead.

Sprout’s steps are in its API documentation.

  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.
Terminal window
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.

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 LabsSprout SocialNotes
texttitle and textJoin them with a line break when both are present.
userIdfrom.guidThe author’s native profile id with Sprout’s prefix, colons replaced: sprout-fbpr-1112345. Fall back to the sessionId.
incomingfrom.guid, profile_guidfalse when they are equal (your profile wrote it), true otherwise.
sessionIdcase_id, else guidsprout-case-CASEID, or sprout-GUID. Sprout’s ids contain colons: replace them with -.
conversationIdsame as sessionId
dimensionlabs_timestampcreated_timeISO 8601 in UTC. Convert with toMillis.
platformJsonguid, network, post_type, post_category, customer_profile_id, case_id, language_codepost_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.

// 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.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
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.

Save the code as sprout-social.mjs in a project set up as in Set up your project, and run it with the 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 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 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, check it with the file checker, upload it, then run a backfill.

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