Skip to content

05 Reference

Universal API

Send messages to Dimension Labs from any system, one POST per message, to the real-time or the historical tracker.

The universal API takes messages from any system: a help desk, a chat platform, a contact center, a survey tool, a warehouse or your own app. You send one POST per message. Every universal API integration guide uses it. (Intercom has its own one-click connector and doesn’t need it.)

  1. S Your systemTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily
{
  "text": "My order never arrived",
  "userId": "cust-17",
  "incoming": true,
  "sessionId": "zd-48213",
  "dimensionlabs_timestamp": 1767299700000
}

You need three things:

  1. Your data, out of the source system. Read your source’s own API docs and find the endpoints that return messages, conversations, reviews or responses. Each integration guide does this for one vendor.
  2. An API key for a Dimension Labs source. See Your API key.
  3. A choice of endpoint: the real-time tracker for new messages, the historical tracker for older ones. See Endpoints.

Then convert each record to the conversation format and post it.

Each source has its own key:

  1. In the app, open Data Sources and add a source under Unstructured data.
  2. Choose the Universal API tile, for every guide on this site, and give the source a name, for example “Zendesk”.
  3. Open the new source’s integration instructions and copy its API key.

Keep the key in an environment variable or a secrets manager, never in code you commit. The examples read it from DL_API_KEY.

One source per system keeps your data easy to filter later: one for Zendesk, one for Qualtrics. Several surveys, locations or apps from the same vendor can share one source; the guides put the survey, location or app id in platformJson so you can tell them apart.

The Export API uses a different key, from Settings.

Use it forEndpoint
New messages, as they happenhttps://tracker.dimensionlabs.io/track
History: anything older than an hourhttps://tracker.dimensionlabs.io/trackhistorical

Both take the same query parameters and the same body.

ParameterValue
platformuniversal
v11.1.0-rest
typeincoming for a message from the customer or end user, outgoing for one from your bot, agent or company
apiKeyYour source’s API key. See Your API key.

Set the header Content-Type: application/json.

Which endpoint, and when Dimensions arrive

Section titled “Which endpoint, and when Dimensions arrive”
  • Send new messages to /track as they happen. A live job sending to /track is enriched automatically every day.
  • Send anything older than an hour to /trackhistorical. That rule is safe whatever the exact real-time limit is. Then start a backfill from the Backfill tab. A backfill typically shows up within the hour, depending on how much data is being enriched.
  • A file upload is treated like history: it needs a backfill too.

The helper takes { historical: true } to send to /trackhistorical. The integration guides decide per message: anything more than an hour old when the job sees it goes to /trackhistorical.

Rate limits and error bodies are not published. Send messages one at a time, or with low concurrency, and retry 429 and 5xx responses with exponential backoff, honoring Retry-After. The helper does both for you. Any other 4xx means the message itself was rejected: fix it rather than resend it unchanged.

The tracker was built for conversations, so every record has a direction. The rule is simple: anything written by a customer, user or other source outside your company is incoming. Anything your company, bot or agent wrote is outgoing. This holds even when the data doesn’t feel like a conversation:

SourceIncomingOutgoing
Chat, bot or ticketWhat the customer wroteWhat the bot or agent wrote
Online reviewThe reviewYour reply to it, if you have one
SurveyEach answerEach question
News article or forum postThe article or post

Set type in the URL and incoming in the body to match. If you use the helper, set only incoming: it sets type for you.

A minimal message:

{
"text": "Where is my order?",
"userId": "cust-17",
"incoming": true,
"sessionId": "conv-1001",
"conversationId": "conv-1001",
"dimensionlabs_timestamp": 1703240320000,
"platformJson": { "channel": "chat", "store_id": "SF-04" }
}
FieldRequiredTypeNotes
textYesstringThe words to enrich. Never empty.
userIdYesstringWho sent it. For a bot conversation, the end user’s id on both their messages and the bot’s. If you don’t have one, copy the sessionId here. Never an email address, phone number or name. See Participants.
incomingYesbooleantrue from the customer or end user, false from your side. Match it to type.
sessionIdYesstringGroups the messages of one conversation. Enrichment reads a whole session at once. No colons.
dimensionlabs_timestampYesnumberUnix time in milliseconds (13 digits). Seconds read as a date in 1970. See Timestamps.
conversationIdRecommendedstringKeeps a thread together when the sender changes, for example when a human agent takes over from a bot. No colons.
platformJsonOptionalobjectAny other fields about the message or conversation: channel, rating, store, plan, account id. Kept as metadata the agent can filter and join on. See Metadata.
platformUserJsonOptionalobjectAttributes of the customer, such as tier or locale. See Customer attributes.
intentOptionalobjectOnly for an intent-based chatbot: { "name": "TRACK_ORDER", "confidence": 0.94 }. See Intent.
imagesOptionalarray of objectsImages in the message. See Rich content.
buttonsOptionalarray of objectsButtons offered in the message.
postbackOptionalobjectA record of a button click.

