Quip Bulk Export at Scale: Rate Limits, Checkpointing & Runbook
A technical runbook for exporting Quip at scale: Bulk Export API rate headers, Automation API 503 handling, folder deduplication, checkpointing, and completeness verification.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Exporting a Quip workspace at volume is a multi-day, multi-API engineering project. There is no single endpoint that produces a complete archive. You need three distinct export surfaces — the per-document UI, the Bulk Export API, and the Automation API — each with different rate ceilings, format options, and metadata coverage. This guide is the operational companion to our Quip export overview. Where that post maps the landscape, this one covers the mechanics: how to build the pipeline, stay inside the rate limits, checkpoint a run that spans days, and prove completeness when it finishes.
Quip End-of-Life (March 2027): Salesforce has announced that all Quip products are being retired and subscriptions cannot be renewed after March 1, 2027. After expiration, the site enters a 90-day read-only phase, then blocks logins, then deletes data. API rate limits mean large workspaces don't come out quickly — plan accordingly. See our Quip retirement decision guide for destination planning.
The three Quip export surfaces and when each applies
Quip exposes three ways to get content out. They are not interchangeable — each covers different data and has different constraints.
| Surface | What it exports | Rate ceiling | Plan requirement | Best for |
|---|---|---|---|---|
| Per-document UI | Single doc → PDF, DOCX, XLSX, HTML, Markdown | Manual (no API limit) | Any plan | Spot-checking, one-off grabs |
| Bulk Export API | Batches of docs → DOCX, XLSX, HTML, PDF | 36,000 docs/hour per company | Quip Plus or Advanced | Compliance archives, full-workspace file dumps |
| Automation API | Threads, folders, members, messages, blobs (JSON + HTML) | 50 req/min per user; 600 req/min per company | Any plan (Admin API requires Plus/Advanced) | Migration-ready extraction, comment retrieval, metadata assembly |
Per-document UI export is the Download menu in the Quip web app. It produces one file at a time. Comments are not included. Salesforce Data Mentions may render blank. It exists for quick grabs and visual QA — not migration work.
Bulk Export API is an asynchronous endpoint. POST /1/threads/export/async (or the Admin variant POST /1/admin/threads/export/async for org-wide scope) accepts a list of thread IDs with per-document format choices. You submit the batch, receive a request_id, and poll until each document's status flips to EXPORTED with a download URL. Both variants share the same 36,000-document hourly ceiling. Both require Quip Plus or Quip Advanced — the Admin variant specifically requires that the calling user is listed in the Admin API Users section of the Quip Admin Console. (quip.com)
Automation API is the general-purpose REST surface. It gives you thread metadata, folder trees, HTML bodies, messages, blobs, and member lists — the raw material for a structured migration. It does not produce rendered files (DOCX, PDF) directly, but you can call single-thread export endpoints (GET /1/threads/{id}/export/docx, GET /1/threads/{id}/export/xlsx) for individual documents. This is the only surface that exposes comments, folder membership, and blob content.
Check entitlements before you design the run. Quip's current documentation is not perfectly consistent on API licensing. The Automation docs list Quip Plus, Quip Advanced, pre-existing Quip for Customer 360, and certain Lightning editions as eligible products. The Admin docs list slightly different SKUs and add two operational requirements: the caller must be an admin, and Admin API access must be enabled for the Quip instance. Verify your SKU and Admin API enablement before assuming you can bulk export. (quip.com)
How does the Bulk Export API rate limit work?
The Bulk Export API enforces a company-wide cap of 36,000 documents exported per hour — separate from the per-user and per-company request limits on the Automation API. Every response from this endpoint includes four custom headers:
X-Documentbulkexport-RateLimit-Limit— total documents per hour your company can exportX-Documentbulkexport-RateLimit-Remaining— documents left in the current windowX-Documentbulkexport-RateLimit-Reset— UTC timestamp when the window resetsX-Documentbulkexport-Retry-After— seconds until your company can make calls again
The enforcement is strict: if Remaining is 1,000 and your next batch contains 1,001 documents, the entire request fails — and subsequent requests fail too until Reset. Your batching code must read Remaining before each submission and size the batch accordingly.
Recommended starting batch size: 500 thread IDs per submission. This leaves headroom within the 36,000-document ceiling while keeping individual request payloads manageable. If Remaining drops below 500 before your next submission, drain the remaining quota with a smaller batch and pause until Reset.
import time, requests
SAFE_BATCH_SIZE = 500
def submit_bulk_batch(session, thread_batch, base_url, headers):
resp = session.post(
f"{base_url}/1/threads/export/async",
json={"threads": thread_batch, "include_conversations": True},
headers=headers
)
remaining = int(resp.headers.get("X-Documentbulkexport-RateLimit-Remaining", 0))
retry_after = int(resp.headers.get("X-Documentbulkexport-Retry-After", 0))
if resp.status_code == 429 or resp.status_code == 503:
time.sleep(max(retry_after, 60))
return None, remaining
return resp.json().get("request_id"), remaining
def batch_with_quota_check(session, all_threads, base_url, headers):
i = 0
while i < len(all_threads):
_, remaining = submit_bulk_batch(session, [], base_url, headers) # probe headers
batch_size = min(SAFE_BATCH_SIZE, remaining, len(all_threads) - i)
if batch_size == 0:
time.sleep(60)
continue
request_id, remaining = submit_bulk_batch(
session, all_threads[i:i+batch_size], base_url, headers
)
if request_id:
i += batch_sizeTwo production details worth noting:
The include_conversations parameter appends conversation history to DOCX and XLSX exports only — it does not work with HTML. If your goal is compliance archiving, DOCX with conversations enabled is the most complete rendered format. But this is appended history, not inline anchored comments — useful for audit trails, not for reconstructing comment positions in a target system.
The omit_if_unchanged_since_usec parameter is the native resume lever for long-running or restarted exports. Pass a microsecond timestamp, and the API skips threads that haven't changed since that time. Use it on reruns to avoid re-exporting your entire workspace. (quip.com)
PDF is a separate flow. PDF export uses its own async endpoint (POST /1/admin/threads/export/pdf/async) and counts against the same 36,000-document hourly ceiling. PDF export can take up to 10 minutes per large document, is capped at 40,000 spreadsheet cells, and excludes charts entirely.
Automation API rate limits: why 503 matters more than 429
The Automation API rate limits operate on three tiers simultaneously:
| Tier | Default limit | Header prefix |
|---|---|---|
| Per-user (per token) | 50 requests/minute, 750 requests/hour | X-Ratelimit-* |
| Per-company | 600 requests/minute | X-Company-RateLimit-* |
| Bulk export (docs) | 36,000 documents/hour | X-Documentbulkexport-RateLimit-* |
The per-user and per-company limits are enforced independently. Running three tokens in parallel gives you up to 150 requests/minute in theory — and the company ceiling of 600 requests/minute means you can safely run up to 12 tokens (600 ÷ 50) before hitting it. In practice, 3 tokens is the recommended ceiling for production pipelines: it triples throughput without risking company-wide lockout if one worker misfires. Every API call from every integration, Flow, and Process Builder action at your company counts against the company pool, so real headroom is lower than 600/minute on active instances.
Quip signals rate-limit throttling with HTTP 503, not the standard HTTP 429. This is documented behavior (quip.com), but it breaks nearly every standard HTTP retry library out of the box. Most libraries classify 503 as a transient server error and retry aggressively with short backoffs — exactly the wrong behavior, because it burns through your remaining quota and can trigger longer lockout windows.
Your retry logic must explicitly intercept 503 responses and treat them as rate-limit signals. Read the X-Ratelimit-Reset or X-Company-RateLimit-Reset headers if present. Fall back to exponential backoff with jitter if they are absent. Do not retry for at least 60 seconds after the first 503.
def handle_quip_response(resp):
if resp.status_code == 503:
# Rate limited — NOT a server error
reset = resp.headers.get("X-Ratelimit-Reset")
if reset:
wait = max(int(reset) - time.time(), 1)
else:
wait = 60 # conservative fallback
time.sleep(wait)
return "rate_limited"
resp.raise_for_status()
return "ok"Complete Quip API error code reference
Most guides only discuss 503. Here is the full set of HTTP status codes your pipeline will encounter and how to handle each:
| Status code | Quip meaning | Correct action |
|---|---|---|
| 200 | Success | Process response normally |
| 400 | Bad request — malformed JSON, invalid thread IDs in batch, XLSX format requested on a non-spreadsheet thread | Log and skip; do not retry |
| 401 | Invalid or expired access token | Refresh token (see token expiry section below); halt if refresh fails |
| 403 | Valid token, insufficient permissions — thread exists but caller lacks access | Log as access-restricted; do not retry |
| 404 | Thread not found — commonly a soft-deleted thread that still appears in Admin list calls | Log as soft-deleted; do not retry |
| 429 | Returned by some older Quip endpoints; treat identically to 503 | Respect Retry-After header; exponential backoff |
| 500 | Genuine server error | Retry with exponential backoff; max 3 attempts |
| 503 | Rate limit exceeded (primary throttle signal) | Read X-Ratelimit-Reset; wait; do not retry aggressively |
A 400 on a bulk export batch typically means one or more thread IDs in the list are invalid or the format is mismatched (e.g., XLSX on a document thread). Bisect the batch to isolate the offending IDs rather than discarding the whole batch.
Company-wide ceilings are shared. The 600 requests per minute company-wide limit means you cannot distribute your export across 20 different user tokens to bypass per-user limits. If your distributed workers collectively exceed 600 requests in a minute, all tokens will begin receiving 503 errors.
Quip support can raise your rate limits. File a support request before attempting bulk operations — approvals can take several business days to process, and the increase is not guaranteed for all plan tiers.
Token expiry in long-running pipelines
Access tokens issued by Quip do not expire on a fixed schedule by default, but pipelines spanning multiple days can encounter token invalidation from several causes: password resets, admin revocation, session timeout on OAuth flows, and Salesforce SSO re-authentication requirements. A pipeline that doesn't handle token expiry will silently fail with 401 errors mid-run and leave partial exports without obvious attribution.
Mitigation steps:
- Use a dedicated service account token with no SSO dependency. Tokens generated from the Quip developer settings page for a non-SSO admin account are the most stable for long-running jobs.
- Detect 401 explicitly and halt with a clear error — do not retry a 401 blindly, as repeated failed auth attempts can trigger account lockouts.
- Test token validity at startup before the run begins. Call
GET /1/users/currentand verify a 200 response before queuing any work. - Log token-related failures separately from rate-limit failures so you can distinguish auth problems from quota problems during post-run reconciliation.
def validate_token(session, base_url):
resp = session.get(f"{base_url}/1/users/current")
if resp.status_code == 401:
raise RuntimeError("Token invalid or expired. Regenerate before resuming.")
resp.raise_for_status()
return resp.json()What each export format costs you in fidelity
No single format captures everything. Each one drops something.
| Format | Comments | Salesforce Data Mentions | Live Apps | Internal Links | Images | Section IDs |
|---|---|---|---|---|---|---|
HTML (Automation API html field) |
❌ | ❌ (may render blank) | Static data-live-app-payload JSON |
⚠️ Quip-internal URLs | ✅ As blob references | ✅ Preserved in id attributes |
| DOCX (bulk or single-thread) | ⚠️ Via include_conversations only |
⚠️ May render blank | ❌ Omitted | ❌ Broken | ✅ Embedded | ❌ Lost |
| XLSX (spreadsheet threads only) | ⚠️ Via include_conversations only |
⚠️ May render blank | ❌ | ❌ | N/A | ❌ Lost |
| PDF (async export) | ✅ If conversation included | ⚠️ May truncate | ⚠️ Distorted | ❌ Static text | ✅ Rasterized | ❌ Lost |
For migration work, HTML is the best intermediate format. It preserves section IDs (needed for comment anchoring), carries blob references you can resolve, and is parseable for transformation into target formats.
XLSX is a spreadsheet export specifically — Quip's Admin docs note that it works for Quip spreadsheets or documents with embedded spreadsheets, and rejects document threads that don't contain spreadsheet content. If you submit a document thread for XLSX export, the API returns a 400 error for that thread ID. Filter by thread type before constructing export batches — GET /1/admin/threads/list returns a type field (document, spreadsheet, slides) for each thread. (quip.com)
Which metadata no export format carries
Some metadata is not available in any rendered export format. You must reconstruct it from separate API calls or accept its loss:
- Version/edit history — Quip tracks per-section edit history internally, but no API endpoint exposes full version snapshots. The
message_type=editparameter on Get Recent Messages returns edit summaries, not restorable document states. (quip.com) - Per-section authorship — Who wrote or last edited a specific paragraph is visible in the UI but not in any export payload.
- Read receipts — Not exposed via any API.
- Document-level permissions (ACLs) — Member lists and access levels require
GET /2/threads/{id}/memberscalls.Get Thread Members V2returns only direct thread members — it explicitly excludes access granted through folder-sharing, link-sharing, Salesforce synced sharing, and email invitation. (quip.com) - Folder placement — Folder membership, child folders, and shortcuts via
linked_from_folder_idslive in folder and thread-folder endpoints, not in exported files. - Notification and sharing settings — Thread link-share settings, comment-allow flags, and edit-lock status are API-only metadata.
- Live App interactive state — Kanban boards, project trackers, and calendars export as static JSON payloads or flat content. The interactive behavior does not survive.
- Salesforce record data — Data Mentions reference live Salesforce records. If the exporting user lacks Salesforce permissions, these render blank. No export format resolves them to static values by default.
- Granular section permissions — Quip allows locking specific paragraphs or spreadsheet ranges. This permission metadata is not exposed via the API.
If any of these matter for your target system or compliance requirements, plan separate extraction passes before you build the pipeline.
How to crawl the folder tree without duplicate fetches
Quip folders are not a filesystem — they function as labels. A single thread can appear in multiple folders simultaneously. If you naively recurse the folder tree and export documents as you find them, you will fetch the same thread multiple times, wasting rate-limit budget and creating duplicates.
The correct approach separates discovery from extraction:
Step 1 — Build the folder graph. Start from the company's root folder (or all shared folders visible to your admin token). Call GET /1/folders/{id} recursively. Each response includes a children array containing thread_id and folder_id entries. Queue child folders for traversal; collect thread IDs into a set.
Step 2 — Filter by thread type. As you collect thread IDs, also record each thread's type from the Admin thread list. Tag each ID as document, spreadsheet, or slides so you can route it to the correct export format without a separate lookup.
Step 3 — Deduplicate threads. Because a thread can appear under multiple folders, your discovery set will contain duplicates. Maintain a seen_thread_ids set and skip any thread already in it.
Step 4 — Record the folder-to-thread mapping. For every thread, store the list of parent folder IDs. This is your canonical folder membership data — you'll need it to reconstruct hierarchy in the target system.
import collections
from collections import deque
seen_threads = set()
seen_folders = set()
folder_graph = {} # folder_id → [child_folder_ids]
thread_folders = collections.defaultdict(list) # thread_id → [folder_ids]
thread_types = {} # thread_id → "document" | "spreadsheet" | "slides"
queue = deque([root_folder_id])
while queue:
folder_id = queue.popleft()
if folder_id in seen_folders:
continue
seen_folders.add(folder_id)
data = quip_get(f"/1/folders/{folder_id}")
children = data.get("children", [])
child_folders = []
for child in children:
if "folder_id" in child:
child_folders.append(child["folder_id"])
queue.append(child["folder_id"])
elif "thread_id" in child:
tid = child["thread_id"]
thread_folders[tid].append(folder_id)
seen_threads.add(tid)
folder_graph[folder_id] = child_folders
# After crawl: hydrate types from Admin thread list
# and partition into document_ids, spreadsheet_ids, slides_ids before exportThis approach makes exactly one folder call per folder and zero redundant thread-content calls. For a workspace with 5,000 threads across 200 folders, the discovery phase costs roughly 200 API calls — well within a single hour's budget at 50 requests/minute.
After the crawl, hydrate metadata in batches. The Automation API's Get Threads V2 endpoint accepts up to 100 thread IDs per call; the Admin variant accepts up to 1,000. Export content only after you have the deduplicated set, partitioned by type. (quip.com)
If the folder crawl returns child folders marked restricted: true, your result is partial for that token. For a company-wide export, the correct denominator is the Admin List Threads inventory, not a single user's folder tree. (quip.com)
How message pagination works and when it terminates
Comment and message extraction uses cursor-based pagination. The GET /1/threads/{id}/messages endpoint returns a page of messages and, if more exist, a cursor value in the response body. Pass that cursor as a query parameter on the next call to get the next page. Pagination terminates when the response either omits the cursor field entirely or returns an empty messages array.
The checkpoint record stores messages_cursor so a resumed run can continue from the last successful page rather than re-fetching from the beginning.
def fetch_all_messages(session, base_url, thread_id, starting_cursor=None):
messages = []
cursor = starting_cursor
while True:
params = {"count": 100}
if cursor:
params["cursor"] = cursor
resp = session.get(
f"{base_url}/1/threads/{thread_id}/messages",
params=params
)
handle_quip_response(resp)
data = resp.json()
batch = data.get("messages", [])
messages.extend(batch)
cursor = data.get("cursor")
if not cursor or not batch:
break # pagination exhausted
return messages, cursor # return final cursor for checkpointKey operational detail: each message page request counts against your 50 requests/minute per-user limit. A thread with 500 messages at 100 messages per page costs 5 API calls. For a workspace where average threads have 20 messages, message pagination adds roughly 0.2 calls per thread — low overhead. For threads with heavy comment activity (200+ messages), budget accordingly and record per-thread message counts during the metadata phase so you can prioritize high-volume threads.
Blob download rate limits and pipeline design
Blob downloads use GET /1/blob/{thread_id}/{blob_id}. Each blob request counts against the same 50 requests/minute per-user ceiling as all other Automation API calls — there is no separate blob rate tier. Blob downloads are not counted against the 36,000-document bulk export ceiling.
This means blob extraction competes directly with metadata and comment fetching for your per-user quota. In a pipeline running all operations concurrently on a single token, blob downloads can starve higher-priority operations. Recommended sequencing:
- Phase 1 — Folder crawl and thread ID discovery (low call volume)
- Phase 2 — Thread metadata hydration in batches of 100–1,000 (Admin API)
- Phase 3 — HTML body and comment extraction (parallel, rate-limited)
- Phase 4 — Blob downloads (lowest priority; run after content extraction completes)
Parse HTML bodies during Phase 3 to extract all blob_id references. Store them in the checkpoint. Run blob downloads in Phase 4 as a separate pass so a blob failure doesn't block document content.
import re
def extract_blob_ids_from_html(html_content):
# Quip blob references appear as src="/blob/{thread_id}/{blob_id}"
pattern = r'/blob/([A-Za-z0-9]+)/([A-Za-z0-9]+)'
return re.findall(pattern, html_content)Checkpointing a multi-day export
A workspace with 10,000+ documents will not export in one sitting. At 50 requests per minute per token, just fetching thread metadata takes over 3 hours. Adding HTML body retrieval, blob downloads, and comment pagination roughly triples that. You need durable checkpointing.
The checkpoint tracks four categories:
- Discovery state — which folders have been crawled, which thread IDs collected.
- Export state — for each thread ID, what has been fetched (metadata? HTML body? blobs? comments?) and at what timestamp.
- Bulk export batches — which
request_ids have been submitted and which are still pending poll. - Token health — last successful API call timestamp, so you can detect silent token failures on resume.
Store this as a JSON file or SQLite database. On crash or rate-limit pause, the script reads the checkpoint and resumes from the last incomplete thread.
A practical per-thread checkpoint record:
{
"thread_id": "AVN9AAeqq5w",
"thread_type": "document",
"updated_usec": 1558224928731511,
"formats": ["HTML", "DOCX"],
"folders": ["RKa9OAiGmsC"],
"bulk_request_id": "AMXAEA8lSEj:1644470492776",
"bulk_status": "EXPORTED",
"messages_cursor": 1632348632519081,
"messages_complete": false,
"blob_ids_remaining": ["DiPp1ZQyC8QUtvBT4vojzM"]
}For SQLite, create an export_state table:
CREATE TABLE export_state (
thread_id TEXT PRIMARY KEY,
thread_type TEXT, -- "document", "spreadsheet", "slides"
status TEXT DEFAULT 'PENDING', -- PENDING, DOWNLOADED, FAILED
html_fetched INTEGER DEFAULT 0,
blobs_fetched INTEGER DEFAULT 0,
comments_fetched INTEGER DEFAULT 0,
messages_cursor TEXT,
messages_complete INTEGER DEFAULT 0,
last_attempted_at INTEGER,
error_code INTEGER, -- HTTP status on failure
error_log TEXT
);When the script starts, call validate_token() first, then query SELECT thread_id FROM export_state WHERE status = 'PENDING'. As each thread completes, update its status. If a thread fails after retries, mark it as FAILED with the HTTP error code and message. Restarting the script resumes from exactly where it stopped.
Three edge cases to handle:
- Documents modified during the export. If your run spans multiple days, documents may change between your inventory snapshot and the content fetch. Record
updated_usecfrom the inventory and compare it to the thread'supdated_usecat fetch time. Flag discrepancies for a second pass. - Soft-deleted threads. The Admin API's thread listing can return thread IDs that have been soft-deleted. These return metadata in listing calls but 404 on export. Log them separately (with HTTP 404) instead of failing the run.
- Type mismatches in export batches. A document thread submitted for XLSX export returns 400. The
thread_typecolumn in your checkpoint table prevents this by letting you route each thread to the correct format before submission.
For resumed bulk exports, use omit_if_unchanged_since_usec to skip threads that haven't been modified since your last successful checkpoint. This keeps delta runs small. (quip.com)
Verifying completeness against a pre-run inventory
Before starting an export, take a baseline inventory. Use the Admin API's GET /1/admin/threads/list to collect every thread ID, type, title, and updated_usec timestamp. Export this to a CSV — it becomes your migration manifest and the denominator you verify against when the run ends.
After the export completes, reconcile:
| Check | Method | Expected outcome |
|---|---|---|
| Thread count | Compare exported count vs inventory count | Match within soft-delete margin |
| Missing threads | Set difference: inventory_ids - exported_ids |
Only deleted or access-restricted threads |
| Format verification | Check file size > 0 for every exported file | Zero-byte files indicate silent failures |
| Comment coverage | For threads with message_count > 0, verify comment data exists |
Comment extraction is a separate pipeline |
| Blob coverage | Parse HTML for blob_id references, verify each was downloaded |
Missing blobs = broken images in target |
| Freshness | Compare export-time updated_usec to inventory updated_usec |
Flag threads modified after inventory snapshot |
| Type routing | Verify no XLSX exports contain 400-error logs | Type mismatch indicates pre-filter gap |
import os
inventory = load_inventory_csv("quip_inventory.csv")
exported = set(state["exported_threads"].keys())
missing = set(inventory.keys()) - exported
for tid in missing:
print(f"MISSING: {tid} — {inventory[tid]['title']} ({inventory[tid]['type']})")
for tid in exported:
fpath = f"exports/{tid}.html"
if not os.path.exists(fpath) or os.path.getsize(fpath) == 0:
print(f"ZERO-BYTE: {tid}")Run a delta pass after the main export. On a multi-day run, content will change underneath you unless the workspace is frozen. Rerun the inventory, diff against your original snapshot, and re-export anything whose timestamp moved. omit_if_unchanged_since_usec keeps the delta cost low.
Run reconciliation before you tear down your Quip access. Once the subscription expires and the 90-day read-only window closes, you cannot go back for anything you missed.
How long does a Quip bulk export actually take?
The Bulk Export API alone can push 36,000 rendered documents per hour, but rendered files (DOCX/PDF) don't include comments or blob references — they're an archival snapshot, not a migration-ready dataset.
Here is the theoretical minimum time for a 10,000-document workspace using the Automation API at default limits (50 req/min, single token). These are derived from API arithmetic, not empirical benchmarks — real-world runs will be longer due to 503 recovery, blob size variance, and comment volume.
| Operation | Calls per thread | Total calls | Time at 50 req/min (theoretical floor) |
|---|---|---|---|
| Thread metadata | 1 | 10,000 | ~3.3 hours |
| HTML body | 1 | 10,000 | ~3.3 hours |
| Comments (avg 2 pages) | 2 | 20,000 | ~6.7 hours |
| Blobs (avg 1.5/doc) | 1.5 | 15,000 | ~5 hours |
| Total | 55,000 | ~18 hours |
Parallelizing across 3 user tokens cuts the theoretical floor to roughly 6 hours. The 600 requests/minute company ceiling allows up to 12 tokens before hitting it (600 ÷ 50), but 3 tokens is the practical recommendation — it triples throughput without creating fragile coordination overhead or risking company-wide lockout from a worker misfire. Budget 2–4 days for a workspace of this size when you include retries, 503 recovery, delta verification passes, and real-world rate variance.
When to use each surface: a decision tree
- Under 50 documents, no compliance requirements → Per-document UI export. Manual but fast enough.
- Compliance archive, format is DOCX/PDF, no re-import needed → Bulk Export API with
include_conversations: true. Fastest path to rendered files. - Migration to another platform (Notion, Confluence, SharePoint, Coda, or Slack Canvases) → Automation API for HTML extraction + separate comment/blob/folder passes. The only path that gives you parseable, re-importable content.
- Mixed: archive + migrate → Run the Bulk Export API for your archival copy (DOCX/PDF), then the Automation API pipeline for your migration-ready dataset. They share the company-wide rate pool, so stagger them.
- Spreadsheet-heavy workspace → Partition threads by type before batching. Route spreadsheet threads to XLSX; document threads to HTML or DOCX. Submitting document threads for XLSX export returns 400 errors that waste quota and require bisection to isolate.
Putting it together
A production-grade Quip export at scale is an extraction pipeline with discovery, type partitioning, deduplication, token validation, checkpointing, multi-format export, comment pagination with cursor tracking, blob resolution, and post-run reconciliation. The API constraints — 50 requests per minute per token, 600 per minute per company, 503 instead of 429, and a 36,000-document hourly ceiling on bulk exports — are the fixed parameters your design must work within.
The predictable failure modes, roughly in order of frequency:
- 503 mishandled as a server error — triggers aggressive retries that burn quota and extend lockout windows
- No thread type filtering — XLSX submissions on document threads generate 400 errors that require batch bisection
- Comment extraction underestimated — doubles or triples total call volume; must be budgeted separately
- Token expiry mid-run — silent 401 failures that look like access errors without explicit detection
- Blob verification skipped — zero-byte or missing blobs discovered only after Quip access expires
- No checkpointing — multi-day runs that restart from zero after any failure
All solvable with the patterns in this guide — but the margin for error shrinks as your Quip subscription expiration approaches.
For the broader picture on Quip's export capabilities, start with our complete Quip export guide. If you're evaluating destinations, our Quip retirement playbook covers the platform trade-offs.
Frequently Asked Questions
- What is the Quip Bulk Export API rate limit?
- The Quip Bulk Export API enforces a company-wide cap of 36,000 documents exported per hour. The API returns X-Documentbulkexport-RateLimit-Remaining and X-Documentbulkexport-Retry-After headers so your code can pace submissions. If your next batch exceeds the remaining quota, the entire request fails until the window resets.
- Why does Quip return 503 instead of 429 for rate limiting?
- Quip uses HTTP 503 to signal rate-limit throttling rather than the standard 429. This is documented behavior, but it breaks most standard HTTP retry libraries, which treat 503 as a transient server error and retry aggressively. Your scripts must explicitly intercept 503 as a rate-limit signal and implement exponential backoff with at least a 60-second initial delay.
- Does the Quip Bulk Export API require Quip Plus or Advanced?
- Yes. Both the Bulk Export API and the Admin API require Quip Plus, Quip Advanced, or certain other qualifying SKUs. The calling user must also be listed in the Admin API Users section of the Quip Admin Console. The standard Automation API is available on any Quip plan with API access.
- How long does it take to export 10,000 documents from Quip?
- At default Automation API limits of 50 requests per minute per token, exporting 10,000 documents with HTML bodies, comments, and blobs requires roughly 55,000 API calls — about 18 hours with a single token. Parallelizing across 3 tokens and using the Bulk Export API for rendered files can compress this, but budget 2–4 days including retries and verification.
- What metadata is lost when exporting from Quip?
- No export format captures version/edit history, per-section authorship, read receipts, document-level permissions (ACLs), notification settings, granular section permissions, or interactive Live App state. Salesforce Data Mentions may render blank depending on the exporting user's permissions. Comments require separate API extraction and are not included in rendered file exports by default.