Skip to content

02 Get your data in /Integrations

Freshdesk

Send Freshdesk tickets and their conversations to Dimension Labs with the universal API.

  1. F FreshdeskTickets, 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 Freshdesk ticket as one conversation: the customer’s original description, then the replies and public notes. You run it on your side: a small script pulls from the Freshdesk API and posts each message to the universal API.

  • A Freshdesk agent account that can view the tickets you want to send.
  • That agent’s Freshdesk API key.
  • A Dimension Labs API key for a new Universal API source. See Your API key.
  1. Log in to your Freshdesk support portal.
  2. Click your profile picture in the top right corner and go to Profile settings.
  3. Copy the API key shown below the change password section.
  4. Authenticate with Basic auth, using the API key as the username and any characters, such as X, as the password (authentication).

List all tickets with the updated_since filter, sorted oldest change first:

Terminal window
curl -u "$FRESHDESK_API_KEY:X" \
"https://YOUR_DOMAIN.freshdesk.com/api/v2/tickets?updated_since=2026-10-13T00:00:00Z&order_by=updated_at&order_type=asc&per_page=100"

Without updated_since, the list only returns tickets created in the past 30 days. It returns at most 300 pages. per_page goes up to 100, and the link response header holds the next page’s URL until the last page (pagination).

For each ticket:

Rate limits, per Freshdesk, are per minute for the whole account and depend on the plan: Growth 100 (Tickets List 40), Pro 400 (Tickets List 100), Enterprise 700 (Tickets List 200), and 50 for trial accounts. Every response has X-RateLimit-Total and X-RateLimit-Remaining headers, and a rate-limited response has Retry-After (rate limit).

Dimension LabsFreshdeskNotes
textticket description_text, then conversation body_textBoth are plain text. Skip conversations where private is true.
userIdticket requester_idFreshdesk’s internal contact id for the customer, on both their messages and the agents’ replies.
incomingconversation user_id equals ticket requester_idtrue for the requester’s messages. The description is always true. Freshdesk’s own incoming field means “created from outside”, not “written by the customer”, so don’t use it for this.
sessionIdticket idPrefix it, for example freshdesk-20.
conversationIdticket idSame value as sessionId.
dimensionlabs_timestampticket created_at for the description, conversation created_at for the restConvert with toMillis.
platformJsonticket source, status, priority, type, tags, group_id, company_idcompany_id lets you join to account data.

Personal data. Don’t send the requester’s email, phone or name. Ticket text often holds them: redact them before you send. See PII redaction.

// freshdesk.mjs. Environment: FRESHDESK_DOMAIN, FRESHDESK_API_KEY, DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const base = `https://${process.env.FRESHDESK_DOMAIN}.freshdesk.com/api/v2`;
const auth = 'Basic ' + Buffer.from(`${process.env.FRESHDESK_API_KEY}:X`).toString('base64');
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
// Retries Freshdesk's rate limits (429) and server errors (5xx), honoring Retry-After.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { Authorization: auth } });
if (res.ok) {
const next = res.headers.get('link')?.match(/<([^>]+)>;\s*rel="next"/)?.[1] ?? null;
return { body: await res.json(), next };
}
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Freshdesk returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
async function sendTicket(id, since, historical) {
const { body: t } = await get(`${base}/tickets/${id}`);
const meta = { source: t.source, status: t.status, priority: t.priority, type: t.type, tags: t.tags, group_id: t.group_id, company_id: t.company_id };
const msgs = [{ text: t.description_text, user_id: t.requester_id, created_at: t.created_at }];
for (let c = `${base}/tickets/${id}/conversations?per_page=100`; c; ) {
const res = await get(c);
msgs.push(...res.body.filter((m) => !m.private));
c = res.next;
}
let newest = since;
for (const m of msgs) {
const ts = toMillis(m.created_at);
const text = m.text ?? m.body_text;
if (ts < since || !text?.trim()) continue;
await sendToDimensionLabs({
text,
userId: String(t.requester_id),
incoming: m.user_id === t.requester_id,
sessionId: `freshdesk-${t.id}`,
conversationId: `freshdesk-${t.id}`,
dimensionlabs_timestamp: ts,
platformJson: meta,
}, { historical: historical || ts < Date.now() - HOUR });
newest = Math.max(newest, ts);
}
return newest;
}
// Sends every message written at or after `since` (Unix ms). Returns the new `since`.
export async function syncFreshdesk({ since, historical = false }) {
let newest = since;
let from = since;
for (;;) {
let url = `${base}/tickets?updated_since=${iso(from)}&order_by=updated_at&order_type=asc&per_page=100`;
let pages = 0;
while (url) {
const page = await get(url);
for (const t of page.body) {
newest = Math.max(newest, await sendTicket(t.id, since, historical));
from = Math.max(from, toMillis(t.updated_at));
}
url = page.next;
if (url && ++pages === 300) break; // Freshdesk stops at 300 pages: start again from the last ticket seen
}
if (!url) return newest;
}
}

The list stops at 300 pages, so on a big history load the code starts the list again from the last ticket it saw. Tickets at the restart point can be sent twice; duplicates are removed automatically.

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

  • History first: node run.mjs 2025-01-01 sends every message since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new messages to /track, and they are enriched automatically every day. A message 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.

Each ticket costs at least two Freshdesk calls, so a history load on a Growth plan (100 calls a minute) takes a while; the code waits whenever Freshdesk asks it to. You can also export tickets from Freshdesk and upload the file (check it with the file checker), then run a backfill.

Checked against Freshdesk’s documentation on 2026-10-13.