Field names are case-sensitive: userId and sessionId, not USERID, SESSIONID, userid or sessionid. In dimensionlabs_timestamp, dimensionlabs counts as one word, so it stays lowercase. Field values can use any case, for example TRACK_ORDER.

Reviews and surveys use the same shape. A review is one incoming message with its own sessionId. For a survey, each question is an outgoing message and each answer an incoming one, under the response’s sessionId.

A line of a JSONL upload has exactly this shape too, nested platformJson included.

userId says who sent the message:

  • Bot conversations. Use the end user’s id on both incoming and outgoing messages, not the bot’s id.
  • Tickets and email. Use the customer’s id on every message of the ticket, the agents’ replies included. The integration guides do this.
  • Live chat with a human agent. You can give each participant their own id, as long as conversationId ties the conversation together.
  • Omni-channel sources. When one source carries records from several channels, keep each person’s id consistent across channels if you can, and put the channel in platformJson.
  • No personal data. Use the tool’s internal customer id. If the only key you have is an email address, send a consistent hash of it (the same hash on both sides of any join), never the address itself. See PII redaction.

incoming marks direction. Whatever the source, incoming: true is always the part of a record or transcript that came from the customer or end user. incoming: false covers your bot, system and human agents.

sessionId and conversationId often hold the same value, and in most data they cover the same span of messages. They still do different jobs:

conversationIdsessionId
What it groupsEvery message in a conversation, across all participantsOne session or interaction
What it controlsConversation identity. Keeps a thread with two or more userIds intact. Messages with the same conversationId always appear in the same transcript.Session boundaries: what gets enriched together

Always send conversationId. Without it, a session can split each time the userId changes, for example when a human agent joins a live chat.

Neither id can contain a colon (:). If you build an id from several values, join them with - or _.

Always set sessionId yourself, to the vendor’s own ticket, chat or response id, rather than relying on how sessions are cut without one. More on Sessions and conversations.

dimensionlabs_timestamp goes at the root of the JSON, as Unix time in milliseconds:

{
"dimensionlabs_timestamp": 1627996516303,
"text": "..."
}
  • Send the time the message was written in your source system, not the time your script runs, so conversations are ordered the way they happened.
  • The field is required: always send it.
  • Keep messages in the same session at least one second (1,000 ms) apart, so their order is unambiguous. When a source gives several messages the same time (survey answers, for example), add 1,000 ms per message.
  • To check a value by hand, paste it into an epoch converter such as epochconverter.com.

Duplicates you send are removed automatically when your data is ingested, so re-sending is safe. A retried run, a re-sent review, or a history load that overlaps your live job doesn’t create extra copies. Use the vendor’s own ids for sessionId and conversationId, so a re-sent message lands in the same conversation.

Terminal window
curl -X POST -H "Content-Type: application/json" \
-d '{"text":"Where is my order?","userId":"cust-17","incoming":true,"sessionId":"conv-1001","conversationId":"conv-1001","dimensionlabs_timestamp":1703240320000}' \
"https://tracker.dimensionlabs.io/trackhistorical?platform=universal&v=11.1.0-rest&type=incoming&apiKey=$DL_API_KEY"

This example’s timestamp is in 2023, so it goes to /trackhistorical. For a message with a current timestamp, use /track. An outgoing message is the same call with "incoming":false in the body and type=outgoing in the URL.

