# Freshdesk

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

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](/api/tracker).

## 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](/api/tracker#your-api-key).

## 1. Get API access in Freshdesk

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](https://developers.freshdesk.com/api/#authentication)).

## 2. Pull new tickets

[List all tickets](https://developers.freshdesk.com/api/#list_all_tickets) with the `updated_since` filter, sorted oldest change first:

```bash
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](https://developers.freshdesk.com/api/#pagination)).

For each ticket:

- [View a ticket](https://developers.freshdesk.com/api/#view_a_ticket), `GET /api/v2/tickets/[id]`, returns `description_text`: the customer's first message.
- [List all conversations of a ticket](https://developers.freshdesk.com/api/#list_all_ticket_notes), `GET /api/v2/tickets/[id]/conversations`, returns the replies and notes. Private notes have `private: true` and aren't visible to the customer ([conversations](https://developers.freshdesk.com/api/#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](https://developers.freshdesk.com/api/#ratelimit)).

## 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](/data/pii-redaction).

## 4. Send it

```js
// freshdesk.mjs. Environment: FRESHDESK_DOMAIN, FRESHDESK_API_KEY, DL_API_KEY.


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

Save the code as `freshdesk.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/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](/dimensions/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](/data/upload-a-file) (check it with the [file checker](/data/check-a-file)), then run a backfill.

## Freshdesk reference

- [Authentication](https://developers.freshdesk.com/api/#authentication)
- [List all tickets](https://developers.freshdesk.com/api/#list_all_tickets)
- [View a ticket](https://developers.freshdesk.com/api/#view_a_ticket)
- [Conversations](https://developers.freshdesk.com/api/#conversations)
- [List all conversations of a ticket](https://developers.freshdesk.com/api/#list_all_ticket_notes)
- [Pagination](https://developers.freshdesk.com/api/#pagination)
- [Rate limit](https://developers.freshdesk.com/api/#ratelimit)

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