# Freshchat

> Send Freshchat chat transcripts to Dimension Labs with the universal API.

This brings in your Freshchat chats: what each customer wrote and what your agents and bots replied, with one session per interaction (from the first message until the chat is resolved). You run it on your side: a small script pulls the Chat-Transcript raw report from the Freshchat API and posts each message to the [universal API](/api/tracker).

## What you need

- A Freshchat admin, to generate an API key.
- Your Freshchat API key and account URL.
- Node's `csv-parse` package: `npm install csv-parse`. The report arrives as CSV.
- 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 Freshchat

Freshchat's own steps are in its [API reference](https://developers.freshchat.com/api/), under **Authentication**.

1. In a standalone Freshchat account, go to **Admin > CONFIGURE > API Tokens** and click **Generate Token**. Copy the API key.
2. In Freshchat inside Freshsales Suite, go to **Settings > Admin Settings > Website Tracking and APIs > API Settings**. Under **API DETAILS FOR CHAT**, copy **Your API Key** (click **Generate token** if it is empty). The same section shows **Your chat URL**.
3. Send the key as `Authorization: Bearer YOUR_API_KEY`.

Your account URL is `https://<account-name>.freshchat.com` for a standalone account, or your chat URL for Freshsales Suite. API paths go under `/v2`.

## 2. Pull new chats

Freshchat has no endpoint that lists every conversation. The [raw reports](https://developers.freshchat.com/api/) API does: ask for a `Chat-Transcript` report, wait for it, then download it.

```bash
curl -X POST "https://YOUR_ACCOUNT.freshchat.com/v2/reports/raw" \
  -H "Authorization: Bearer $FRESHCHAT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"start":"2026-10-25T00:00:00.000Z","end":"2026-10-26T00:00:00.000Z","event":"Chat-Transcript","format":"csv"}'
```

- The response holds the report `id`. Call `GET /v2/reports/raw/{id}` until `status` is `COMPLETED` (or `FAILED`). Then each entry in `links` has a download URL in `link.href`.
- A `Chat-Transcript` window can be at most 24 hours, and `start` can be no earlier than 15 months ago.
- The download links expire after one hour, so download them as soon as the report is ready.
- Freshworks's [extract API article](https://crmsupport.freshworks.com/support/solutions/articles/50000004465-extract-api) says reports can take up to an hour to generate and only one report request is processed per minute. Its [raw data article](https://crmsupport.freshworks.com/support/solutions/articles/50000004681-how-can-i-fetch-the-raw-data-of-my-chats-) lists 120 Chat-Transcript requests a day.

Rate limits, per Freshchat: the hourly number of API calls depends on your plan. Read it from the `X-RateLimit-Limit` and `X-RateLimit-Remaining` response headers.

## 3. Map the fields

Each row of the report is one message.

| Dimension Labs | Freshchat | Notes |
|---|---|---|
| `text` | `message_parts` | A JSON array in the CSV cell. Join the `text.content` of each part. Skip rows whose `message_type` is `private` (private notes) or `system`. |
| `userId` | `customer_id` | Freshchat's id for the customer who started the chat, on every message. Fall back to the `sessionId`. |
| `incoming` | `actor_type` | `true` when `actor_type` is `USER`. `AGENT`, `BOT` and `SYSTEM` are your side. |
| `sessionId` | `interaction_raw_id` | One interaction, from the first message until the chat is resolved. A reopened chat gets a new one. Prefix it, for example `freshchat-...`. |
| `conversationId` | `conversation_id` | The whole conversation thread, across interactions. |
| `dimensionlabs_timestamp` | `created_time` | UTC. Convert with `toMillis`. |
| `platformJson` | `channel_name`, `message_source`, `actor_type` | `channel_name` is the topic, such as Billing or Sales. |

**Personal data.** Don't send `actor_email`, `actor_phone`, `actor_first_name` or `actor_last_name`. Messages can hold names, emails and order numbers: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// freshchat.mjs. Environment: FRESHCHAT_URL (for example https://yourcompany.freshchat.com), FRESHCHAT_API_KEY, DL_API_KEY.
// Needs: npm install csv-parse



const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const base = `${process.env.FRESHCHAT_URL.replace(/\/$/, '')}/v2`;
const headers = { Authorization: `Bearer ${process.env.FRESHCHAT_API_KEY}`, 'Content-Type': 'application/json', Accept: 'application/json' };

// Retries rate limits (429) and server errors (5xx), honoring Retry-After.
async function call(url, options = {}) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, options);
    if (res.ok) return res;
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Freshchat returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Asks for one Chat-Transcript report and waits until it is ready (up to 80 minutes).
async function transcriptReport(start, end) {
  const body = JSON.stringify({ start: new Date(start).toISOString(), end: new Date(end).toISOString(), event: 'Chat-Transcript', format: 'csv' });
  const { id } = await (await call(`${base}/reports/raw`, { method: 'POST', headers, body })).json();
  for (let i = 0; i < 80; i++) {
    const report = await (await call(`${base}/reports/raw/${id}`, { headers })).json();
    if (report.status === 'COMPLETED') return report;
    if (report.status === 'FAILED') throw new Error(`Freshchat report ${id} failed`);
    await sleep(60 * 1000);
  }
  throw new Error(`Freshchat report ${id} was not ready after 80 minutes`);
}