Send a few messages by hand before you write the integration. Any tool that can make an HTTP POST works: curl in a terminal on macOS, Windows or Linux, or an API client such as Postman or Insomnia. To send data at scale you will still write code (see Set up your project).

  1. Download, install and open Postman.
  2. Create a new Collection.
  3. In the collection, create a new request and choose HTTP.
  4. Set the request type to POST.
  5. In the URL box, enter https://tracker.dimensionlabs.io/track (or /trackhistorical for a timestamp older than an hour).
  6. Under Params, add platform = universal, v = 11.1.0-rest, type = incoming, and apiKey = your source’s API key.
  7. Under Headers, add Content-Type = application/json.
  8. Under Body, choose raw and JSON, and paste a message such as the minimal one above, with a current timestamp.
  9. Select Send. A 2xx status means the message was received.

Use the command in Send one message. For /track, replace the timestamp with a current one: date +%s000 in a terminal prints the time in milliseconds (to the whole second, which is fine). Set DL_API_KEY first.

Use a test source for this, so test messages don’t mix with real data. To start over, create a new source: you can’t delete individual messages yourself. For a targeted deletion on a source, email [email protected].

After a sync, ask the agent “How many messages arrived in my new source today?” Dimensions follow the timing in Which endpoint.

Every universal API guide imports two helpers from one file. Download it and put it next to your scripts:

sendToDimensionLabs(msg, { historical }) posts one message, to /track or /trackhistorical, and retries 429 and 5xx responses with backoff, honoring Retry-After. toMillis(date) turns any date into Unix milliseconds: seconds become milliseconds, numeric text is read as a number, and text without a time zone is read as UTC, not your computer’s local time.

Set up a Node project once:

Terminal window
mkdir dl-sync && cd dl-sync
curl -O https://docs.dimensionlabs.io/templates/dimensionlabs.mjs
npm init -y
npm pkg set type=module
npm install google-auth-library # only the vendor packages your guide lists
export DL_API_KEY=your-source-api-key

npm pkg set type=module lets Node run the guides’ import lines. Each guide lists the packages it needs, if any. Save the guide’s code as its own file, for example zendesk.mjs, and every guide starts with the same import:

import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';

To run it once, and then on a schedule, see Scheduled sync.

In Python the import is from dimensionlabs import send_to_dimension_labs, to_millis. The integration guides are written in JavaScript; to port one, follow its mapping table. Run Python 3.11 or later if your vendor’s dates use offsets such as +0000.

The JavaScript file, for reference:

// Dimension Labs helpers for the universal API. Save this file next to your script as dimensionlabs.mjs.
// Node 18 or later (uses the built-in fetch). Set DL_API_KEY in the environment.
// Docs: https://docs.dimensionlabs.io/api/tracker
const DL = 'https://tracker.dimensionlabs.io';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/**
* Send one message. New messages go to /track; pass { historical: true } for anything older than an hour,
* then run a backfill. Retries rate limits (429) and server errors (5xx) with exponential backoff and
* honors Retry-After. Duplicates are removed by Dimension Labs, so a retry is safe.
*/
export async function sendToDimensionLabs(msg, { historical = false, apiKey = process.env.DL_API_KEY, retries = 5 } = {}) {
if (!apiKey) throw new Error('Set DL_API_KEY to your source API key (Data Sources > your source > integration instructions).');
const type = msg.incoming ? 'incoming' : 'outgoing';
const url = `${DL}/${historical ? 'trackhistorical' : 'track'}?platform=universal&v=11.1.0-rest&type=${type}&apiKey=${encodeURIComponent(apiKey)}`;
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(msg) });
if (res.ok) return;
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt >= retries) throw new Error(`Dimension Labs returned ${res.status}: ${await res.text()}`);
const after = Number(res.headers.get('retry-after'));
await sleep(Number.isFinite(after) && after > 0 ? after * 1000 : 500 * 2 ** attempt);
}
}
/**
* Any date to Unix milliseconds. Numbers below 1e12 are read as seconds. Text without a time zone
* (for example "2026-01-02 09:30:00") is read as UTC, not your computer's local time.
*/
export function toMillis(d) {
if (typeof d === 'number') return d < 1e12 ? Math.round(d * 1000) : Math.round(d);
if (d instanceof Date) return d.getTime();
const s = String(d).trim();
if (/^\d+(\.\d+)?$/.test(s)) return toMillis(Number(s));
const zoned = /([zZ]|[+-]\d\d:?\d\d)$/.test(s);
const t = new Date(zoned ? s : s.replace(' ', 'T') + 'Z').getTime();
if (Number.isNaN(t)) throw new Error(`Can't read the date "${s}"`);
return t;
}

