---
title: How a Bilingual 585-Article Help Center Moved from Freshdesk to HubSpot Without a Public Write API
slug: freshdesk-to-hubspot-knowledge-base-migration-case-study
date: 2026-09-10
author: Raajshekhar Rajan
categories: [Case Studies]
excerpt: "A European software company migrated its English and German help center from Freshdesk to HubSpot with ClonePartner. 1,394 images re-hosted, 1,767 internal links rewritten, and a 1,588-row redirect sheet — delivered through HubSpot's own editor APIs, because no public write API for knowledge articles exists."
canonical: https://clonepartner.com/blog/freshdesk-to-hubspot-knowledge-base-migration-case-study
---

# How a Bilingual 585-Article Help Center Moved from Freshdesk to HubSpot Without a Public Write API


## TL;DR

**Customer:** A European B2B software company (anonymized at the customer's request). Public help center in English and German, maintained by a dedicated knowledge and content team.

**The Move:** Freshdesk help center to HubSpot Service Hub Enterprise Knowledge Base. Knowledge base only — tickets and contacts were out of scope.

**The Scope:** 361 Freshdesk parent articles, published as 585 live language versions (322 English, 263 German) plus drafts, across two HubSpot knowledge bases. 1,394 inline images, 1,767 internal links, the full category and folder hierarchy, tags and meta descriptions.

**The Roadblock:** HubSpot has no public write API for knowledge articles. Every create, update, translation pairing and publish had to run through the same internal endpoints the HubSpot editor calls, on a session that expires in a day or two. HubSpot's native Freshdesk importer caps at 400 articles, drops tables, strips custom-styled divs, and has no translation pairing. And HubSpot's redirect tool exact-matches Freshdesk's combined `{id}-{slug}` path segment, so a wildcard on the article ID cannot cover renamed slugs or bare-ID bookmarks.

**The Outcome:** Both knowledge bases populated with paired language variants, theme appearance inlined, publish state preserved article by article, and CHANGED at zero after the final delta. Project signed off in early September 2026.

---

## By the Numbers

- **Internal links rewritten to HubSpot:** 1,767
- **Redirect-variant rows delivered:** 1,588
- **Inline images re-hosted to HubSpot File Manager:** 1,394
- **Bare-ID in-article links found in a 585-article scan:** 1,093
- **Audit sheet rows (one per language version):** 633
- **Live Freshdesk language versions:** 585 (322 English, 263 German)
- **Freshdesk parent articles:** 361
- **Stale-slug links after title renames:** 301
- **Table-of-contents anchors auto-repaired:** 80
- **HubSpot knowledge bases built:** 2

---

## The Challenge: A Themed, Bilingual Help Center and a Destination That Cannot Be Written To

Most migration enquiries open with a volume and a deadline. This one opened with five qualifying questions, and the first was the one that decides whether the project is possible at all: HubSpot has no public Knowledge Base API, so how does article creation and language-variant pairing actually happen — browser automation, an undocumented API, or something else, and how stable has it been in practice?

The remaining four covered re-hosting of Freshdesk CDN images, automatic rewriting of internal article links, and whether a full Freshdesk-to-HubSpot article ID mapping log would be provided for post-migration audit and redirect setup. The customer's knowledge and content team had already worked out where this kind of project breaks. A quote was explicitly contingent on answering question one clearly.

The reason those questions mattered is that the help center was not a flat set of documents. English was the primary language in Freshdesk with German stored as a translation variant, and both had to arrive in HubSpot as correctly paired language-variant groups rather than two separate flat articles. A reader who opened an article the day after cutover had to see the same structure, the same language, and the same working destinations as the day before.

That parity bar set the requirements:

- **Full bilingual corpus.** English originals and German translations, paired as HubSpot language variants rather than duplicate primary-language articles.
- **Hierarchy intact.** The Freshdesk category and folder tree, recreated across two separate HubSpot knowledge bases.
- **Formatting intact.** Callouts, tabs, tables, code blocks, videos and heading anchors, so table-of-contents links still scroll to the right place.
- **No residual Freshdesk dependency.** Every inline image and article attachment re-hosted, so nothing still loaded from Freshdesk after cutover.
- **Honest links.** Every working internal link repointed at the matching HubSpot article or category — and every already-broken Freshdesk link left alone rather than guessed at.
- **Publish state preserved.** Live articles stay live, genuine drafts stay drafts. Nothing auto-publishes.
- **A moving source.** Freshdesk stayed the editing surface for weeks, so the pipeline had to keep syncing until go-live and then hand over a redirect map for every public URL shape still in circulation.

Two platform limits sat underneath all of it. HubSpot's documented CMS and knowledge base APIs are effectively read-only for article bodies — creating, updating, pairing translations, publishing and placing articles in categories exist only as the internal endpoints the HubSpot knowledge editor itself calls. And in Freshdesk, an article's numeric ID and its title slug share a single URL path segment, which HubSpot's exact-match redirect tool cannot wildcard.

---

## Why HubSpot's Native Freshdesk Importer Was Not Enough

HubSpot ships a built-in knowledge base import: Smart copy from a Freshdesk help-center homepage, or a CSV of URL, title, category and body. For a small, single-language, plainly formatted help center, that is a reasonable one-shot copy. This corpus ruled it out on six counts, most of them documented by HubSpot itself:

- **The 400-article cap.** At 361 parent articles, English alone would sit at the ceiling. At 585 live bilingual versions plus a category tree, one run was not an option.
- **Tables are unsupported.** They come through as placeholders and land in an "articles to fix" CSV. These articles are full of checklists, fee matrices and export layouts. A placeholder is a broken article, not a migration.
- **Custom-styled divs are stripped.** The importer's own release notes flag stripped `<div>`s and poor handling of nested lists and side-by-side images. The customer's live look is exactly that: Froala and theme markup driving callouts, tabs, modals and colored nested lists.
- **No translation pairing.** Smart copy crawls one homepage into one knowledge base. CSV import has no language or translation-link column, and translated rows land as extra primary-language articles.
- **All-drafts or all-published.** Publishing everything would have pushed unfinished articles live; drafting everything would have 404'd the help center.
- **No delta.** CSV Article Body keeps source image URLs, so the help center stays dependent on Freshdesk. And a second import is an overwrite or a duplicate, not a sync — there is no fingerprint comparison and no in-place update that preserves HubSpot IDs.

The native importer is a bulk paste of article HTML. This project needed a bilingual, theme-faithful, link-rewritten, resumable sync that could survive Freshdesk's API locking and HubSpot's sessions expiring. Different jobs.

---

## The ClonePartner Solution: Treating the Editor's Own Contract as the Migration API

**Discovery and sample.** Endpoints were discovered on a HubSpot test portal first, by driving the knowledge editor and capturing the XHR calls the product already makes. Article create, language-variant pairing, full-object draft PATCH, category create and rename, tag upsert and publish were all verified there before a single line of customer content was written. Five real articles — with German variants, category path, images and links — then went to the live customer portal so the team could review pairing, placement, and how callouts and videos looked after HubSpot's editor had opened the article. Only after that review did the full corpus move.

**Writing through the editor API.** HubSpot's knowledge editor talks to internal endpoints that require the portal owner's authenticated session and CSRF header, and expire in roughly a day or two. ClonePartner treated that as a first-class constraint rather than an inconvenience:

- Every write path is resumable. A session timeout mid-run cannot create duplicates, because destination IDs are written to the ledger after each article rather than at the end of a batch.
- PATCH is a full-object replace. Sending only changed fields returns a 500, and omitting the `translations` object risks wiping the German variant, so the pipeline always GETs first and spreads.
- New articles land as Draft. Publish is a separate, verified call, used only where the article was already live — so a delta update can never unpublish the help center.
- Articles are updated in place, never deleted and recreated, so HubSpot IDs and URLs stay stable across every sync.

**Surviving two HTML rewrites.** A body that looks right immediately after PATCH is not the body a reviewer sees. HubSpot's backend normalizes embeds on write, and the editor normalizes again when a human opens the article — unwrapping `h1` and `h2`, stripping heading `id`s, and rebuilding video chrome from iframe attributes. The pipeline demotes `h1`/`h2` to `h3` before push, moves heading anchors to `<a id data-hs-anchor="true">` so TOC links survive, emits real embed dimensions as attributes, and verifies by opening articles in the editor headlessly and reading the body back — not by trusting the write response.

**Theme, media and links.** Freshdesk article HTML is mostly class names; the appearance lives in the help-center theme stylesheet, which HubSpot does not ship. ClonePartner extracted the live theme and inlined both families of rules — class-based widgets, and element-level styles for `code`, `pre`, `blockquote`, nested list bullets and paragraph color. Sweeping only the classes would have left the customer to discover the second family. CSS counters cannot be inlined, so marker elements were injected instead. All 1,394 images and every article attachment were re-hosted into HubSpot File Manager, deduplicated by source URL, with attachments rendered as a download section because HubSpot Knowledge Base has no native attachment field. Internal links were rewritten only after the full corpus existed, to root-relative HubSpot paths, so the later custom-domain cutover would not hard-code the temporary HubSpot host into every article.

**Bilingual structure.** English and German were built as two separate HubSpot knowledge bases, with German articles using locale `de-de` to match the KB variation and `translatedFromId` set in both directions. Categories were created in English, then the German stubs renamed.

> *Thank you for confirming this and for rewriting the internal links so quickly, we really appreciate it.*
>
> <footer>Knowledge &amp; Content Manager</footer>

---

## Delta Syncs, Validation and the Redirect Sheet

Freshdesk remained the editing surface through review, so deltas ran repeatedly: re-read Freshdesk, classify each language version as NEW, CHANGED, IN_SYNC or gone, dry-run the transforms, then push only NEW and CHANGED parent IDs in place.

Crucially, those runs were not saved for cutover day. A single delta held back until go-live has to reconcile everything that changed since the original extraction — on a live help center under active editing, that can be weeks of accumulated edits arriving in one pass, at the exact moment there is least room to investigate anything unexpected. Running interim deltas through the review period keeps each one small: the final pass processes a few days of changes rather than the whole review window. It also means the delta path is exercised and proven repeatedly on real content before the run that actually matters, instead of being used in earnest for the first time under a deadline. By the last sync, only a handful of pairs were left to reconcile, because the earlier passes had already absorbed the bulk.

Classification runs on a content fingerprint — title, body, tags, meta, attachments, placement — not on `updated_at`. That is not a preference. Freshdesk treats editing a published article as flipping its status to draft while the previous published version stays live, so status and timestamps both lie about what visitors are actually seeing.

Partway through, the integrated Freshdesk account moved to `needs_reauth` and authenticated pulls started returning 401 while the customer was still publishing. ClonePartner switched the re-pull to the public folder and article JSON the help center already serves, capturing live published bodies without waiting on new API keys. Genuine drafts 404 on that path and were left untouched in the snapshot rather than being mistaken for deletions.

Validation ran as two separate audits, because they answer different questions. Source fidelity asks whether the pushed snapshot matches what was meant to be pushed. Live soundness asks whether links resolve, images load and styles still exist after HubSpot and the editor have rewritten the HTML. The second audit caught defects the first could not, including surviving theme rules and editor-stripped anchors. Checkers were themselves verified against known false positives — HubSpot search lag after bulk writes, `data-local-id` matching a naive `id="` regex, protocol-relative URLs, and editor-rounded padding.

Links already broken in Freshdesk were not quietly fixed. Deleted targets, help-center home and ticket URLs with no article equivalent, and TOC anchors that never matched a heading were carried across as-is and listed for the customer. Where heading text still matched uniquely, 80 TOC entries were auto-repaired; the rest were too ambiguous to guess at.

The redirect work came out of the customer's own developers. They confirmed HubSpot's redirect tool performs exact-string matching only and cannot wildcard a numeric ID while ignoring the rest of the path, because Freshdesk combines ID and slug into one segment. Their internal scan across all 585 live articles found 1,093 in-article links using the bare article ID and 301 still using a stale slug after a title rename — occurrences inside article bodies, many of them repeats of the same string.

Because the migration had already had to identify every variant to rewrite internal links correctly, those variants were still on hand. ClonePartner expanded the audit into an explicit 1,588-row redirect sheet covering 633 language versions: one row per distinct Freshdesk URL string, typed as `current_title`, `bare_id` or `historical_slug`, with percent-encoded and Unicode forms as separate rows where they differ, all pointing at the same HubSpot target. Every language version has a canonical row and a bare-ID row. Rows with no HubSpot target — internal test articles never published to the help center — are labeled so they are not imported as live redirects.

> *The redirect sheet looks exactly like what we need and saves us a lot of time.*

---

## The Results: Two Knowledge Bases Live, CHANGED at Zero

On 1 September 2026, a pre-cutover delta updated 25 changed article pairs in place, published articles staying published and drafts staying drafts, plus one new German article that had no HubSpot version yet and was created as a draft for the customer to review. That pass cleared the backlog of edits made during review, so when the final delta ran a few days later — re-reading Freshdesk from the public help center — only the remaining few changed pairs were left to push. After that run, CHANGED was zero.

The help center now exists as 361 Freshdesk parent articles represented across 633 English and German HubSpot language versions, with the category hierarchy recreated, German variants paired to their English parents, 1,394 images and downloads hosted in HubSpot File Manager, and 1,767 working internal links pointed at HubSpot. Theme appearance is inlined, so nothing renders from Freshdesk CSS.

Publish state is aligned article by article, and deliberately not force-matched. Freshdesk's "Published" and "Published (edits pending)" both mean live; HubSpot's "Published" means live in HubSpot. Four language versions were live in Freshdesk but still Draft in HubSpot at handover, and stayed that way — the pipeline does not auto-publish a HubSpot draft even when the source is live, because a migration should not decide what goes public.

The team holds two customer-facing sheets. The article audit carries one row per language version with the Freshdesk URL and publication status, the HubSpot URL and publication status, and both article IDs — the status columns added at the customer's request so their team could see at a glance which articles were live in each tool. The redirect sheet carries 1,588 rows ready for HubSpot's exact-match redirect tool, and went straight to the customer's developers for implementation. A hidden HTML comment in each HubSpot body keeps the Freshdesk source ID recoverable from inside the editor.

The customer confirmed the project complete in early September 2026, with remaining cutover work — mapping the custom help-center domain and loading the redirect sheet — on their side. Because in-article links are root-relative, that domain switch will not require rewriting every article again, which is why slug-prefix decisions were locked before go-live rather than after.

> *Thank you so much for your excellent work throughout this migration. It's been a smooth and well-organized process, and I really appreciate all your support and attention to detail.*

The redirect sheet went to the customer's developers for implementation, and the account team offered to point colleagues elsewhere in the business toward the same approach.

> *We are happy to recommend ClonePartner to other teams within our company who may be considering a similar migration.*

---

## What Made This Migration Different

For teams migrating a help center into a platform with limited write access, three details from this project are worth noting:

- **When the destination has no public write API, the product's own editor is the API.** HubSpot's knowledge editor has a contract; it just is not documented for third parties. Discovering it on a test portal, building every write path to resume after session expiry, and refusing delete-and-recreate as a shortcut turns an unsupported surface into a reliable one. The alternative is accepting whatever the native importer happens to do.

- **A native importer is a bulk paste, not a migration.** Article caps, dropped tables, stripped styling and missing translation pairing are not edge cases discovered late. They are documented, and they are checkable against a real corpus before anyone signs anything. The gap between "the importer supports this" and "our help center needs this" is the whole scope of the project.

- **Delta on a content fingerprint, never on a status flag.** Freshdesk marks a published article as draft the moment someone edits it, while the previous version keeps serving to visitors. Any delta driven by `updated_at` or by the source platform's own draft flag will push the wrong set of articles. Comparing a fingerprint of the content itself is the only classification that survives contact with a source system that is still being edited.

- **Parity includes honesty.** Links that 404 in the source should 404 in the destination until the customer chooses a new target. Inventing destinations for 1,093 bare-ID references and 301 stale slugs would have looked like a cleaner migration and behaved like a worse one.
