02 Get your data in /Integrations
Freshdesk
Send Freshdesk tickets and their conversations to Dimension Labs with the universal API.
- F FreshdeskTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- 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.
What you need
Section titled “What you need”- 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. Get API access in Freshdesk
Section titled “1. Get API access in Freshdesk”- Log in to your Freshdesk support portal.
- Click your profile picture in the top right corner and go to Profile settings.
- Copy the API key shown below the change password section.
- Authenticate with Basic auth, using the API key as the username and any characters, such as
X, as the password (authentication).
2. Pull new tickets
Section titled “2. Pull new tickets”List all tickets with the updated_since filter, sorted oldest change first:
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:
- View a ticket,
GET /api/v2/tickets/[id], returnsdescription_text: the customer’s first message. - List all conversations of a ticket,
GET /api/v2/tickets/[id]/conversations, returns the replies and notes. Private notes haveprivate: trueand aren’t visible to the customer (conversations).
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).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Freshdesk | Notes |
|---|---|---|
text | ticket description_text, then conversation body_text | Both are plain text. Skip conversations where private is true. |
userId | ticket requester_id | Freshdesk’s internal contact id for the customer, on both their messages and the agents’ replies. |
incoming | conversation user_id equals ticket requester_id | true 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. |
sessionId | ticket id | Prefix it, for example freshdesk-20. |
conversationId | ticket id | Same value as sessionId. |
dimensionlabs_timestamp | ticket created_at for the description, conversation created_at for the rest | Convert with toMillis. |
platformJson | ticket source, status, priority, type, tags, group_id, company_id | company_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.
4. Send it
Section titled “4. Send it”// 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.
5. Run it
Section titled “5. Run it”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-01sends 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.
Freshdesk reference
Section titled “Freshdesk reference”- Authentication
- List all tickets
- View a ticket
- Conversations
- List all conversations of a ticket
- Pagination
- Rate limit
Checked against Freshdesk’s documentation on 2026-10-13.