The shape of every integration: fetch records from the source’s API, convert each to the conversation format, send each one. Every guide exports one function, syncX({ since, historical }), that sends everything from since (Unix ms) and returns the new since for the next run. This sketch uses a chatbot vendor’s messages (fields as in the Ada guide); swap in your vendor’s API and field names.

import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
export async function syncMessages({ since, historical = false }) {
const vendorMessages = await fetchMessagesSince(since); // your vendor's API, oldest first
let newest = since;
for (const m of vendorMessages) {
if (!m.message_data?.body?.trim()) continue; // skip events, images, quick replies
const ts = toMillis(m.date_created);
await sendToDimensionLabs({
text: m.message_data.body,
userId: m.chatter_id, // the end user, on both sides of the conversation
incoming: m.sender.toLowerCase() !== 'ada', // true only for what the customer wrote
sessionId: m.conversation_id,
conversationId: m.conversation_id,
dimensionlabs_timestamp: ts,
platformJson: { messageId: m._id }, // any extra fields you want to keep
}, { historical: historical || ts < Date.now() - HOUR }); // older than an hour: /trackhistorical
newest = Math.max(newest, ts);
}
return newest; // the next run starts here
}

Instead of posting, you can also write the converted messages to a JSONL file (one JSON object per line) and upload the file.

Send history to /trackhistorical, or upload it as a file. Then run a backfill from the Backfill tab once it has arrived: history is not picked up by the daily enrichment of live data. The scheduled sync runner does a history load with one command.

A large history doesn’t need splitting: Dimension Labs splits a large file for you, with no size limit. Posting one message at a time takes a while for millions of messages, so for a very large history a file upload is usually quicker.

  • Direction. Many vendor APIs call the agent or bot the “author” or “sender”. incoming is about the customer: true only for what the customer wrote.
  • Milliseconds. JavaScript’s Date.getTime() gives milliseconds. Python’s datetime.timestamp() gives seconds: multiply by 1,000. toMillis handles both.
  • One second apart. Space messages in the same session at least 1,000 ms apart.
  • Stable ids. Use the vendor’s own ticket, conversation or response id as sessionId, so a retried run doesn’t create new conversations.
  • No colons in sessionId or conversationId. Use - or _.
  • Strip HTML. Ticket and email bodies often arrive as HTML. Send the plain text.
  • Skip empty and system messages. An empty text, or an automatic “ticket closed” note, adds nothing.
  • No personal data. Never use email addresses, phone numbers or names as userId, sessionId or a join key, and don’t put them in platformJson. Remove them from message text before you send. See PII redaction.

Stuck, or the tracker rejects your messages? Email [email protected] with the source name, the status code and one sample payload (with the API key removed).

You are probably already collecting metadata such as intents, events, scores and context. None of it is required, but sending it gives the agent more to filter, group and join on, and gives enrichment more context.

platformJson is the catch-all for anything about the record: your platform’s own message JSON, CSAT or NPS scores, experience ratings, the weather, the game or event, the store. It is the preferred place for every field beyond the required ones.

{
"platformJson": {
"channel": "webchat",
"csatScore": 4,
"weatherConditions": "rain",
"gameSeries": "home-opener"
}
}
  • Metadata you want the agent to filter or join on must be sent in platformJson.
  • It is visible in transcripts.
  • Pick one encoding per field and keep it. "yes", "Yes" and 1 become three different values.

platformUserJson holds attributes of the person, kept apart from the message metadata. Use it only when keeping them separate is meaningful; otherwise use platformJson.

FieldType
localestring
timezonestring
customerTierstring
abTestGroupstring
{
"platformUserJson": {
"locale": "en-CA",
"timezone": "America/Toronto",
"customerTier": "silver",
"abTestGroup": "B"
}
}

Some connectors, Intercom for example, fill platformUserJson for you.

intent carries the intent your bot or NLP engine matched for the message. You need it only for an intent-based chatbot. The full guide, including the NotHandled intent and event tracking, is Intents and events.

FieldTypeRequiredDescription
namestringYesName of the intent, for example TRACK_ORDER
inputsarray of objectsNoInputs (slots or entities) captured with the intent, each { "name": "...", "value": "..." }
confidencenumberNoFrom 0.0 (completely uncertain) to 1.0 (completely certain)
{
"intent": {
"name": "TRACK_ORDER",
"confidence": 0.948014021,
"inputs": [
{ "name": "orderNumber", "value": "DM34137KMX" }
]
}
}

