---
title: "Quip to Confluence Migration: Export Scripts & Link Rewriting"
slug: quip-to-confluence-migration-export-scripts-link-rewriting
date: 2026-10-20
author: Raajshekhar Rajan
categories: [Quip, Confluence, Migration Guide]
excerpt: "No native Quip-to-Confluence path exists. This guide covers export scripts, HTML-to-storage-format conversion, and two-pass link rewriting for a complete migration."
tldr: "Quip-to-Confluence migration requires custom scripts: extract via the Automation API (503 rate limits), convert HTML to Confluence Storage Format, then two-pass link rewriting to replace Quip URLs with ac:link elements."
canonical: https://clonepartner.com/blog/quip-to-confluence-migration-export-scripts-link-rewriting
---

# Quip to Confluence Migration: Export Scripts & Link Rewriting


# Quip to Confluence Migration: Export Scripts & Link Rewriting

**There is no native migration path from Quip to Confluence.** Confluence Cloud has no "Import from Quip" option, and Quip retired its own Upload/Import feature in January 2024. No Marketplace app handles the full pipeline end to end. If you need to move a Quip workspace into Confluence, the core work — extraction, HTML-to-storage-format conversion, attachment re-hosting, and link rewriting — is custom engineering. And with Salesforce retiring all Quip products (subscriptions non-renewable after March 1, 2027), the timeline is not optional. ([help.salesforce.com](https://help.salesforce.com/s/articleView?id=005299603&language=sv&type=1))

This guide covers the data model gap between the two platforms, the API constraints on both sides, the scripts needed for extraction and conversion, and the two-pass link rewriting strategy that prevents your migrated wiki from being a graveyard of dead cross-references.

> [!CAUTION]
> **Quip End-of-Life (March 2027):** Salesforce is retiring all Quip products. Paid subscriptions cannot be renewed after March 1, 2027; free users lose access on March 31, 2027. After expiration, the site enters a 90-day read-only phase, then 30 days of blocked logins, then permanent data deletion. Some extraction workflows — resolving Salesforce Data Mentions, adjusting permissions — require **write access**, meaning you must complete them before read-only mode begins. ([help.salesforce.com](https://help.salesforce.com/s/articleView?id=005299603&language=sv&type=1))

## Quip vs. Confluence: The Data Model Mismatch

**Quip** stores everything as **threads** — a single content object that can be a document, spreadsheet, or chat. Each thread has a permanent 11-character alphanumeric ID (e.g., `AbCdEfGhIjK`) and contains rich text as HTML, embedded spreadsheet grids as `<table>` elements, inline images stored as blobs, and a conversation pane with threaded comments.

**Confluence** stores page content in **Confluence Storage Format** — an XHTML dialect with Confluence-specific XML namespaces (`ac:` for structured macros, `ri:` for resource identifiers). Internal links are not standard `<a href>` tags. They use `<ac:link>` elements with `<ri:page>` resource identifiers that reference pages by title or content ID, not by URL.

This architectural gap drives most of the engineering complexity in a Quip-to-Confluence migration.

| Quip Concept | Confluence Equivalent | Mapping Notes |
|---|---|---|
| Thread (document) | Page | Direct map; one thread = one page |
| Thread (spreadsheet) | Page with table or attached XLSX | Embedded formulas are lost; static table only |
| Thread ID (11-char) | Page ID (numeric) | Build a mapping table for link rewriting |
| Folder | Space or parent page | Top-level folders → spaces; nested folders → page tree |
| Folder nesting | Page tree hierarchy | Quip allows a thread in multiple folders; Confluence pages have one parent |
| Inline images | Attachments + `<ac:image>` macro | Must upload as attachment first, then reference via `ri:attachment` |
| Comments (conversation) | Page comments | Positional section anchoring is lost; comments become page-level |
| `@mentions` | `<ri:user>` macro | Requires user-ID mapping between Quip and Atlassian accounts |
| Salesforce Data Mentions | Static text | No Confluence equivalent; resolve to static values before export |
| Live Apps | No equivalent | Archive as HTML/JSON; rebuild manually if needed |

**A note on two undefined Quip concepts that migration scripts must handle:**

- **Live Apps** — Quip's term for embedded interactive widgets inside documents: polls, project trackers, countdown timers, and third-party integrations. They are rendered in the Quip UI but exported as empty or placeholder HTML by the API. They have no Confluence equivalent and must be archived as HTML snapshots or rebuilt manually.
- **Mirrored Salesforce Permissions** — A Quip configuration in which document access is controlled by the viewer's Salesforce object permissions rather than Quip folder permissions. When this is enabled, the Quip API returns Salesforce Data Mentions only if the requesting user's token has visibility into the referenced Salesforce records. Run bulk exports with a service account that has full Salesforce object visibility.

## What Is the Best Quip to Confluence Migration Method?

For most teams, **API-based extraction plus the Confluence REST API** is the right approach. But there are alternatives worth knowing about.

- **Confluence HTML importer** — Confluence accepts a **.zip** containing **.html** files. The folder name becomes the space name and each `.html` filename becomes the page name. The importer preserves headings, hyperlinks, images, tables, and inline code. **Iframes** are unsupported, **code blocks** become plain text, and **page links** become plain hyperlinks. This works as a formatting smoke test, not a production pipeline. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/import-data-from-html-to-confluence/))

- **DOCX import** — Useful if your Quip docs are mostly standalone and you care about preserving comments (Quip documents conversation export only for DOCX and XLSX, not HTML). Atlassian's Word importer is capped at **30 files**, **50 MB total**, and **10 MB per file**. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/import-content-into-confluence-cloud/))

- **Marketplace app** — Atlassian Marketplace lists Swiftix's **Quip to Confluence Integration and Migration** app for Cloud and Data Center. ([marketplace.atlassian.com](https://marketplace.atlassian.com/apps/1231437/quip-to-confluence-integration-and-migration?tab=overview)) No app handles the full pipeline including two-pass link rewriting, attachment re-hosting, and comment migration.

- **Custom API pipeline** — The right choice when page hierarchy, attachments, deep links, retries, and repeatable cutover matter. This is what the rest of this guide covers.

## How to Extract Data from Quip at Scale

The **Quip Automation API** is the only viable path for [bulk extraction](https://clonepartner.com/blog/blog/how-to-export-data-from-quip-methods-api-limits-migration-prep). Manual GUI export — one document at a time — is not a migration strategy.

### Quip API rate limits

| Scope | Limit | Notes |
|---|---|---|
| Per-user (Automation API) | 50 requests/min, 750/hour | The binding constraint for single-token scripts |
| Per-company | 600 requests/min | Allows parallelizing with multiple tokens |
| Bulk Export (DOCX/HTML/XLSX) | 36,000 documents/hour | Async; poll for completion |
| Get Threads V2 batch | 100 thread IDs per call | Useful for batching metadata lookups |
| Cursor expiration | 30 minutes | Checkpoint progress so long runs can resume |
| HTTP status on rate limit | **503** (not 429) | Breaks standard retry logic — see below |

> [!WARNING]
> **Quip returns 503, not 429, when rate-limited.** This is documented Quip API behavior, not a server error. Standard HTTP retry libraries treat 503 as "service unavailable" and may retry aggressively, which makes the rate-limit window worse. Your scripts must explicitly detect 503, read the `X-Ratelimit-Remaining` and `X-Ratelimit-Reset` headers, and implement exponential backoff. ([quip.com](https://quip.com/dev/automation/documentation/all))

### Traversing the folder tree

Quip does not offer a bulk export of your folder structure. Your script must start at the root company folder and recursively walk the directory tree using `GET /1/folders/{folder_id}`. The response returns child folders and threads. Store this hierarchy in a local database (like SQLite) to recreate the equivalent space and page tree in Confluence.

### Extracting thread content

Use `GET /1/threads/{thread_id}` to retrieve each document's HTML body and metadata. The response includes an `attachments` array with metadata for embedded images and files. Download blobs via `GET /1/blob/{thread_id}/{blob_id}` and stage them locally.

For larger workspaces, you have two paths for the HTML body. The higher-level option is Quip's asynchronous **bulk export** endpoint, which returns document URLs after you submit a request (36,000 documents/hour). The lower-level option is **Get Thread HTML V2**, which outputs section IDs on document elements — useful for preserving deep links — and supports cursor-based pagination. ([quip.com](https://quip.com/dev/automation/documentation/all))

**Comments do not come with HTML.** Quip's `include_conversations` flag is available only for DOCX and XLSX bulk exports, not HTML. If you want comments in Confluence, pull them separately via `GET /1/messages/{thread_id}` (paginated at 25 messages per call). ([quip.com](https://quip.com/dev/automation/documentation/all))

### The extraction script

```python
import requests
import time
import os

QUIP_TOKEN = os.environ["QUIP_TOKEN"]
BASE_URL = "https://platform.quip.com/1"
HEADERS = {"Authorization": f"Bearer {QUIP_TOKEN}"}

def quip_get(endpoint, retries=5):
    """GET with 503 rate-limit handling."""
    for attempt in range(retries):
        resp = requests.get(f"{BASE_URL}{endpoint}", headers=HEADERS)
        if resp.status_code == 503:
            wait = int(resp.headers.get("X-Ratelimit-Reset", 60)) - int(time.time())
            wait = max(wait, 2 ** attempt)
            time.sleep(wait)
            continue
        resp.raise_for_status()
        return resp.json()
    raise Exception(f"Rate-limited after {retries} retries: {endpoint}")

def extract_thread(thread_id):
    """Extract thread HTML, metadata, and blob URLs."""
    data = quip_get(f"/threads/{thread_id}")
    thread = data["thread"]
    html_body = data.get("html", "")
    return {
        "id": thread["id"],
        "title": thread["title"],
        "html": html_body,
        "author_id": thread["author_id"],
        "created_usec": thread["created_usec"],
        "updated_usec": thread["updated_usec"],
        "type": thread["type"],  # "document" or "spreadsheet"
    }

def enumerate_folder(folder_id):
    """Recursively list all thread IDs in a folder tree."""
    data = quip_get(f"/folders/{folder_id}")
    thread_ids = [
        c["thread_id"] for c in data.get("children", []) if "thread_id" in c
    ]
    for child in data.get("children", []):
        if "folder_id" in child:
            thread_ids.extend(enumerate_folder(child["folder_id"]))
    return thread_ids
```

> [!WARNING]
> **Blob downloads count against rate limits.** Each inline image requires a separate `GET /1/blob/{thread_id}/{blob_id}` call. A 2,000-document workspace with an average of three images per document means ~6,000 extra API calls just for images — roughly two hours of extraction time at the 50 req/min cap with a single token.

### Multi-token parallelism

The per-company cap of 600 requests/min means you can run multiple user tokens in parallel. With 4 tokens, you reach ~200 requests/min — well under the company ceiling but 4× faster than a single token. Partition your thread list into non-overlapping segments and assign each to a separate worker process.

### Salesforce Data Mentions edge case

When **Mirrored Salesforce Permissions** is enabled, a Salesforce Data Mention is returned by the API only if it has been displayed to the requesting user. Run exports with a service account that has full visibility into the content you expect to migrate. ([quip.com](https://quip.com/dev/automation/documentation/all))

## How to Convert Quip HTML to Confluence Storage Format

**Confluence Storage Format is not regular HTML.** Posting raw Quip HTML into the `body.storage.value` field of the Confluence REST API will produce mangled output — missing formatting, broken links, or pages silently falling back to the legacy editor. The storage format is XHTML with Confluence-specific XML namespaces for links, images, macros, and structured content.

The conversion pipeline:

1. **Parse the Quip HTML** with an HTML parser (BeautifulSoup or lxml). Quip HTML is not always well-formed XML, so use an HTML parser, not an XML parser.
2. **Strip unsupported elements.** Remove Quip-specific attributes (`data-section-id`, `data-quip-*`), Live App embeds, and inline style attributes that Confluence ignores.
3. **Convert elements to storage format equivalents.** Most basic tags (`<p>`, `<h1>`–`<h6>`, `<ul>`, `<ol>`, `<li>`, `<strong>`, `<em>`, `<code>`) pass through. Several require transformation:
   - `<a href="...">` tags pointing to Quip threads → `<ac:link>` elements (handled in the link rewriting pass)
   - `<img src="...">` tags → `<ac:image>` elements referencing uploaded attachments
   - `<pre><code>` blocks → `<ac:structured-macro ac:name="code">` with a `<ac:plain-text-body>` CDATA section
   - Quip checklists (exported as `<ul>` with custom classes) → `<ac:task-list>` and `<ac:task>` XML structures
   - `@mentions` → `<ri:user ri:account-id="..."/>` (requires Quip email → Atlassian Account ID mapping)
   - `<table>` elements from Quip spreadsheets → standard XHTML tables (watch for `colspan`/`rowspan` issues in spreadsheet grids)
4. **Serialize as valid XHTML.** Every tag must be closed, ampersands must be `&amp;`, and the `ac:` / `ri:` namespace prefixes must be correct.

**Known conversion failure modes:**
- **UTF-8 corruption in blob downloads** — Binary image data fetched without `stream=True` in Python's `requests` library can be silently decoded as text. Always open blob writes in binary mode (`"wb"`).
- **Deeply nested tables** — Confluence's storage format parser rejects `<table>` elements nested more than two levels deep. Quip spreadsheets with merged cell ranges that simulate sub-tables will fail validation. Flatten or restructure before upload.
- **Body size limits** — Confluence Cloud returns HTTP 413 for pages whose storage-format body exceeds the tenant's configured limit (typically in the hundreds of KB). Very large Quip documents — such as those containing entire exported spreadsheets — must be split across multiple pages or the data attached as a file.
- **Race conditions on version numbers** — The Confluence page-update endpoint (`PUT /wiki/api/v2/pages/{id}`) requires the current version number incremented by exactly one. If two concurrent processes update the same page (e.g., the attachment pass and the link-rewriting pass overlap), one will fail with HTTP 409 Conflict. Serialize updates per page or implement optimistic retry with version re-fetch.

```python
from bs4 import BeautifulSoup
import re

def quip_html_to_storage_format(html, attachment_map):
    """Convert Quip HTML to Confluence Storage Format XHTML."""
    soup = BeautifulSoup(html, "html.parser")

    # Strip Quip-specific data attributes
    for tag in soup.find_all(True):
        attrs_to_remove = [a for a in tag.attrs if a.startswith("data-")]
        for attr in attrs_to_remove:
            del tag[attr]

    # Convert code blocks
    for pre in soup.find_all("pre"):
        code = pre.find("code")
        code_text = code.string if code else pre.string or ""
        lang = ""
        if code and code.get("class"):
            lang_classes = [c for c in code["class"] if c.startswith("language-")]
            lang = lang_classes[0].replace("language-", "") if lang_classes else ""
        macro = soup.new_tag("ac:structured-macro", attrs={"ac:name": "code"})
        if lang:
            param = soup.new_tag("ac:parameter", attrs={"ac:name": "language"})
            param.string = lang
            macro.append(param)
        body = soup.new_tag("ac:plain-text-body")
        body.string = code_text
        macro.append(body)
        pre.replace_with(macro)

    # Convert images (after attachments are uploaded)
    for img in soup.find_all("img"):
        src = img.get("src", "")
        filename = attachment_map.get(src)
        if filename:
            ac_image = soup.new_tag("ac:image")
            ri_att = soup.new_tag("ri:attachment", attrs={"ri:filename": filename})
            ac_image.append(ri_att)
            img.replace_with(ac_image)

    return str(soup)
```

> [!NOTE]
> **Editor version matters.** Confluence Cloud pages created via the API may default to the legacy editor unless you explicitly set the `editor` content property to `v2`. After creating each page, set the property via `POST /wiki/api/v2/pages/{id}/properties` with `{"key": "editor", "value": "v2"}`. Pages in the legacy editor may not render all storage-format macros correctly.

## How Does Quip-to-Confluence Link Rewriting Work?

**Link rewriting is a two-pass process.** You cannot rewrite links during initial page creation because you do not know the Confluence page IDs (or titles, if using a title-based link model) until all pages exist.

### Pass 1: Create all pages with placeholder content

Insert every page into Confluence with its converted content using `POST /wiki/api/v2/pages`. Internal Quip links (`https://yourcompany.quip.com/{thread_id}`) remain as literal `<a href>` tags during this phase. Record the mapping:

```json
{
  "quip_thread_id": "abC1DeF2ghI3",
  "confluence_page_id": "987654321",
  "confluence_page_title": "Architecture Overview",
  "confluence_space_key": "ENG"
}
```

Quip thread URLs may use a **secret path** — a URL slug that differs from the API-returned thread ID. The secret path is visible in the browser address bar when viewing a document; it is also available in the thread metadata returned by the API under the `link` field. Your mapping table should store both identifiers (thread ID and secret path) so your regex can resolve links regardless of which format appears in the exported HTML. ([quip.com](https://quip.com/dev/automation/documentation/all))

### Pass 2: Rewrite links using the mapping table

Fetch each page's storage-format body, find all `<a>` tags whose `href` matches a Quip URL pattern, and replace them with proper Confluence `<ac:link>` elements. Then update the page via `PUT /wiki/api/v2/pages/{page_id}`, incrementing the version number.

### The Quip link pattern

Quip internal links in exported HTML follow several formats:

```
https://yourcompany.quip.com/{11-char-id}
https://yourcompany.quip.com/{11-char-id}#section-id
https://quip.com/{11-char-id}/{slug}
```

A regex to match them:

```python
QUIP_LINK_PATTERN = re.compile(
    r'https?://(?:[\w-]+\.)?quip\.com/([A-Za-z0-9]{11})(?:[/#]\S*)?'
)
```

### The Confluence link target format

Confluence Storage Format uses `<ac:link>` with `<ri:page>` resource identifiers — not standard HTML anchors:

```xml
<ac:link>
  <ri:page ri:content-title="Target Page Title" ri:space-key="MYSPACE" />
  <ac:plain-text-link-body><![CDATA[Link text]]></ac:plain-text-link-body>
</ac:link>
```

### `ri:content-title` vs. `ri:content-id`: choosing a link model

There are two ways to express internal Confluence links in storage format, and the choice has long-term consequences:

| Approach | Syntax | Survives page rename? | Human-readable in storage format? | Recommended when |
|---|---|---|---|---|
| Title-based | `ri:content-title="Page Name"` | **No** — broken link if page is renamed | Yes | Wiki is static post-migration; no planned renames |
| ID-based | `ri:content-id="987654321"` (numeric) | **Yes** | No | Wiki will be actively edited; renames are expected |

The `ri:content-title` attribute must match the **exact page title** in Confluence — a single mismatched character causes Confluence to render a "create this page" prompt instead of a working link. No error is raised at write time. If you choose the ID-based model, populate `ri:content-id` from your Pass 1 mapping table using the Confluence page ID returned when each page was created.

For most migrations, **ID-based links are the safer default**: they eliminate an entire class of silent post-migration breakage with no meaningful tradeoff beyond reduced readability in raw storage XML.

### The rewriting script

```python
def rewrite_links_in_page(confluence_client, page_id, link_map):
    """
    Fetch a Confluence page's storage body,
    replace Quip <a> hrefs with <ac:link> elements,
    and update the page.

    link_map: {quip_id: {"title": str, "space_key": str, "confluence_id": str}}
    """
    page = confluence_client.get_page_by_id(
        page_id, expand="body.storage,version"
    )
    body = page["body"]["storage"]["value"]
    soup = BeautifulSoup(body, "html.parser")
    modified = False

    for a_tag in soup.find_all("a", href=True):
        match = QUIP_LINK_PATTERN.match(a_tag["href"])
        if not match:
            continue
        quip_id = match.group(1)
        target = link_map.get(quip_id)
        if not target:
            # Link to a thread not in scope — leave as external URL
            continue

        link_text = a_tag.get_text()
        ac_link = soup.new_tag("ac:link")

        # Use ID-based linking to survive future page renames
        ri_page = soup.new_tag(
            "ri:page",
            attrs={"ri:content-id": target["confluence_id"]},
        )
        ac_link.append(ri_page)
        if link_text:
            link_body = soup.new_tag("ac:plain-text-link-body")
            link_body.string = link_text
            ac_link.append(link_body)
        a_tag.replace_with(ac_link)
        modified = True

    if modified:
        new_version = page["version"]["number"] + 1
        confluence_client.update_page(
            page_id,
            page["title"],
            str(soup),
            minor_edit=True,
            version_number=new_version,
        )
```

> [!TIP]
> **Preserve deep links via section IDs.** Quip's HTML V2 endpoint outputs section IDs on document elements. If you reuse the old Quip section ID as the new Confluence anchor name (`<ac:structured-macro ac:name="anchor">`), you get stable deep-link targets without inventing a second naming scheme. This means URLs like `https://yourcompany.quip.com/AbCdEfGhIjK#section-XYZ` can resolve to a working Confluence anchor after migration. ([quip.com](https://quip.com/dev/automation/documentation/all))

## How to Handle Attachments and Inline Images

**Quip images are stored as blobs, not URL-addressable files.** Each inline image is a binary object tied to its parent thread. To display it in Confluence:

1. Upload the blob as an attachment to the corresponding Confluence page via `POST /wiki/rest/api/content/{page_id}/child/attachment`. This uses the v1 REST API endpoint, which remains the most documented and reliable path for bulk operations on Confluence Cloud. The upload requires the `X-Atlassian-Token: nocheck` header.
2. Replace the original `<img src="...">` in the page body with an `<ac:image>` element referencing the attachment by filename.

```xml
<ac:image>
  <ri:attachment ri:filename="architecture_diagram.png" />
</ac:image>
```

**Watch for filename collisions.** If two Quip documents each contain an image named `image.png`, uploading both to the same Confluence page creates a versioning conflict. Prefix blob filenames with the Quip thread ID or a hash to guarantee uniqueness.

**Download blobs in binary mode.** When fetching blob content with Python's `requests` library, use `response.content` (bytes), not `response.text` (decoded string), and write with `open(path, "wb")`. Decoding binary image data as text silently corrupts the file.

## What Breaks During a Quip to Confluence Migration

### Embedded spreadsheets

Quip documents can contain inline spreadsheet grids with formulas, conditional formatting, and cross-sheet references. Confluence has no native spreadsheet engine. The Quip API exports spreadsheets as static HTML `<table>` elements — all formulas, filters, and conditional formatting are permanently lost. For basic data tables, this translates cleanly to Confluence. For critical spreadsheets with complex formulas, export as XLSX and attach the file to the Confluence page alongside the static table.

### Comments and conversation threads

Quip comments are anchored to specific **section IDs** within a document — individual paragraphs, list items, or table cells. Confluence inline comments also use positional anchoring, but the anchor model is incompatible. In practice, Quip comments migrate as **page-level footer comments**, losing their positional context. Extract comments via `GET /1/messages/{thread_id}` (paginated at 25 messages per call) and either append them as a formatted block at the bottom of the Confluence page or import them as page comments via the Confluence REST API.

HTML exports do not include comments — you need to pull them from the messages endpoint or use a DOCX/XLSX sidecar export. ([quip.com](https://quip.com/dev/automation/documentation/all))

### Salesforce Data Mentions and Live Apps

Quip **Data Mentions** are embedded references to Salesforce records — contacts, opportunities, accounts — that render as live, permission-gated field values inside Quip documents. They have no Confluence equivalent. Resolve Data Mentions to static text values **before export** — this requires write access to the Quip API, meaning it must happen during the active subscription period, not during the 90-day read-only phase.

**Live Apps** — Quip's embedded interactive widgets (polls, project trackers, countdown timers, third-party integrations) — export as empty or placeholder HTML. Archive them as HTML snapshots where the rendered state matters.

### Multi-folder membership

A single Quip thread can appear in multiple folders. A Confluence page has exactly one parent. During migration, choose a **canonical parent** for each page. A common strategy: use the first folder in the thread's `shared_folder_ids` array as the canonical location and add cross-reference links from the other locations.

### Storage format validation failures

Confluence's API accepts storage-format XML but does not always return a useful error message when the body is invalid. Common silent failures: unclosed tags (BeautifulSoup's HTML serializer sometimes omits closing tags for void elements), invalid namespace prefixes, and `<ac:parameter>` elements placed after `<ac:plain-text-body>` in a macro. Validate generated storage-format XML against the Confluence schema before upload, or run a post-import rendering check by fetching the page body back from the API and comparing it to what was sent.

## Confluence API Constraints That Shape Your Script

Confluence Cloud uses a **points-based rate-limit model** (enforced since March 2, 2026) for Forge, Connect, and OAuth 2.0 apps. **API token (basic auth) traffic is not subject to the points-based quotas** and remains under existing burst rate limits, which makes API tokens the preferred authentication method for migration scripts. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/rate-limiting/))

| Constraint | Value | Notes |
|---|---|---|
| Rate limit response | HTTP 429 with `Retry-After` header | Standard behavior (unlike Quip's 503) |
| Page body size | Varies by tenant config | Large bodies return HTTP 413; split oversized docs |
| Attachment upload endpoint | `POST /rest/api/content/{id}/child/attachment` | Requires `X-Atlassian-Token: nocheck` header |
| Page version increment | Current version + 1 (exact) | HTTP 409 if wrong; re-fetch version before retry |
| Concurrent update conflict | HTTP 409 Conflict | Serialize updates per page or implement version re-fetch retry |

Confluence's page-update endpoint (`PUT /wiki/api/v2/pages/{id}`) requires the current version number incremented by one. If you skip or repeat a version number, the API rejects the update with HTTP 409. Your script must track page versions for every page it touches, and re-fetch the current version before any retry after a conflict.

## Step-by-Step Migration Workflow

1. **Inventory your Quip workspace.** Enumerate all folders and threads via the Automation API. Record thread IDs, types (document vs. spreadsheet), folder membership, and content size. This determines total scope and extraction time.

2. **Resolve Salesforce Data Mentions.** While you still have write access, convert Data Mentions to static text. This is irreversible and must happen before the read-only phase.

3. **Extract all threads.** Fetch HTML bodies, download all blobs in binary mode, and pull comment threads separately via `GET /1/messages/{thread_id}`. Implement 503-aware backoff and checkpoint progress to SQLite or a local file so long runs can resume after failures.

4. **Build the folder → space/page-tree mapping.** Decide which Quip folders become Confluence spaces and which become parent pages. Resolve multi-folder membership conflicts by choosing a canonical parent for each thread.

5. **Transform HTML to Confluence Storage Format.** Parse each document, strip Quip-specific markup, convert code blocks to macros, convert checklists to task lists, and prepare image references. Validate output XML before proceeding.

6. **Create all Confluence pages (Pass 1).** Upload each page with converted content. Leave Quip internal links as `<a href>` placeholders. Record the mapping of Quip thread IDs to Confluence page IDs, titles, and space keys.

7. **Upload attachments.** For each page, upload extracted blobs as Confluence attachments. Use blob filenames prefixed with thread ID to avoid collisions. Update page bodies to replace `<img>` tags with `<ac:image>` references.

8. **Rewrite internal links (Pass 2).** Iterate through every page, find Quip URL patterns in `<a>` tags, and replace them with `<ac:link>` elements using the mapping table. Use `ri:content-id` (numeric page ID) rather than `ri:content-title` to survive future renames. Update each page via the Confluence API.

9. **Validate.** Run the following checks:
   - **Record count parity**: Quip thread count (from inventory) vs. Confluence page count in the target space(s)
   - **Attachment count per page**: Compare extracted blob count to Confluence attachment count per page ID
   - **Regex scan for remaining Quip URLs**: Any `quip.com` substring remaining in page bodies is a missed link
   - **Storage format round-trip check**: Fetch each page body back from the Confluence API and verify it matches what was sent (catches silent truncation or validation rejection)
   - **Rendering spot-check**: Verify a sample of pages in both the legacy editor and v2 editor to confirm macros render correctly

> [!TIP]
> **Post-cutover redirect:** Quip documents a **Bulk Mark Threads as Moved to an Off-Quip Location** endpoint that can point old threads to new external URLs, but this endpoint is available only to select customers via Quip support. If you expect years of stale bookmarks and Slack links pointing to Quip, request access before your subscription ends. ([quip.com](https://quip.com/dev/automation/documentation/all))

## Migration Scope and Effort Estimation

The engineering effort for a Quip-to-Confluence migration scales non-linearly with workspace complexity. The following factors dominate:

| Factor | Low complexity | High complexity |
|---|---|---|
| Document count | < 500 | > 5,000 |
| Spreadsheet threads | None | Widespread, with formulas |
| Internal cross-links | Few or none | Dense (> 5 links/doc average) |
| Inline images per doc | 0–1 | 5+ |
| Salesforce Data Mentions | None | Widespread |
| Multi-folder membership | Rare | Common |
| Comment preservation required | No | Yes |

**At the 50 req/min single-token Quip API limit**, a workspace of 2,000 documents with 3 images each requires approximately 8,000 API calls for thread metadata and blobs alone — roughly 2.5–3 hours of extraction time, not counting the Confluence upload pass. Multi-token parallelization (up to 4 tokens at ~200 req/min combined) reduces this proportionally.

The two-pass link rewriting strategy adds a full second traversal of every page in the workspace. For 5,000 pages, this means 5,000 GET requests and up to 5,000 PUT requests on the Confluence side, subject to Confluence's own rate limits.

Organizations consistently underestimate conversion edge cases: storage format validation failures, UTF-8 corruption in blob downloads, version number conflicts from concurrent updates, and deeply nested table structures that Confluence rejects. Budget at least 20–30% of estimated scripting time for debugging these failure modes before treating the pipeline as production-ready.

**Related reading:**
- [How to Export Data from Quip: Methods, API Limits & Migration Prep](https://clonepartner.com/blog/blog/how-to-export-data-from-quip-methods-api-limits-migration-prep)

## Frequently asked questions

### Is there a native import tool for migrating Quip to Confluence?

No. Confluence has no Quip importer, and Quip retired its own import feature in January 2024. The entire migration requires custom scripts using the Quip Automation API for extraction and the Confluence REST API for page creation. A Marketplace app (Swiftix) exists but does not handle the full pipeline for complex workspaces.

### How do you rewrite Quip internal links for Confluence?

Use a two-pass approach. In Pass 1, create all Confluence pages and build a mapping table of Quip thread IDs to Confluence page titles and space keys. In Pass 2, iterate through every page's storage-format body, find Quip URL patterns, and replace them with Confluence <ac:link> elements using <ri:page> resource identifiers.

### What are the Quip API rate limits for bulk export?

The Quip Automation API allows 50 requests per minute per user and 750 per hour, with a company-wide cap of 600 requests per minute. The Bulk Export API supports 36,000 documents per hour. Quip returns HTTP 503 (not 429) when rate-limited, which breaks standard retry logic.

### What data is lost when migrating from Quip to Confluence?

Spreadsheet formulas, conditional formatting, Salesforce Data Mentions, Live Apps, comment positional anchoring, and multi-folder membership are lost or degraded. Data Mentions must be resolved to static text before export while write access is still available.

### Can I preserve Quip comments when moving to Confluence?

Not through HTML export alone. Quip includes conversation history only in DOCX and XLSX bulk exports, not HTML. For HTML-based migrations, pull comments separately via the messages API endpoint and import them as page-level Confluence comments. Section-level anchoring is lost either way.