function textOf(row) {
  try {
    return JSON.parse(row.message_parts || '[]').map((p) => p.text?.content).filter(Boolean).join('\n');
  } catch {
    return '';
  }
}

// Sends every customer, agent and bot message created at or after `since` (Unix ms). Returns the new `since`.
export async function syncFreshchat({ since, historical = false }) {
  const now = Date.now();
  let start = since;
  while (start < now) {
    const end = Math.min(start + DAY, now); // Chat-Transcript reports cover 24 hours at most
    const report = await transcriptReport(start, end);
    for (const { link } of report.links ?? []) {
      const csv = await (await call(link.href)).text(); // download right away: links expire after an hour
      for (const row of parse(csv, { columns: true, skip_empty_lines: true })) {
        const text = textOf(row);
        if (row.message_type !== 'normal' || !text.trim()) continue; // skips private notes and system messages
        const ts = toMillis(row.created_time);
        const sessionId = `freshchat-${row.interaction_raw_id || row.conversation_id}`.replace(/:/g, '-');
        await sendToDimensionLabs({
          text,
          userId: row.customer_id || sessionId,
          incoming: String(row.actor_type).toUpperCase() === 'USER',
          sessionId,
          conversationId: `freshchat-${row.conversation_id}`,
          dimensionlabs_timestamp: ts,
          platformJson: { channel_name: row.channel_name, message_source: row.message_source, actor_type: row.actor_type },
        }, { historical: historical || ts < Date.now() - HOUR });
      }
    }
    start = end;
    if (start < now) await sleep(60 * 1000); // Freshworks processes one report request a minute
  }
  return start;
}
```

## 5. Run it

Save the code as `freshchat.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 { syncFreshchat as sync } from './freshchat.mjs';`.

- **History first:** `node run.mjs 2025-08-01` asks for one report per day since that date (15 months back at most) and sends every message to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every hour:** a report can take up to an hour to generate and the daily number of report requests is limited, so run this job hourly rather than every 15 minutes. Use `run.mjs` on a server or cron, not Lambda: waiting for a report can outlast a Lambda run. Messages that are more than an hour old when the job sees them go to `/trackhistorical`, and need a backfill.
- **Re-runs are safe:** duplicates are removed automatically.

A history load uses one report request per day of history and waits at least a minute between requests, so a long history takes hours, and more than 120 days of history needs more than one day of runs. Run it again with the same date if it stops: what was already sent is deduplicated. You can also [upload a file](/data/upload-a-file) of exported chats with one row per message (check it with the [file checker](/data/check-a-file)), then run a backfill.

## Freshchat reference

- [Freshchat API reference](https://developers.freshchat.com/api/) (Authentication, Rate limits, Reports, The message object)
- [Extract API](https://crmsupport.freshworks.com/support/solutions/articles/50000004465-extract-api)
- [How can I fetch the raw data of my chats?](https://crmsupport.freshworks.com/support/solutions/articles/50000004681-how-can-i-fetch-the-raw-data-of-my-chats-)

Checked against Freshchat's documentation on 2026-10-26.
