Skip to content
TidyKB

Export Freshdesk knowledge base articles

Export every Freshdesk solution article with its full content to CSV and HTML: a free script that works today, and a no-signup export tool opening soon.

Updated

Knowledge base exportnot open yet

Paste your public help-center URL. The export will list every public solution article with its full content, as CSV, HTML or Markdown. It isn’t open yet: leave your email and we’ll send the file when it opens. The free audit of the same articles runs today.

We read only public pages, like any visitor. Need drafts and private articles today? Use the script below.
  • CSV · HTML · Markdown
  • No signup
  • Public articles only

What the built-in Freshdesk export gives you

Freshdesk can hand you a list of your solution articles and their properties: title, status, author, folder, dates. It does not give you the article bodies in that list. The content itself only comes out of the account-wide data export in the admin settings, which bundles your knowledge base with tickets, contacts and everything else as XML files. That is a backup, not something you can read in a spreadsheet or feed to a review.

The Freshworks Marketplace lists a few paid export apps (for example Help Center PDF Export and KnowledgeBase Exporter). They are marketplace apps with Freshworks billing, installed into your account.

Checked 2026-09-18 · Freshdesk's own article on exporting (support.freshdesk.com, doc 225163) now asks for a sign-in, so we could not re-read it on 2026-09-19. Check the current wording in your account before you rely on the description above.

Export every article with the API today

You don't have to wait for the hosted tool. The script below reads your whole knowledge base through the Freshdesk API v2 and writes one CSV row and one HTML file per article. It runs on your own computer, so the key never leaves it.

  1. 01

    Get a read-capable API key

    Use the key of a dedicated agent that can only see the knowledge base. The KB-only API key guide shows how.
  2. 02

    Save the script

    Copy or download export-freshdesk-kb.mjs into an empty folder. It needs Node 22 or newer and nothing else.
  3. 03

    Run it

    FRESHDESK_DOMAIN=yourcompany.freshdesk.com FRESHDESK_API_KEY=your_key node export-freshdesk-kb.mjs. Add a language code at the end (fr, de) to export that translation instead.
  4. 04

    Open the result

    freshdesk-kb-export/articles.csv opens in Excel, Numbers or Google Sheets. Each article body is in html/.
export-freshdesk-kb.mjs · 119 lines · Node 22+ Download
// export-freshdesk-kb.mjs: export every Freshdesk solution article, with its content, to CSV + one HTML file each.
// Node 22+, no dependencies, read-only (GET requests only). Source: https://tidykb.net/tools/freshdesk-knowledge-base-export
//
//   FRESHDESK_DOMAIN=yourcompany.freshdesk.com FRESHDESK_API_KEY=your_key node export-freshdesk-kb.mjs
//   ... node export-freshdesk-kb.mjs fr        # one translation (language code as in Freshdesk)
//
// Optional: RATE_PER_MINUTE (default 40, stay well under your plan's limit), OUT_DIR (default ./freshdesk-kb-export).
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

const domain = process.env.FRESHDESK_DOMAIN;
const apiKey = process.env.FRESHDESK_API_KEY;
const lang = process.argv[2] ?? '';
const perMinute = Number(process.env.RATE_PER_MINUTE ?? 40);
const outDir = process.env.OUT_DIR ?? 'freshdesk-kb-export';
if (!domain || !apiKey) {
  console.error('Set FRESHDESK_DOMAIN (yourcompany.freshdesk.com) and FRESHDESK_API_KEY.');
  process.exit(1);
}
const base = process.env.FRESHDESK_API_BASE ?? `https://${domain}/api/v2`;
const auth = `Basic ${Buffer.from(`${apiKey}:X`).toString('base64')}`;
const suffix = lang ? `/${encodeURIComponent(lang)}` : '';
const gapMs = 60000 / perMinute;
let lastCall = 0;
let calls = 0;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// GET with a steady pace, Retry-After on 429 and a few retries on 5xx. 404 = "not there" (e.g. no subfolders).
async function get(path) {
  for (let attempt = 1; ; attempt++) {
    const wait = lastCall + gapMs - Date.now();
    if (wait > 0) await sleep(wait);
    lastCall = Date.now();
    calls++;
    const res = await fetch(base + path, { headers: { Authorization: auth, Accept: 'application/json' } });
    if (res.status === 429 && attempt <= 6) {
      const seconds = Number(res.headers.get('retry-after')) || 60;
      console.error(`Rate limited, waiting ${seconds}s`);
      await sleep(seconds * 1000);
      continue;
    }
    if (res.status >= 500 && attempt <= 3) {
      await sleep(2000 * attempt);
      continue;
    }
    if (res.status === 404) return null;
    if (!res.ok) throw new Error(`${res.status} on GET ${path}: ${(await res.text()).slice(0, 300)}`);
    return res.json();
  }
}

// Every page of a list endpoint (max 100 per page).
async function list(path) {
  const items = [];
  for (let page = 1; ; page++) {
    const batch = await get(`${path}?per_page=100&page=${page}`);
    if (!Array.isArray(batch) || batch.length === 0) break;
    items.push(...batch);
    if (batch.length < 100) break;
  }
  return items;
}

// Folders of a category, then their subfolders (Freshdesk's flexible hierarchy), depth-first.
async function walkFolders(parentPath, trail, into) {
  for (const folder of await list(parentPath)) {
    const path = [...trail, folder.name];
    into.push({ folder, path });
    await walkFolders(`/solutions/folders/${folder.id}/subfolders${suffix}`, path, into);
  }
}

const csvCell = (value) => {
  let s = value == null ? '' : Array.isArray(value) ? value.join('; ') : String(value);
  if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
  return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};
