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.
- SS Salesforce Service CloudTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- 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.
What you need
Section titled “What you need”- 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.
1. Get API access in Salesforce
Section titled “1. Get API access in Salesforce”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).
- From Setup, enter External Client Apps in the Quick Find box and create a new app.
- Enable OAuth settings and add the Manage user data via APIs (api) scope.
- Select Enable Client Credentials Flow.
- In the app’s policies, under Client Credentials Flow, set Run As to the integration user.
- Copy the consumer key and consumer secret.
Then request an access token. The response includes access_token and instance_url (connect using OAuth):
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.
2. Pull new case emails
Section titled “2. Pull new case emails”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, CreatedDateFROM EmailMessageWHERE ParentId != null AND CreatedDate >= 2026-10-13T00:00:00ZORDER BY CreatedDateSOQL 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.
Messaging sessions
Section titled “Messaging sessions”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.
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Salesforce | Notes |
|---|---|---|
text | EmailMessage TextBody | Plain text. Cut the quoted thread. Skip empty values. |
userId | Case 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. |
incoming | EmailMessage Incoming | true for a received email. |
sessionId | EmailMessage ParentId (the case id) | Salesforce ids have no colons. |
conversationId | Same as sessionId | |
dimensionlabs_timestamp | EmailMessage CreatedDate | Convert with toMillis. |
platformJson | Case CaseNumber, Origin, Status, AccountId; EmailMessage Subject | AccountId 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.
4. Send it
Section titled “4. Send it”// 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;}5. Run it
Section titled “5. Run it”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-01sends 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.
Sales data: opportunities and accounts
Section titled “Sales data: opportunities and accounts”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).
- 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. - Keep
AccountId: the case emails above carry the same id inplatformJson.account_id, which is what lets the agent join the two. Leave out contact names and emails. - 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.
Salesforce reference
Section titled “Salesforce reference”- Authorization through external client apps or connected apps
- Create an external client app for the OAuth 2.0 client credentials flow
- Connect using OAuth (token request)
- Query resource
- Execute a SOQL query
- SOQL date formats
- System fields
- EmailMessage
- Case
- MessagingSession
- ConversationEntry
- Conversation entries (Connect REST API)
Checked against Salesforce’s documentation on 2026-10-13.