Skip to content

02 Get your data in /Integrations

Salesforce Service Cloud

Send Salesforce Service Cloud case emails to Dimension Labs with the universal API, plus messaging transcripts and opportunity data as files.

  1. SS Salesforce Service CloudTickets, 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 emails on your Service Cloud cases, one conversation per case. You run it on your side: a small script queries Salesforce with SOQL and posts each email to the universal API. Messaging transcripts and sales data (opportunities and accounts) come in as files: see Messaging sessions and Sales data.

  • A Salesforce admin who can create an external client app, and a user for it to run as with read access to Case and EmailMessage.
  • The app’s consumer key and consumer secret, used with the OAuth 2.0 client credentials flow.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Salesforce recommends external client apps for API access. Creating new connected apps is restricted as of Spring ‘26 (authorization through external client apps or connected apps).

  1. From Setup, enter External Client Apps in the Quick Find box and create a new app.
  2. Enable OAuth settings and add the Manage user data via APIs (api) scope.
  3. Select Enable Client Credentials Flow.
  4. In the app’s policies, under Client Credentials Flow, set Run As to the integration user.
  5. Copy the consumer key and consumer secret.

Then request an access token. The response includes access_token and instance_url (connect using OAuth):

Terminal window
curl -X POST https://YOUR_DOMAIN.my.salesforce.com/services/oauth2/token \
-d 'grant_type=client_credentials' \
-d "client_id=$SF_CLIENT_ID" -d "client_secret=$SF_CLIENT_SECRET"

Send the token as Authorization: Bearer ACCESS_TOKEN on every call.

Use the Query resource, GET /services/data/v67.0/query?q=SOQL, and filter on the CreatedDate system field to get only what’s new since your cursor.

EmailMessage records with a ParentId belong to a case, and Parent gives you the case’s fields:

SELECT Id, ParentId, Parent.CaseNumber, Parent.ContactId, Parent.AccountId, Parent.Origin, Parent.Status,
Incoming, TextBody, Subject, CreatedDate
FROM EmailMessage
WHERE ParentId != null AND CreatedDate >= 2026-10-13T00:00:00Z
ORDER BY CreatedDate

SOQL dateTime values take the form YYYY-MM-DDThh:mm:ssZ, without milliseconds (date formats). A query returns up to 2,000 records at a time. When done is false, request the nextRecordsUrl from the response to get the next batch (execute a SOQL query).

TextBody usually includes the earlier emails of the thread, quoted below the reply. The code below cuts each body at the first quoted-reply marker it recognizes, so each message carries only its new text. Check a few of your own emails: the markers depend on your customers’ email clients.

For standard SMS, WhatsApp and Facebook Messenger channels, each message is a ConversationEntry record whose ConversationId is the MessagingSession id. Salesforce notes that these records aren’t created until the session ends and the agent closes the session tab, except the first message of a session, and that for enhanced Messaging channels and Messaging for In-App and Web the Message field is blank or no record is created. Salesforce points to Data Cloud, the Connect REST API, or the Conversation Transcript Export tool for those transcripts. The Connect REST API conversation entries resource is meant for single conversations, not bulk export, and has its own hourly rate limit.

So for messaging, export transcripts with Salesforce’s Conversation Transcript Export and upload the file: one row per message, the MessagingSession id as sessionId, the session’s MessagingEndUserId as userId, and incoming true only when the entry’s ActorType is EndUser. Then run a backfill.

Dimension LabsSalesforceNotes
textEmailMessage TextBodyPlain text. Cut the quoted thread. Skip empty values.
userIdCase ContactId (via Parent.ContactId)Salesforce’s internal contact id. Fall back to the case id when it’s empty. Don’t use FromAddress: it is an email address.
incomingEmailMessage Incomingtrue for a received email.
sessionIdEmailMessage ParentId (the case id)Salesforce ids have no colons.
conversationIdSame as sessionId
dimensionlabs_timestampEmailMessage CreatedDateConvert with toMillis.
platformJsonCase CaseNumber, Origin, Status, AccountId; EmailMessage SubjectAccountId lets you join to account data, such as opportunities.

Personal data. Email bodies hold signatures, names and phone numbers: redact them before you send. See PII redaction.

// salesforce.mjs. Environment: SF_DOMAIN, SF_CLIENT_ID, SF_CLIENT_SECRET, DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const soqlTime = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
// Cuts the quoted earlier emails: "On ... wrote:", "-----Original Message-----", or a "From:" header line.
const newText = (body) => body.split(/\n(?:On .+wrote:|-{2,} ?Original Message ?-{2,}|From: .+)\s*\n/i)[0].trim();
async function token() {
const body = new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.SF_CLIENT_ID, client_secret: process.env.SF_CLIENT_SECRET });
const res = await fetch(`https://${process.env.SF_DOMAIN}.my.salesforce.com/services/oauth2/token`, { method: 'POST', body });
if (!res.ok) throw new Error(`Salesforce token request returned ${res.status}: ${await res.text()}`);
return res.json(); // { access_token, instance_url }
}
// Sends every case email created at or after `since` (Unix ms). Returns the new `since`.
export async function syncSalesforce({ since, historical = false }) {
const { access_token, instance_url } = await token();
// Retries server errors (5xx) and rate limits with a growing pause.
const get = async (url) => {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${access_token}` } });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Salesforce returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
};
const soql = `SELECT Id, ParentId, Parent.CaseNumber, Parent.ContactId, Parent.AccountId, Parent.Origin, Parent.Status, Incoming, TextBody, Subject, CreatedDate FROM EmailMessage WHERE ParentId != null AND CreatedDate >= ${soqlTime(since)} ORDER BY CreatedDate`;
let path = `/services/data/v67.0/query?q=${encodeURIComponent(soql)}`;
let newest = since;
while (path) {
const page = await get(instance_url + path);
for (const e of page.records) {
const text = e.TextBody ? newText(e.TextBody) : '';
if (!text) continue;
const ts = toMillis(e.CreatedDate);
await sendToDimensionLabs({
text,
userId: e.Parent?.ContactId ?? e.ParentId,
incoming: e.Incoming,
sessionId: e.ParentId,
conversationId: e.ParentId,
dimensionlabs_timestamp: ts,
platformJson: { case_number: e.Parent?.CaseNumber, origin: e.Parent?.Origin, status: e.Parent?.Status, account_id: e.Parent?.AccountId, subject: e.Subject },
}, { historical: historical || ts < Date.now() - HOUR });
newest = Math.max(newest, ts);
}
path = page.done ? null : page.nextRecordsUrl;
}
return newest;
}

Save the code as salesforce.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncSalesforce as sync } from './salesforce.mjs';.

  • History first: node run.mjs 2025-01-01 sends every case email since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new emails to /track, and they are enriched automatically every day. An email that is 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.

You can also upload a file of exported emails or transcripts (check it with the file checker), then run a backfill.

Opportunities, accounts and renewals are business data, not conversations. They are never enriched, and they come in by file upload only today (an API for business data is on the roadmap).

  1. Export the records to CSV, for example with a Salesforce report or Data Loader, or with SOQL such as SELECT Id, AccountId, StageName, Amount, CloseDate, Type FROM Opportunity.
  2. Keep AccountId: the case emails above carry the same id in platformJson.account_id, which is what lets the agent join the two. Leave out contact names and emails.
  3. In Data Sources, upload the file under Structured data. It is queryable within about a minute. See Business data format and Join business data.

To refresh it, upload a new export on your own schedule.

Checked against Salesforce’s documentation on 2026-10-13.