Skip to content

02 Get your data in /Integrations

LivePerson Conversational Cloud

Send LivePerson Conversational Cloud messaging conversations to Dimension Labs with the universal API.

  1. LC LivePerson Conversational 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 each LivePerson messaging conversation as one conversation: what the consumer wrote and what your agents and bots replied, across web, app, SMS and social channels. You run it on your side: a small script pulls from LivePerson’s Messaging Interactions API and posts each message to the universal API.

  • A Conversational Cloud user with Administrator permissions, to create an OAuth 2.0 application.
  • Your LivePerson account ID (your site ID), and the client_id and client_secret of that application.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

LivePerson recommends OAuth 2.0 client credentials for machine-to-machine access, and the Messaging Interactions API supports it (OAuth 2.0 client credentials).

  1. Log in to Conversational Cloud with Administrator permissions and open Manage > Management Console.
  2. Search for “Application Install”, select it, then click Proceed to Configure.
  3. Click Create Application and configure a client_credentials application. For the Messaging Interactions API, use the scope data.services.external.msghist (Messaging Interactions API overview).
  4. Save. Select edit on the new application in the list to copy its client_id and client_secret.

Your LivePerson account team can also install the application for you.

To get an access token, look up your authorization server with the Domain API (service name sentinel), then post your credentials to it:

Terminal window
curl -X POST "https://SENTINEL_DOMAIN/sentinel/api/v2/account/$LP_ACCOUNT_ID/app/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=$LP_CLIENT_ID&client_secret=$LP_CLIENT_SECRET&grant_type=client_credentials"

The response holds an access_token. Send it as Authorization: Bearer ACCESS_TOKEN.

The Messaging Interactions API lives on an account-specific domain. Look it up with the Domain API, using the service name msgHist:

Terminal window
curl "https://api.liveperson.net/api/account/$LP_ACCOUNT_ID/service/msgHist/baseURI.json?version=1.0"

Then search conversations by start time:

Terminal window
curl -X POST "https://MSGHIST_DOMAIN/messaging_history/api/account/$LP_ACCOUNT_ID/conversations/search?offset=0&limit=100&sort=start:asc&source=DimensionLabsSync" \
-H "Authorization: Bearer $LP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"start":{"from":1760313600000,"to":1760918400000},"contentToRetrieve":["messageRecords","consumerParticipants"]}'
  • start is required: epoch milliseconds, at most three months per request, rounded to the nearest 10 minutes.
  • limit goes up to 100. Page with offset; _metadata.count is the total number of matches.
  • source is required: a name for your process, 20 characters at most.
  • When you send contentToRetrieve, list every type you want, including default ones such as messageRecords.
  • LivePerson keeps conversations for 13 months. The API can lag by up to a minute.
  • If a conversation’s info.isPartial is true, its data was truncated. Get it in full with Get conversation by conversation ID.
  • The API returns 429 when you send too many requests. LivePerson’s docs don’t publish the limits.
Dimension LabsLivePersonNotes
textmessageRecords[].messageData.msg.textSkip messages whose audience is AGENTS_AND_MANAGERS: those are private messages consumers can’t see. Skip messages with no text, such as files.
userIdconsumerParticipants[].participantIdLivePerson’s id for the consumer, on every message of the conversation. Fall back to the sessionId.
incomingmessageRecords[].sentBytrue when sentBy is Consumer. Agents and bots are Agent.
sessionIdinfo.conversationIdPrefix it, for example liveperson-e5c58e49-....
conversationIdinfo.conversationIdSame value as sessionId.
dimensionlabs_timestampmessageRecords[].timeLAlready in milliseconds.
platformJsoninfo.source, info.device, info.latestSkillName, info.status, info.mcs, info.csatRate; message dialogIdThe skill name lets you group by team or queue.

Personal data. Don’t send consumerParticipants[].firstName, lastName, email or phone, or the agent names and login names in info. Messages can hold names, addresses and account numbers: redact them before you send. See PII redaction.