Rich content: images, buttons and postbacks

Section titled “Rich content: images, buttons and postbacks”

If your messages include images or interactive elements, send them in these shapes.

Images. images is an array of objects with one field, url.

{
"images": [
{ "url": "https://example.com/product-photo.jpg" }
]
}

Buttons. buttons is an array of the options offered in the message.

FieldTypeDescription
idstringId of the button
labelstringLabel shown on the button
valuestringValue of the button
{
"buttons": [
{ "id": "btn-track", "label": "Track my order", "value": "TRACK_ORDER" }
]
}

Postbacks. postback records a button click. It holds a buttonClick object with one field, buttonId, the id of the button that was clicked.

{
"postback": {
"buttonClick": { "buttonId": "btn-track" }
}
}

The confidence values below are examples. Leave confidence out if your platform doesn’t produce one.

Four messages between one user and a bot. userId, conversationId and sessionId stay the same across the session, and each timestamp is at least a second after the one before.

[
{
"text": "hey",
"userId": "7675836189527716",
"incoming": true,
"conversationId": "conv-88213407",
"sessionId": "1434868331",
"dimensionlabs_timestamp": 1703240310000,
"intent": { "name": "HELLO", "confidence": 0.963456013 }
},
{
"text": "Hi! How can I help you?",
"userId": "7675836189527716",
"incoming": false,
"conversationId": "conv-88213407",
"sessionId": "1434868331",
"dimensionlabs_timestamp": 1703240315000,
"intent": { "name": "HELLO_RESPONSE" }
},
{
"text": "I want to know where to track my deliveries",
"userId": "7675836189527716",
"incoming": true,
"conversationId": "conv-88213407",
"sessionId": "1434868331",
"dimensionlabs_timestamp": 1703240320000,
"intent": { "name": "TRACK_ORDER", "confidence": 0.948014021 }
},
{
"text": "You can track your order anytime at example.com/orders.",
"userId": "7675836189527716",
"incoming": false,
"conversationId": "conv-88213407",
"sessionId": "1434868331",
"dimensionlabs_timestamp": 1703240325000,
"intent": { "name": "TRACK_ORDER_RESPONSE" }
}
]

Each object is one POST: the incoming ones with type=incoming, the outgoing ones with type=outgoing.

Two participants, each with their own userId. The shared conversationId keeps the conversation together; without it, the session could split at each change of sender.

[
{
"text": "My internet has been down since this morning.",
"userId": "cust-4471920",
"incoming": true,
"conversationId": "conv-90551216",
"sessionId": "9950934871",
"dimensionlabs_timestamp": 1703278090000
},
{
"text": "Sorry to hear that. Let me look into your account.",
"userId": "agent-302",
"incoming": false,
"conversationId": "conv-90551216",
"sessionId": "9950934871",
"dimensionlabs_timestamp": 1703278095000
},
{
"text": "Thanks. The router lights are all red.",
"userId": "cust-4471920",
"incoming": true,
"conversationId": "conv-90551216",
"sessionId": "9950934871",
"dimensionlabs_timestamp": 1703278100000
},
{
"text": "I see an outage in your area. Crews are on it, and service should be back within two hours.",
"userId": "agent-302",
"incoming": false,
"conversationId": "conv-90551216",
"sessionId": "9950934871",
"dimensionlabs_timestamp": 1703278105000
}
]
{
"text": "I'd like to order a pizza and a drink",
"userId": "user01",
"incoming": true,
"sessionId": "order-5521",
"conversationId": "order-5521",
"dimensionlabs_timestamp": 1703240320000,
"intent": {
"name": "ORDER",
"inputs": [
{ "name": "dish", "value": "pizza" },
{ "name": "extra", "value": "drink" }
],
"confidence": 1.0
},
"images": [{ "url": "https://example.com/orders/pizza.png" }],
"buttons": [
{ "id": "btn-pizza", "label": "Pizza", "value": "pizza" },
{ "id": "btn-drink", "label": "Drink", "value": "drink" }
],
"postback": { "buttonClick": { "buttonId": "btn-pizza" } },
"platformJson": { "channel": "webchat", "store_id": "SF-04" },
"platformUserJson": { "locale": "en-US", "customerTier": "gold" }
}

Last reviewed on 2026-10-14.