const slug = (s) => String(s).toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60) || 'article';
const escapeHtml = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);

const columns = ['id', 'title', 'status', 'category', 'folder_path', 'language', 'portal_url', 'created_at', 'updated_at', 'views', 'thumbs_up', 'thumbs_down', 'tags', 'seo_title', 'seo_description', 'html_file'];
const rows = [];
const htmlDir = join(outDir, lang ? `html-${lang}` : 'html');
await mkdir(htmlDir, { recursive: true });

for (const category of await list(`/solutions/categories${suffix}`)) {
  const folders = [];
  await walkFolders(`/solutions/categories/${category.id}/folders${suffix}`, [], folders);
  for (const { folder, path } of folders) {
    for (const a of await list(`/solutions/folders/${folder.id}/articles${suffix}`)) {
      const file = `${a.id}-${slug(a.title)}.html`;
      const page = `<!doctype html>\n<meta charset="utf-8">\n<title>${escapeHtml(a.title)}</title>\n<h1>${escapeHtml(a.title)}</h1>\n${a.description ?? ''}\n`;
      await writeFile(join(htmlDir, file), page);
      rows.push({
        id: a.id,
        title: a.title,
        status: a.status === 2 ? 'published' : a.status === 1 ? 'draft' : a.status,
        category: category.name,
        folder_path: path.join(' / '),
        language: lang || 'primary',
        portal_url: `https://${domain}/support/solutions/articles/${a.id}`,
        created_at: a.created_at,
        updated_at: a.updated_at,
        views: a.hits,
        thumbs_up: a.thumbs_up,
        thumbs_down: a.thumbs_down,
        tags: a.tags,
        seo_title: a.seo_data?.meta_title,
        seo_description: a.seo_data?.meta_description,
        html_file: join(lang ? `html-${lang}` : 'html', file),
      });
    }
  }
}

const csv = [columns.join(','), ...rows.map((r) => columns.map((c) => csvCell(r[c])).join(','))].join('\r\n');
await writeFile(join(outDir, lang ? `articles-${lang}.csv` : 'articles.csv'), `\uFEFF${csv}\r\n`);
console.log(`Exported ${rows.length} articles with ${calls} API calls to ${outDir}/`);

Read-only: the script only sends GET requests. Use the key of a dedicated agent with knowledge-base-only access.

How it behaves:

  • Every folder, every page. It walks categories, then folders, then nested subfolders, and asks for 100 articles per page until a page comes back short.
  • Polite with your rate limit. It sends 40 requests a minute by default. Current Freshdesk plans allow 100 (Growth), 400 (Pro) or 700 (Enterprise) per minute, and some older accounts count 3,000 to 5,000 per hour, so 40 leaves room for your other integrations. Change it with RATE_PER_MINUTE. If Freshdesk answers 429, the script waits for the Retry-After time and carries on.
  • Read-only. It only sends GET requests. Nothing in your help center changes.
  • Spreadsheet-safe CSV. Commas, quotes and line breaks are quoted, and a cell that starts with =, +, - or @ gets a leading apostrophe so Excel doesn't run it as a formula.

A knowledge base with 1,000 articles in 5 categories and 30 folders takes about 70 requests: under two minutes at the default pace.

Checked 2026-09-19 · Endpoints and pagination against developers.freshdesk.com/api. Rate limits as documented on 2026-09-18. The subfolder endpoint only matters with nested folders; the script treats a 404 there as "no subfolders".

What the export includes

ColumnWhat it holds
idThe article ID, as in /support/solutions/articles/48000123
titleThe article title
statuspublished or draft
category, folder_pathWhere it sits; nested folders are joined with a slash
languageprimary, or the language code you passed
portal_urlThe article's address on your Freshdesk domain
created_at, updated_atDates from Freshdesk, in UTC
views, thumbs_up, thumbs_downFreshdesk's own counters for the article
tagsTags, separated by semicolons
seo_title, seo_descriptionThe article's SEO fields
html_fileThe file with the full body: title plus the article HTML as Freshdesk stores it

updated_at and views together are the quickest way to find old articles people still read. Sort by views, then look at anything not updated in a year. The knowledge base audit checklist walks through the rest.

Private and draft articles

The hosted export will read your help center the way a visitor does, so it will only see published articles in public folders. Drafts, articles in folders limited to agents or specific companies, and translations that aren't published need the API. That is what the script above does, with your key and on your machine.

In TidyKB

TidyKB's private scan uses the same read-only API calls to check every article, draft and translation for broken links, stale content and outdated translations each week. It needs a KB-only API key and never reads tickets. It opens in October 2026, first for founding customers; the health check page shows what it covers.

Freshdesk and Freddy are trademarks of Freshworks Inc. TidyKB is an independent product and is not affiliated with, endorsed or sponsored by any company named on this page.

FAQ

Questions, answered

How do I export solution articles from Freshdesk?

Freshdesk exports a list of article properties, and the full content only comes with the account-wide XML data export. To get every article with its body as CSV and HTML, run the free read-only script on this page with a KB-only API key. It pages through every category, folder and subfolder through the Freshdesk API v2.

Can I export Freshdesk articles to PDF?

Not in bulk from Freshdesk itself; you can print a single article from your browser. The script writes one HTML file per article, which any browser prints to PDF. Paid marketplace apps such as Help Center PDF Export also exist.

Does it include translations?

Yes, one language per run. Add the language code at the end of the command (for example fr or de) and the script exports that translation of every article to its own CSV and HTML folder. The hosted export will include public translations.

See your help center’s score.

Paste a URL. No signup, no API key, no call.

Run free audit