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.)
- S Your systemTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
{
"text": "My order never arrived",
"userId": "cust-17",
"incoming": true,
"sessionId": "zd-48213",
"dimensionlabs_timestamp": 1767299700000
} Before you start
Section titled “Before you start”You need three things:
- 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.
- An API key for a Dimension Labs source. See Your API key.
- 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.
Your API key
Section titled “Your API key”Each source has its own key:
- In the app, open Data Sources and add a source under Unstructured data.
- Choose the Universal API tile, for every guide on this site, and give the source a name, for example “Zendesk”.
- 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.
Endpoints
Section titled “Endpoints”| Use it for | Endpoint |
|---|---|
| New messages, as they happen | https://tracker.dimensionlabs.io/track |
| History: anything older than an hour | https://tracker.dimensionlabs.io/trackhistorical |
Both take the same query parameters and the same body.
| Parameter | Value |
|---|---|
platform | universal |
v | 11.1.0-rest |
type | incoming for a message from the customer or end user, outgoing for one from your bot, agent or company |
apiKey | Your 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
/trackas they happen. A live job sending to/trackis 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 errors
Section titled “Rate limits and errors”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.
Incoming or outgoing
Section titled “Incoming or outgoing”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:
| Source | Incoming | Outgoing |
|---|---|---|
| Chat, bot or ticket | What the customer wrote | What the bot or agent wrote |
| Online review | The review | Your reply to it, if you have one |
| Survey | Each answer | Each question |
| News article or forum post | The 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.
The message
Section titled “The message”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" }}| Field | Required | Type | Notes |
|---|---|---|---|
text | Yes | string | The words to enrich. Never empty. |
userId | Yes | string | Who 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. |
incoming | Yes | boolean | true from the customer or end user, false from your side. Match it to type. |
sessionId | Yes | string | Groups the messages of one conversation. Enrichment reads a whole session at once. No colons. |
dimensionlabs_timestamp | Yes | number | Unix time in milliseconds (13 digits). Seconds read as a date in 1970. See Timestamps. |
conversationId | Recommended | string | Keeps a thread together when the sender changes, for example when a human agent takes over from a bot. No colons. |
platformJson | Optional | object | Any 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. |
platformUserJson | Optional | object | Attributes of the customer, such as tier or locale. See Customer attributes. |
intent | Optional | object | Only for an intent-based chatbot: { "name": "TRACK_ORDER", "confidence": 0.94 }. See Intent. |
images | Optional | array of objects | Images in the message. See Rich content. |
buttons | Optional | array of objects | Buttons offered in the message. |
postback | Optional | object | A 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.
Participants
Section titled “Participants”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
conversationIdties 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.
Sessions and conversations
Section titled “Sessions and conversations”sessionId and conversationId often hold the same value, and in most data they cover the same span of messages. They still do different jobs:
conversationId | sessionId | |
|---|---|---|
| What it groups | Every message in a conversation, across all participants | One session or interaction |
| What it controls | Conversation 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.
Timestamps
Section titled “Timestamps”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 and re-sends
Section titled “Duplicates and re-sends”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.
Send one message
Section titled “Send one message”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.
Test before you build
Section titled “Test before you build”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).
With Postman
Section titled “With Postman”- Download, install and open Postman.
- Create a new Collection.
- In the collection, create a new request and choose HTTP.
- Set the request type to POST.
- In the URL box, enter
https://tracker.dimensionlabs.io/track(or/trackhistoricalfor a timestamp older than an hour). - Under Params, add
platform=universal,v=11.1.0-rest,type=incoming, andapiKey= your source’s API key. - Under Headers, add
Content-Type=application/json. - Under Body, choose raw and JSON, and paste a message such as the minimal one above, with a current timestamp.
- Select Send. A
2xxstatus means the message was received.
With curl
Section titled “With curl”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.
Check it arrived
Section titled “Check it arrived”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.
Set up your project
Section titled “Set up your project”Every universal API guide imports two helpers from one file. Download it and put it next to your scripts:
- JavaScript: dimensionlabs.mjs (Node 18 or later)
- Python: dimensionlabs.py (needs
requests)
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:
mkdir dl-sync && cd dl-synccurl -O https://docs.dimensionlabs.io/templates/dimensionlabs.mjsnpm init -ynpm pkg set type=modulenpm install google-auth-library # only the vendor packages your guide listsexport DL_API_KEY=your-source-api-keynpm 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;}Fetch, convert, send
Section titled “Fetch, convert, send”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.
History
Section titled “History”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.
Rules that trip up integrations
Section titled “Rules that trip up integrations”- Direction. Many vendor APIs call the agent or bot the “author” or “sender”.
incomingis about the customer:trueonly for what the customer wrote. - Milliseconds. JavaScript’s
Date.getTime()gives milliseconds. Python’sdatetime.timestamp()gives seconds: multiply by 1,000.toMillishandles 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
sessionIdorconversationId. 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,sessionIdor a join key, and don’t put them inplatformJson. 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).
Optional fields
Section titled “Optional fields”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.
Metadata: platformJson
Section titled “Metadata: platformJson”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"and1become three different values.
Customer attributes: platformUserJson
Section titled “Customer attributes: platformUserJson”platformUserJson holds attributes of the person, kept apart from the message metadata. Use it only when keeping them separate is meaningful; otherwise use platformJson.
| Field | Type |
|---|---|
locale | string |
timezone | string |
customerTier | string |
abTestGroup | string |
{ "platformUserJson": { "locale": "en-CA", "timezone": "America/Toronto", "customerTier": "silver", "abTestGroup": "B" }}Some connectors, Intercom for example, fill platformUserJson for you.
Intent
Section titled “Intent”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.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Name of the intent, for example TRACK_ORDER |
inputs | array of objects | No | Inputs (slots or entities) captured with the intent, each { "name": "...", "value": "..." } |
confidence | number | No | From 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.
| Field | Type | Description |
|---|---|---|
id | string | Id of the button |
label | string | Label shown on the button |
value | string | Value 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" } }}Complete examples
Section titled “Complete examples”The confidence values below are examples. Leave confidence out if your platform doesn’t produce one.
A bot conversation
Section titled “A bot conversation”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.
A live chat with a human agent
Section titled “A live chat with a human agent”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 }]One message with every optional field
Section titled “One message with every optional field”{ "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.