Skip to content

02 Get your data in /Integrations

MySQL

Send conversation messages stored in MySQL to Dimension Labs with the universal API.

  1. M MySQLTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

If your conversations already land in a MySQL table, you can send them to Dimension Labs from there. A scheduled job on your side queries the rows that are new since its last run and posts each message to the universal API.

  • A MySQL account that can create users and grant access to the table that holds your conversations.
  • The database’s host, port and name, and network access to it from wherever the job runs. Managed hosts often need the job’s IP address on an allowlist.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Ask your database admin to create a MySQL user for the job that can only read the conversations table:

CREATE USER 'dl_sync'@'%' IDENTIFIED BY 'choose-a-long-password';
GRANT SELECT ON support.conversation_messages TO 'dl_sync'@'%';

Replace '%' with the job’s host or IP address if you can, so the account only works from there. Keep the password in a secrets manager, never in code you commit.

Use the mysql2 package. Each run asks for the rows changed since the last run, using updated_at as the cursor:

SELECT message_id, conversation_id, customer_id, sender_type, body, channel, sent_at, updated_at
FROM support.conversation_messages
WHERE updated_at >= ?
ORDER BY updated_at;
  • connection.execute() prepares the statement and fills each ? from the values you pass (mysql2 quickstart). The code passes the cursor as a JavaScript Date.
  • mysql2 turns DATETIME and TIMESTAMP values into JavaScript Date objects, and back, using its timezone option, which defaults to 'local' (connection options). The code sets timezone: 'Z', so a DATETIME is read and written as UTC. Store your times in UTC, or set timezone to the offset they are stored in, in the form +HH:MM (option reference, which mysql2 follows).

This guide assumes a table with one row per message, with these columns. Rename them to match yours.

ColumnHolds
message_idThe message’s own id
conversation_idThe conversation, ticket or chat the message belongs to
customer_idYour internal id for the customer in that conversation, on every row, including agent and bot rows. Not an email address or phone number.
sender_typeWho wrote the message, for example customer, agent or bot
bodyThe message text
channelChat, email, voice and so on
sent_atWhen the message was sent (DATETIME in UTC, or TIMESTAMP)
updated_atWhen the row was written or last changed (same type)
Dimension LabsMySQL columnNotes
textbodySkip rows where it is empty. Strip HTML if bodies are HTML.
userIdcustomer_idIf it is null, use the sessionId.
incomingsender_typetrue only when the customer wrote it. Agent and bot rows are false.
sessionIdconversation_idReplace any colons.
conversationIdconversation_idSame value as sessionId.
dimensionlabs_timestampsent_atPass the Date through toMillis.
platformJsonmessage_id, channelAdd any join keys you have, such as an account or store id.

Personal data. Don’t select email, phone or name columns. If your only customer key is an email, send a consistent hash of it instead. Message text can still hold personal details: redact them in the function before you send. See PII redaction.

Install the driver in your project first: npm install mysql2.

// mysql.mjs. Environment: MYSQL_HOST, MYSQL_PORT (optional, 3306), MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, DL_API_KEY.
import mysql from 'mysql2/promise';
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const SQL = `SELECT message_id, conversation_id, customer_id, sender_type, body, channel, sent_at, updated_at
FROM support.conversation_messages WHERE updated_at >= ? ORDER BY updated_at`;
const clean = (s) => String(s ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
// Sends every row written or changed at or after `since` (Unix ms). Returns the new `since`.
export async function syncMySQL({ since, historical = false }) {
const connection = await mysql.createConnection({
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT || 3306),
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
timezone: 'Z', // read and write DATETIME values as UTC
});
try {
const [rows] = await connection.execute(SQL, [new Date(since)]);
let newest = since;
for (const row of rows) {
const text = clean(row.body);
const id = String(row.conversation_id).replaceAll(':', '-');
if (text) {
const ts = toMillis(row.sent_at);
await sendToDimensionLabs({
text,
userId: row.customer_id ? String(row.customer_id).replaceAll(':', '-') : id,
incoming: row.sender_type === 'customer',
sessionId: id,
conversationId: id,
dimensionlabs_timestamp: ts,
platformJson: { message_id: String(row.message_id), channel: row.channel },
}, { historical: historical || ts < Date.now() - HOUR });
}
newest = Math.max(newest, toMillis(row.updated_at));
}
return newest;
} finally {
await connection.end();
}
}

If your host requires TLS, add the ssl option to createConnection (option reference).

The cursor follows updated_at, so a row that changes later (a status column, say) is sent again. That’s fine: duplicates are removed automatically. An index on updated_at keeps the query fast as the table grows.

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

  • History first: node run.mjs 2025-01-01 sends every row since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new rows to /track, and they are enriched automatically every day. A message whose sent_at is more than an hour old when the job sees it (a late-loaded batch, say) goes to /trackhistorical, and needs a backfill.
  • Re-runs are safe: duplicates are removed automatically.
  • No API rate limit: the database is the only limit. The query reads all new rows into memory at once. For a very large history, start from a recent date and upload the older rows as a file.

You can also export the rows to a CSV file and upload it. Run it through the file checker first.

Checked against the mysql2 documentation on 2026-10-26.