// liveperson.mjs. Environment: LP_ACCOUNT_ID, LP_CLIENT_ID, LP_CLIENT_SECRET, DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const WINDOW = 89 * DAY; // LivePerson allows at most three months per search
const SOURCE = 'DimensionLabsSync'; // LivePerson asks each caller to name itself, 20 characters at most
const CONTENT = ['messageRecords', 'consumerParticipants'];
const account = process.env.LP_ACCOUNT_ID;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Retries rate limits (429) and server errors (5xx), honoring Retry-After.
async function call(url, options = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, options);
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`LivePerson returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
const domain = async (service) =>
(await call(`https://api.liveperson.net/api/account/${account}/service/${service}/baseURI.json?version=1.0`)).baseURI;
async function getToken() {
const sentinel = await domain('sentinel');
const body = new URLSearchParams({ client_id: process.env.LP_CLIENT_ID, client_secret: process.env.LP_CLIENT_SECRET, grant_type: 'client_credentials' });
const res = await call(`https://${sentinel}/sentinel/api/v2/account/${account}/app/token`, {
method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body,
});
return res.access_token;
}
// Posts a search. Gets a new token once if the current one has expired (401).
let token = null;
async function search(url, body) {
for (let fresh = false; ; fresh = true) {
token ??= await getToken();
try {
return await call(url, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
} catch (err) {
if (fresh || !String(err.message).includes(' 401')) throw err;
token = null;
}
}
}
// Sends every ended conversation started at or after `since` (Unix ms). Returns the new `since`.
export async function syncLivePerson({ since, historical = false }) {
const base = `https://${await domain('msgHist')}/messaging_history/api/account/${account}/conversations`;
const now = Date.now();
let newest = since;
let holdAt = Infinity; // the start of the oldest conversation that is still open
for (let from = since; from < now; from += WINDOW) {
const to = Math.min(from + WINDOW, now);
for (let offset = 0; ; ) {
const page = await search(`${base}/search?offset=${offset}&limit=100&sort=start:asc&source=${SOURCE}`, { start: { from, to }, contentToRetrieve: CONTENT });
const records = page.conversationHistoryRecords ?? [];
for (let r of records) {
const started = r.info.startTimeL;
if (r.info.status !== 'CLOSE' && started > now - DAY) { holdAt = Math.min(holdAt, started); continue; } // send it once it ends
if (r.info.isPartial) {
const full = await search(`${base}/conversation/search?source=${SOURCE}`, { conversationId: r.info.conversationId, contentToRetrieve: CONTENT });
r = full.conversationHistoryRecords?.[0] ?? r;
}
const id = `liveperson-${r.info.conversationId}`;
const consumer = r.consumerParticipants?.[0]?.participantId;
for (const m of r.messageRecords ?? []) {
const text = m.messageData?.msg?.text;
if (m.audience === 'AGENTS_AND_MANAGERS' || !text?.trim()) continue;
const ts = toMillis(m.timeL);
await sendToDimensionLabs({
text,
userId: consumer ?? id,
incoming: String(m.sentBy).toLowerCase() === 'consumer',
sessionId: id,
conversationId: id,
dimensionlabs_timestamp: ts,
platformJson: { source: r.info.source, device: r.info.device, skill: r.info.latestSkillName, status: r.info.status, mcs: r.info.mcs, csat: r.info.csatRate, dialogId: m.dialogId },
}, { historical: historical || ts < Date.now() - HOUR });
}
newest = Math.max(newest, started);
}
offset += records.length;
if (!records.length || offset >= (page._metadata?.count ?? 0)) break;
}
}
return Math.min(newest, holdAt);
}

A conversation that is still open is skipped, and the cursor stays at its start, so the next run picks it up once it has closed. Later conversations that already closed are sent straight away; when the cursor is held, they come round again on the next run and the duplicates are removed automatically. A conversation left open for more than a day is sent as it is, so one forgotten conversation can’t hold the cursor back.

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

  • History first: node run.mjs 2025-11-01 sends every conversation since that date to /trackhistorical. LivePerson keeps 13 months, so start no earlier than that. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends closed conversations to /track, and they are enriched automatically every day. Because a conversation is sent once it closes, the first messages of a conversation that ran for more than an hour go to /trackhistorical, and need a backfill.
  • Re-runs are safe: duplicates are removed automatically.

The code waits whenever LivePerson returns 429. You can also upload a file of exported conversations with one row per message (check it with the file checker), then run a backfill.

Checked against LivePerson’s documentation on 2026-10-26.