---
title: "How to Export Data from HubSpot CMS Hub: Methods, APIs & Limits"
slug: how-to-export-data-from-hubspot-cms-hub-methods-apis-limits
date: 2026-08-20
author: Roopi
categories: [HubSpot, Migration Guide]
excerpt: "Complete guide to exporting HubSpot CMS Hub data — blog posts, pages, HubDB, files — via UI, CLI, and CMS APIs, with rate limits and portability traps."
tldr: "HubSpot CMS Hub has no single export button. Use the CMS APIs for full extraction, the CLI for themes, and download CDN assets before cancellation."
canonical: https://clonepartner.com/blog/how-to-export-data-from-hubspot-cms-hub-methods-apis-limits/
---

# How to Export Data from HubSpot CMS Hub: Methods, APIs & Limits


# How to Export Data from HubSpot CMS Hub: Methods, APIs & Limits

> [!NOTE]
> **TL;DR — HubSpot CMS Hub Data Export**
>
> HubSpot CMS Hub (renamed **Content Hub** in April 2024) has no single "export everything" button. The native UI gives you blog CSV metadata (without body content), an HTML bulk export (without structured data), and HubDB table CSVs (without relational integrity). For a full-fidelity extraction — body HTML, `layoutSections`, SEO metadata, HubDB rows, and file assets — you need the CMS APIs: Blog Posts (`/cms/blogs/2026-03/posts`), Pages (`/cms/v3/pages/site-pages` and `/landing-pages`), HubDB, and the Files API. Developer assets (themes, templates, custom modules) require the CLI or Source Code API. API rate limits range from 100 requests/10s (Free/Starter) to 190 requests/10s (Pro/Enterprise), with daily caps of 250K–1M calls. The biggest portability constraint is HubL — HubSpot's proprietary markup language — which is embedded in API responses and must be parsed before import into any other CMS.
>
> *Last verified against HubSpot API documentation as of August 2026. HubSpot is actively rolling out date-versioned API paths (e.g., `/cms/blogs/2026-03/`). Check the [HubSpot API changelog](https://developers.hubspot.com/changelog) for updates.*

Exporting data from HubSpot CMS Hub is fundamentally different from exporting CRM records. A CRM export deals with flat, structured data — Contacts, Deals, Tickets. A CMS export involves hierarchical page content, proprietary templating code (HubL), relational databases (HubDB), unstructured file assets, and dynamic page generation logic.

There is no single "export site" button. If you are migrating to WordPress, Webflow, Contentful, or any other platform, relying solely on native UI exports will leave you with missing body content, broken image links, flattened database relationships, and raw HubL code rendering on your live pages.

This guide covers exactly how to extract every layer of your HubSpot CMS, the technical constraints of each method, the actual API response structures you'll encounter, and the portability traps that catch teams mid-migration.

## What Data Lives in HubSpot CMS Hub?

HubSpot stores web content across several distinct systems, each with its own export path and its own blind spots.

| Content Type | Native UI Export | API Export | Key Gaps |
|---|---|---|---|
| **Blog Posts** | ✅ CSV (metadata only), HTML bulk | ✅ `GET /cms/blogs/2026-03/posts` | CSV omits body content, meta descriptions, featured images |
| **Website Pages** | ✅ HTML bulk export | ✅ `GET /cms/v3/pages/site-pages` | HTML export loses structured `layoutSections` data |
| **Landing Pages** | ✅ HTML bulk export | ✅ `GET /cms/v3/pages/landing-pages` | Same `layoutSections` caveat |
| **HubDB Tables** | ✅ CSV from table UI | ✅ `GET /cms/v3/hubdb/tables/{id}/rows` | CSV flattens foreign keys; 10,000 row max per table |
| **Files & Assets** | ✅ Individual download | ✅ Files API | URLs tied to HubSpot CDN — must re-host |
| **Themes & Modules** | ✅ Via Design Manager | ✅ Source Code API / CLI | HubL code not portable to other platforms |
| **Forms** | ❌ No bulk form export | ✅ Forms API | Form submissions exportable separately |
| **URL Redirects** | ✅ CSV from Settings | ✅ URL Redirects API | Large sets need pagination |

> [!WARNING]
> **CMS Hub vs. Content Hub naming:** HubSpot renamed CMS Hub to Content Hub in April 2024. The underlying CMS — website builder, hosting, themes, HubDB — is the same product. Existing CMS Hub customers were migrated automatically. The APIs are unchanged. This guide uses "CMS Hub" because that's still the dominant search term among engineers.

## Method 1: Native UI Exports

The HubSpot UI provides limited export capabilities. These are suitable for inventories, archives, and flat data extraction — but not for structured migrations.

### Blog CSV Export

The fastest path to a blog inventory is the built-in CSV export.

1. Navigate to **Content > Blog**
2. Click **Actions** → **Export blog posts**
3. Select CSV, XLS, or XLSX
4. HubSpot emails you a download link

This gives you post titles, URLs, publish dates, and authors in spreadsheet format. It does not include the full body content, meta descriptions, or featured image URLs.

Use the CSV export as a verification checklist — to confirm you have every post — not as your primary migration method.

### HTML Bulk Export

On the content index page, click the Actions dropdown and select **Export all pages and blog posts (HTML)**. Once processed, you'll receive a download link through email and in your notification center.

This produces rendered HTML files with template files placed in the **Styles** folder. It is useful for archival or content parity checks, but not for structured migration. The HTML export flattens everything — you lose `layoutSections` structure, module-level field values, and dynamic content powered by HubDB or smart rules.

> [!TIP]
> **When the UI export is enough:** Archiving a site for compliance, creating a static backup, or verifying content parity after migration. If you're rebuilding on another platform and need structured content, use the API.

### HubDB CSV Export

HubDB tables can be exported from the UI as CSV or XLSX. Navigate to the table and use **Actions > Export**.

The trade-off: HubDB supports linked rows (foreign keys referencing other tables). A CSV export flattens these relationships into text strings. If you need to rebuild a relational database in your target CMS — [Webflow CMS Collections](https://clonepartner.com/blog/blog/how-to-export-data-from-webflow-methods-api-limits-portability/), Contentful, or similar — you lose structural integrity. Use the API to preserve relational IDs.

### Report-Based Page Exports

You can export page data from **Reporting > Reports > Web traffic analysis > Pages**, but this export contains page-level data and metrics only — not full HTML content. A Marketing Hub or Content Hub Professional or Enterprise subscription is required for unsummarized page data.

This is an analytics export, not a content export. Useful for URL inventories and traffic analysis, not for migration.

## Method 2: Developer Assets via CLI and Source Code API

Themes, templates, and custom modules live in HubSpot's **Developer File System** — separate from both the content APIs and the file manager. Neither the standard UI export nor the REST APIs for pages will give you this code.

### HubSpot CLI

Install via npm and authenticate with your portal:

```bash
npm install -g @hubspot/cli
hs auth
```

Then fetch your theme and module code:

```bash
# Fetch an entire theme directory
hs fetch @hubspot/my_custom_theme ./local_theme_backup

# Fetch specific custom modules
hs fetch /my_custom_theme/modules ./local_modules
```

> [!WARNING]
> **CLI version matters:** HubSpot CLI v8 moved several commands under the `hs cms` namespace. Older `hs fetch` syntax may not work in newer CLI versions. Check your installed version and consult HubSpot's CLI documentation for current commands.

### Source Code API

Alternatively, the Source Code API lets you retrieve developer file system assets programmatically. You **cannot download an entire folder in one call** — you must fetch folder metadata and recursively retrieve children.

### The Portability Trap

The code you download is written in **HubL** — HubSpot's proprietary markup language — and relies on HubSpot's rendering engine. You cannot upload a HubSpot theme to WordPress, Shopify, or Webflow. The value of this extraction is reference: your developers can see the logic, CSS, and module schemas so they can rebuild them in the target system.

## Method 3: CMS APIs for Full-Fidelity Extraction

For a real migration, you need the CMS APIs. This is the only way to get structured page data, preserve database relationships, and automate the extraction pipeline.

### Blog Posts API

Retrieve all blog posts via `GET /cms/blogs/2026-03/posts`. The response includes the full HTML body (`postBody`), meta description, featured image URL, author, tags, publish date, slug, and SEO settings.

```bash
curl 'https://api.hubapi.com/cms/blogs/2026-03/posts?limit=100&state=PUBLISHED' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

A truncated example of a blog post API response:

```json
{
  "id": "12345678901",
  "slug": "how-to-export-hubspot-data",
  "htmlTitle": "How to Export Data from HubSpot",
  "postBody": "<div class=\"blog-post-body\">\n<h2>Getting started</h2>\n<p>Here is the body content...</p>\n{% module \"cta_module\" path=\"/themes/my_theme/modules/cta\" %}\n</div>",
  "metaDescription": "A complete guide to exporting HubSpot CMS data.",
  "featuredImage": "https://f.hubspotusercontent40.net/hubfs/123/image.jpg",
  "publishDate": "2025-06-01T10:00:00.000Z",
  "blogAuthorId": "987654321",
  "tagIds": ["111", "222"],
  "state": "PUBLISHED",
  "paging": {
    "next": {
      "after": "NTI1Cg%3D%3D"
    }
  }
}
```

Paginate using the `after` cursor from `paging.next.after`. The default page size for blog posts is **20** — set `limit=100` to reduce round trips.

**Author data gotcha:** The `authorName` field returns the name of the user who most recently published the post, **not** the blog author profile. To preserve real authorship, resolve `blogAuthorId` against the Blog Authors API (`GET /cms/blogs/2026-03/authors/{id}`). If tag landing pages matter in the destination CMS, export tags separately — blog tags are still documented under `/cms/v3/blogs/tags`, not the date-versioned path.

> [!WARNING]
> **Date-versioned API paths:** HubSpot adopted date-based versioning (e.g., `/cms/blogs/2026-03/`) starting March 2026. Older `/cms/v3/` paths still work but may be deprecated. Not every CMS endpoint has moved to the new format yet. Check the [HubSpot API changelog](https://developers.hubspot.com/changelog) for current paths.

### Website Pages and Landing Pages API

Retrieve pages via `GET /cms/v3/pages/site-pages` or `GET /cms/v3/pages/landing-pages`. Filter published pages with `state__in=PUBLISHED_OR_SCHEDULED`.

```bash
curl 'https://api.hubapi.com/cms/pages/2026-03/site-pages?state__in=PUBLISHED_OR_SCHEDULED&limit=100&property=id&property=name&property=url&property=slug&property=templatePath&property=layoutSections&property=translations' \
  --header 'Authorization: Bearer YOUR_TOKEN'
```

A truncated example of a `layoutSections` response from a drag-and-drop page:

```json
{
  "id": "98765432101",
  "name": "Product Features Page",
  "slug": "features",
  "state": "PUBLISHED",
  "layoutSections": {
    "dnd_section_1": {
      "type": "cell",
      "rows": [
        {
          "0": {
            "cells": [
              {
                "0": {
                  "widgets": [
                    {
                      "id": "widget_abc123",
                      "type": "rich_text",
                      "body": {
                        "html": "<h2>Our core features</h2><p>Description here...</p>"
                      }
                    },
                    {
                      "id": "widget_def456",
                      "type": "linked_image",
                      "body": {
                        "img": {
                          "src": "https://f.hubspotusercontent40.net/hubfs/123/feature.jpg",
                          "alt": "Feature diagram"
                        },
                        "href": "/learn-more"
                      }
                    }
                  ]
                }
              }
            ]
          }
        }
      ]
    }
  }
}
```

**The `layoutSections` problem:** For pages built with HubSpot's drag-and-drop editor, the content is not in a single HTML body field. It's distributed across nested `layoutSections` objects — sections, rows, columns, and modules — each containing their own field values. You need to traverse this structure, extract content from each module, and convert it to your target CMS's content format.

This is the single biggest engineering challenge in HubSpot CMS export. Here is the traversal logic and an example mapping to Contentful and Webflow formats:

```python
def extract_modules(layout_sections):
    modules = []
    for section_key, section in layout_sections.items():
        for row_key, row in section.get('rows', [{}])[0].items():
            for cell in row.get('cells', {}).values():
                for widget in cell.get('widgets', []):
                    widget_type = widget.get('type')
                    body = widget.get('body', {})
                    
                    if widget_type == 'rich_text':
                        modules.append({
                            'type': 'rich_text',
                            'html': body.get('html', ''),
                            # Contentful: maps to 'richText' field type
                            # Webflow: maps to Rich Text element
                        })
                    elif widget_type == 'linked_image':
                        img = body.get('img', {})
                        modules.append({
                            'type': 'image',
                            'src': img.get('src', ''),
                            'alt': img.get('alt', ''),
                            'href': body.get('href', ''),
                            # Contentful: maps to 'Asset' with link field
                            # Webflow: maps to Image element with Link wrapper
                        })
                    elif widget_type == 'custom_widget':
                        # Custom modules require manual schema mapping
                        modules.append({
                            'type': 'custom',
                            'module_id': widget.get('id'),
                            'fields': body,
                            'requires_manual_mapping': True
                        })
    return modules
```

**Filtering edge case:** `currentState` is a generated field and cannot be used as a filter parameter. To separate scheduled from published content, use `state__in=PUBLISHED_OR_SCHEDULED` with `publishDate__gt` or `publishDate__lt`. The page model also exposes multi-language and A/B test relationships through `translatedFromId`, `translations`, `abTestId`, and `abStatus`.

**Fetching all language variants of a page:** To retrieve every language variant, first fetch the primary page to get its `id`, then query `GET /cms/v3/pages/site-pages?translatedFromId={primaryPageId}`. This returns all child translations. Each variant's `language` field identifies the locale (e.g., `"fr"`, `"de"`). Build a map of `primaryPageId → [variantId, locale]` tuples during extraction so you can reconstruct language groups in the target CMS.

### HubDB Tables API

HubDB is HubSpot's built-in relational table store. If your site uses HubDB-driven dynamic pages, you need more than a simple page export.

Extract the schema first to understand column types:

```http
GET https://api.hubapi.com/cms/v3/hubdb/tables/{tableId}
```

A truncated schema response:

```json
{
  "id": "5678901",
  "name": "products",
  "columns": [
    { "id": "1", "name": "name", "type": "TEXT" },
    { "id": "2", "name": "price", "type": "NUMBER" },
    { "id": "3", "name": "category_id", "type": "FOREIGN_ID", "foreignTableId": "5678902" },
    { "id": "4", "name": "description", "type": "RICHTEXT" }
  ]
}
```

Then extract rows:

```http
GET https://api.hubapi.com/cms/v3/hubdb/tables/{tableId}/rows?limit=1000
```

A truncated row response showing a foreign key:

```json
{
  "results": [
    {
      "id": "1",
      "values": {
        "1": "Widget Pro",
        "2": 49.99,
        "3": { "id": "42", "type": "foreignid" },
        "4": "<p>Our flagship product...</p>"
      }
    }
  ]
}
```

The `"3"` column returns `{ "id": "42", "type": "foreignid" }` — the row ID from the referenced table. Maintain an identity map during migration: `{ hubspot_row_id: target_cms_record_id }`. The CSV export from the UI flattens column `"3"` to a plain text string (e.g., `"42"`), losing the type context. If you are rebuilding in Webflow CMS, this maps to a [Reference field](https://clonepartner.com/blog/blog/how-to-export-data-from-webflow-methods-api-limits-portability/); in Contentful, to a Link to Entry.

**Documented limits:** 1,000 tables per account, 1 million rows per account, 250 columns per table, 10,000 rows per table, 10 dynamic pages per table, and 65,000 characters per rich text column. The default API page size is 1,000 rows.

**Rate limiting difference:** Unauthenticated public `GET` requests against public HubDB tables are capped at **10 requests per second** and do not count against the daily limit. Authenticated requests fall under standard app limits.

> [!CAUTION]
> **Dynamic pages:** If a page uses `dynamicPageHubDbTableId`, export the page shell and the HubDB table together. You need the table schema, rows, `hs_path`, `hs_name`, and any dynamic metadata mapping to recreate the generated URLs on your target platform.

### Files API

Images, PDFs, and other media in HubSpot's file manager are accessible via the Files API. Every file URL points to HubSpot's CDN (`f.hubspotusercontent*.net`).

**Private files** require a signed URL or the direct download endpoint — the normal metadata URL will 404 for private assets. Hidden files are not returned by list operations and require the `files.ui_hidden.read` scope when fetched by ID. If your migration depends on hidden or private assets, test that path early.

### Forms and URL Redirects

A page can render perfectly in HTML and still be incomplete as a migration artifact if you miss forms and redirects.

**Forms:** Export form definitions (fields, validation rules, redirect URLs) via the Forms API. Historical form submissions are available from the UI (**Site Settings**) or via the submissions endpoint. Form definitions and submissions are separate — capture both if needed.

**URL Redirects:** Export from **Settings > Content > URL Redirects** as CSV, or via the URL Redirects API. The API returns `routePrefix` (old URL) and `destination` (new URL). Format the output for your target hosting provider — `.htaccess`, `vercel.json`, Webflow's 301 redirect CSV, or whatever the platform requires.

> [!WARNING]
> All HubSpot forms lose their paid features once your account is deactivated. If you're canceling, replace embedded forms on external sites before account closure.

## The HubL Problem: Why Template Code Isn't Portable

The most severe portability constraint in HubSpot CMS export is **HubL** — HubSpot's proprietary markup language. HubL uses Jinja2-style syntax and is executed server-side by HubSpot's rendering engine. It is not supported by any other CMS or web framework.

When you query the Pages or Blog API, the `html` field is not always static HTML. It can contain dynamic macros:

```html
<h1>Welcome to our site</h1>
{% module "my_custom_cta" path="/themes/my_theme/modules/cta", label="Primary CTA" %}
{% widget_block rich_text "intro_text" %}
  <p>Default content here</p>
{% end_widget_block %}
<p>{{ custom_company_variable }}</p>
{% if contact.lifecyclestage == "customer" %}
  <div class="customer-only">...</div>
{% endif %}
```

Push this raw payload into another CMS and the HubL tags will render as visible broken text or break the page layout entirely.

**HubL tag categories your parser must handle:**

| Tag Pattern | Description | Handling Strategy |
|---|---|---|
| `{% module "name" path="..." %}` | Inline module insertion | Extract `path` to identify module type; map to target component |
| `{% widget_block type "name" %}...{% end_widget_block %}` | Block-level widget with default content | Extract inner HTML as fallback content |
| `{{ variable }}` | Variable interpolation | Strip or replace with static default |
| `{% if condition %}...{% endif %}` | Conditional rendering | Keep default branch; document smart-content variants |
| `{% for item in items %}...{% endfor %}` | Loop constructs | Strip; document that dynamic content requires rebuild |
| `{# comment #}` | HubL comments | Safe to strip |

**Parsing approach:** Regex is sufficient for stripping simple variable interpolation (`{{ ... }}`) and comments (`{# ... #}`). For block constructs (`{% ... %}...{% end_... %}`), use a recursive descent parser or a Jinja2-compatible tokenizer (since HubL syntax is a superset of Jinja2). The Python `jinja2` library can tokenize most HubL constructs without executing them — use `jinja2.Environment().parse()` to walk the AST and identify node types for targeted extraction or removal.

For `{% module %}` tags specifically, the `path` attribute identifies which custom module is being invoked. Cross-reference the path against your CLI-extracted theme files to find the module's field schema (`fields.json`) and reconstruct equivalent content from its default field values.

## The CDN Asset Trap

When you export pages or blogs via any method, image URLs in the HTML point to HubSpot's CDN (`f.hubspotusercontent*.net`).

If you migrate your site but leave these URLs in the content, images will load fine — until you cancel your HubSpot subscription. The moment the account closes, the CDN links die and every image on your new site returns a 404.

**The fix:**

1. List all files via the Files API
2. Download each binary (accounting for private and hidden files)
3. Upload to your new CDN (AWS S3, Cloudinary, or the target CMS's asset manager)
4. Run a find-and-replace across all blog post bodies, page HTML, and `layoutSections` content to swap every `hubspotusercontent*.net` URL for the new asset URL

This step is easy to forget and painful to fix after launch. The regex pattern to identify HubSpot CDN URLs: `https?://f\.hubspotusercontent\d+\.net/[^\s"'<>]+`

## HubSpot API Rate Limits for CMS Export

Rate limits are the primary constraint on API-based extraction. Limits vary by subscription tier and app type.

### Private App Rate Limits

| Subscription Tier | Burst Limit (per 10s, per app) | Daily Limit (per account) |
|---|---|---|
| Free / Starter | 100 requests | 250,000 |
| Professional | 190 requests | 625,000 |
| Enterprise | 190 requests | 1,000,000 |
| API Limit Increase add-on | 250 requests | +1,000,000 (max 2 purchases) |

*Source: [HubSpot API usage guidelines](https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines)*

The burst limit applies individually per app. The daily limit is shared across all apps within the same HubSpot account. If you have multiple integrations running alongside your export script, they compete for the same daily quota.

**Public OAuth apps** distributed via the HubSpot Marketplace are limited to **110 requests per 10 seconds** per portal.

**CRM Search API** is capped at **4 requests per second** shared across all search endpoints — not per endpoint.

### Error Handling and Retry Patterns

HubSpot returns HTTP 429 when you exceed the burst limit. The response includes a `Retry-After` header specifying how many seconds to wait. Do not retry immediately — implement exponential backoff:

```python
import time
import requests

def hubspot_get(url, token, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, headers={"Authorization": f"Bearer {token}"})
        
        if response.status_code == 200:
            return response.json()
        elif response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 10))
            wait = retry_after * (2 ** attempt)  # exponential backoff
            print(f"Rate limited. Waiting {wait}s (attempt {attempt + 1}/{max_retries})")
            time.sleep(wait)
        elif response.status_code == 504:
            # Gateway timeout on large paginated responses — retry with smaller limit
            wait = 5 * (2 ** attempt)
            print(f"Gateway timeout. Waiting {wait}s before retry")
            time.sleep(wait)
        elif response.status_code in (401, 403):
            raise Exception(f"Auth error {response.status_code}: token invalid or missing scope")
        else:
            raise Exception(f"Unretryable error {response.status_code}: {response.text}")
    
    raise Exception(f"Max retries exceeded for {url}")
```

**HubSpot API error codes relevant to CMS export:**

| HTTP Code | Meaning | Retryable? |
|---|---|---|
| 429 | Rate limit exceeded | Yes — honor `Retry-After` |
| 504 | Gateway timeout (large requests) | Yes — reduce `limit` param |
| 401 | Invalid or expired token | No — re-authenticate |
| 403 | Insufficient scope | No — add scope to private app |
| 404 | Resource not found (deleted page, hidden file) | No — skip and log |

**Token expiry:** Private app tokens in HubSpot do not expire on a time schedule — they remain valid until manually rotated. However, if a token is revoked mid-export (e.g., by an admin), all subsequent calls return 401. Build your extraction script to catch 401 responses distinctly from 429s and halt immediately rather than retrying.

### Pagination

HubSpot uses cursor-based pagination. Responses include a `paging.next.after` token. Append it to subsequent requests (`&after=TOKEN`). Do not rely on offset-based pagination — it is deprecated for most v3 endpoints.

### Practical Impact on Export Time

For a typical CMS export at the Free/Starter rate limit (100 req/10s):

- **100 blog posts:** ~2 API calls at `limit=100`. Completes in under 5 seconds.
- **1,000 website pages:** ~10 API calls for the page list. Completes in under 30 seconds.
- **10,000 blog posts:** ~100 API calls. Completes in approximately 10–15 seconds of API time; wall-clock time ~2–3 minutes with processing overhead.
- **50,000 files:** Each file requires a separate download call. At 100 req/10s burst, listing takes ~500 calls. Downloading binaries is I/O-bound, not API-bound. Expect 2–6 hours depending on file sizes and network throughput.

The CMS content extraction itself is rarely the bottleneck. File download and content transformation are where the time goes.

> [!NOTE]
> **Batch endpoints count as one request.** HubSpot's batch create/update endpoints process up to 100 records per call, counting as a single request against your rate limit. Use batch endpoints where available.

### Incremental and Delta Export

If you need to re-export changes since a specific date — for partial migrations, content sync, or re-runs after a failed job — filter by update timestamp:

```bash
# Export only posts updated after a specific date
curl 'https://api.hubapi.com/cms/blogs/2026-03/posts?updatedAt__gt=2025-01-01T00:00:00.000Z&limit=100' \
  --header 'Authorization: Bearer YOUR_TOKEN'

# Export only pages published in a date range
curl 'https://api.hubapi.com/cms/v3/pages/site-pages?publishDate__gt=2024-06-01T00:00:00.000Z&publishDate__lt=2024-12-31T23:59:59.000Z&limit=100' \
  --header 'Authorization: Bearer YOUR_TOKEN'
```

**Delta export pattern for ongoing sync:**

1. Record the timestamp of your last successful export run.
2. On each subsequent run, query `updatedAt__gt={last_run_timestamp}`.
3. Update your local data store with changed records (upsert by `id`).
4. Advance the timestamp checkpoint only after confirming all records were processed.

This pattern works for blog posts and pages. HubDB does not expose an `updatedAt` filter on row queries — for HubDB delta sync, maintain a local copy and diff by row `id` against a full re-export.

## Step-by-Step: Full CMS Hub Export via API

### Step 1: Create a Private App and Set Scopes

In HubSpot, go to **Settings > Integrations > Private Apps > Create a private app**. Grant scopes:

- `content` — blog posts, pages, landing pages
- `hubdb` — HubDB table access
- `files` — file manager access
- `files.ui_hidden.read` — hidden file access
- `forms` — form definitions and submissions

Copy and securely store the access token. Private app tokens do not expire on a schedule but should be rotated if exposed.

### Step 2: Download Developer Assets

Use the CLI or Source Code API to pull down themes, templates, custom modules, CSS, and JavaScript. This code is reference material for rebuilding on the target platform — not directly importable elsewhere. Specifically capture `fields.json` from each custom module; this file defines the module's configurable fields and is essential for reconstructing content from API-returned field values.

### Step 3: Export Blog Posts

Paginate through `GET /cms/blogs/2026-03/posts?limit=100&state=PUBLISHED` using the `after` cursor. Store each post's `id`, `slug`, `htmlTitle`, `postBody`, `metaDescription`, `featuredImage`, `blogAuthorId`, `tagIds`, and `publishDate`. Resolve `blogAuthorId` against `GET /cms/blogs/2026-03/authors/{id}` for correct attribution — do not rely on `authorName`.

### Step 4: Export Website Pages and Landing Pages

Hit both `GET /cms/v3/pages/site-pages` and `GET /cms/v3/pages/landing-pages` with `state__in=PUBLISHED_OR_SCHEDULED`. Capture the full JSON including `layoutSections`. Run the traversal logic above to extract text, images, CTAs, and custom module field values. For each page with `translatedFromId` set, fetch language variants using `translatedFromId` as a filter parameter.

### Step 5: Export HubDB Tables

List all tables via `GET /cms/v3/hubdb/tables`, then export rows for each via `GET /cms/v3/hubdb/tables/{tableId}/rows?limit=1000`. Store schema definitions (column names, types, foreign keys) alongside row data. Build an identity map of `hubspot_row_id → new_platform_record_id` before inserting into the target system. For dynamic pages, capture `hs_path` and `hs_name` fields.

### Step 6: Download Files and Build a URL Map

List all files via the Files API. Handle private files via the signed URL endpoint. Handle hidden files using the `files.ui_hidden.read` scope. Download each binary. Build a mapping table: `old_hubspot_url → new_cdn_url`. This map is required for Step 8.

### Step 7: Export Forms and URL Redirects

Retrieve form definitions via the Forms API. Export historical submissions separately if needed. Pull URL redirects from Settings or the Redirects API. Format for your target hosting environment.

### Step 8: Rewrite Asset URLs

After uploading files to your new CDN, run a find-and-replace across all blog post bodies, page HTML, and `layoutSections` content. Use the regex `https?://f\.hubspotusercontent\d+\.net/[^\s"'<>]+` to identify URLs, then replace using your URL map from Step 6.

### Step 9: Parse HubL and Map to Target Schema

Run extracted content through a tokenizer to identify HubL tags by category. Strip or replace according to the tag table in the HubL section above. Map `{% module %}` tags to equivalent target CMS components using each module's `fields.json` schema. This is the most labor-intensive step for sites with custom modules.

## What Doesn't Export Cleanly

Every HubSpot CMS migration hits at least one of these edge cases:

**Smart content and personalization rules.** HubSpot's smart content — content that changes based on visitor segment, device, or referral source — has no structured export. The API returns the default variant only. Conditional variants and targeting rules must be manually documented and recreated.

**Multi-language content groups.** HubSpot groups language variants via `translatedFromId`. Rebuilding this grouping on a target platform requires mapping every variant back to its primary page using the filter described in Step 4.

**A/B test variants.** Active A/B tests export as separate page variants with distinct IDs linked via `abTestId`. Decide before export whether to keep the winning variant only or preserve both.

**Historical revision history.** Blog posts have revision endpoints (list, fetch, restore). Pages do not have equivalent revision endpoints in current documentation. Verify page-history requirements before committing to a migration approach.

**Membership and gated content.** No bulk export path. Must be documented manually and recreated.

**CRM-integrated personalization.** Tied to HubSpot CRM contact data at render time. Not portable to any external system.

## Data Portability Summary

**What you keep:**
- Blog post content (HTML body, metadata, SEO fields)
- Page content (via API, including `layoutSections` JSON)
- HubDB structured data (schemas and rows with relational IDs)
- Files and media (must download before cancellation)
- Form definitions and submission history
- URL redirect rules

**What you lose:**
- HubL template logic (must rebuild using extracted module schemas)
- Smart content targeting rules (must document manually)
- CRM-integrated personalization
- Historical revision history for pages
- Membership/gated content configuration
- Connected CRM attribution (page views → contacts)

You can't fetch your HubSpot site in its entirety in a single click. But you can export the different data types that make up your website. The content is portable. The platform logic is not.

## When to DIY vs. Get Help

| Scenario | Recommendation | Estimated Effort |
|---|---|---|
| Small blog (<200 posts), no HubDB | CSV export + API for bodies. Self-serve. | 2–4 hours |
| Medium site (200–1,000 pages), simple templates | API extraction with basic scripting. | 1–2 engineer-days |
| Large site (1,000+ pages) with drag-and-drop pages | `layoutSections` parsing required. | 3–5 engineer-days |
| HubDB-driven dynamic pages | Schema extraction + identity map + content transformation. | 2–3 days |
| Multi-language site | Language variant mapping adds complexity per locale. | +1–2 days per language group |
| 10,000+ files | Bulk download + URL rewriting. Automation required. | 1–2 days |
| Smart content, personalization, A/B tests | Manual audit + recreation. No clean export path. | Project-specific |

If your site is a straightforward blog with standard templates, the API and a weekend of scripting will get you there. Once you add drag-and-drop pages, HubDB relationships, multi-language groups, and tens of thousands of files, the complexity scales non-linearly — primarily because each layer requires a custom transformation step with no standardized tooling.

For a related walkthrough of CRM-side export, see our guide on [exporting data from HubSpot Service Hub](https://clonepartner.com/blog/blog/how-to-export-data-from-hubspot-service-hub-methods-api-limits/).

## Key Decisions Before You Start

1. **Do you need body content or just metadata?** CSV is sufficient for metadata only.
2. **Are pages built with drag-and-drop or custom-coded templates?** Drag-and-drop means `layoutSections` traversal. Custom-coded templates mean simpler HTML body extraction but heavier HubL parsing.
3. **How many files?** Under 1,000 is manageable manually. Over 10,000 requires automation and retry logic.
4. **Do you have HubDB dynamic pages?** These require schema + row export + identity mapping, not just a page export.
5. **Are you doing a one-time migration or ongoing sync?** One-time: full export. Ongoing: implement delta export using `updatedAt__gt` filters.
6. **Keeping HubSpot or leaving entirely?** If leaving, download all files and replace embedded forms before account cancellation — CDN links and form functionality die with the account.

> Need help exporting from HubSpot CMS Hub? Our engineers handle the full extraction — blog posts, pages, HubDB, files, and URL redirects — with zero content loss.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### How do I export blog posts from HubSpot CMS Hub with full content?

The native CSV export only includes titles, URLs, and dates — not body content. To get full HTML bodies, use the Blog Posts API: GET /cms/blogs/2026-03/posts. Paginate with the 'after' cursor and store postBody, metaDescription, and featuredImage for each post. Resolve blogAuthorId via the Blog Authors API for correct attribution.

### Can I export HubSpot page layouts and drag-and-drop content?

Yes, but it's complex. Pages built with the drag-and-drop editor store content in nested layoutSections JSON objects, not a single HTML field. You need to traverse this structure via the Pages API and extract content from each module, section, and column. The HTML bulk export flattens this structure and loses it.

### What happens to HubSpot CMS files after I cancel my account?

All images and files hosted on HubSpot's CDN (f.hubspotusercontent*.net URLs) return 404 errors after account cancellation. You must download all files via the Files API and re-host them on your new CDN before canceling, then update all content references to the new URLs.

### How do I export HubSpot templates and custom modules?

Templates, themes, and custom modules live in the Developer File System — not accessible via the standard content APIs. Use the HubSpot CLI or the Source Code API to download the code. Note that the code is written in HubL and cannot be directly used on any other CMS platform.

### What are HubSpot's API rate limits for CMS content export?

Private apps on Free/Starter get 100 requests per 10 seconds and 250,000 per day. Professional gets 190/10s and 625K/day. Enterprise gets 190/10s and 1M/day. The burst limit is per app; the daily limit is shared across all apps in the account.
