How to Export Data from Unthread: Methods, API Limits & Formats
Learn every way to export data from Unthread — API endpoints, pagination code, attachment downloads, known gaps, and what can't be exported.
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
How to Export Data from Unthread: Methods, API Limits & Formats
Unthread does not offer a one-click "Export All" button. The primary — and most complete — way to export data from Unthread is through its REST API, which provides cursor-based pagination across all core objects: conversations, messages, customers, accounts, users, tags, ticket types, knowledge base articles, surveys, and reporting metrics. Every API response is JSON. If you need CSV, you transform it yourself after extraction.
Because Unthread operates as a Slack-native helpdesk, your support data has a split personality. Ticket metadata (status, assignee, priority, SLAs, custom fields) lives in Unthread, while conversation history and attachments are tightly coupled to Slack's infrastructure. Extracting data with full fidelity requires API scripts and engineering resources — not dashboard clicks.
TL;DR: Most operational data is exportable via the API. Unthread has no native bulk UI export for conversations or tickets. The REST API at api.unthread.io covers conversations, messages, customers, accounts, tags, ticket types, users, knowledge base articles, surveys, and time-series reporting — all returned as JSON with cursor-based pagination capped at 100 records per page. File attachments require a separate per-file download endpoint. The biggest gaps: automation configurations, audit logs, saved views, SLA policy definitions, and user role assignments have no documented export path. You will need a developer to write extraction scripts.
What Data Can You Export from Unthread?
Unthread's data model centers on Conversations (the equivalent of tickets), Messages, Customers (mapped to Slack channels), Accounts (company-level groupings), Users, Tags, Ticket Types (with custom fields), Knowledge Base Articles, Surveys, Outbound Messages, and Reporting Metrics.
[For engineering] Unthread currently uses both a legacy Customer (Channel) model and a newer Account model for external organizations. If your workspace supports external customer accounts, export both — or you risk losing part of the relationship layer between people, channels, and organizations.
| Object | Native UI Export | API Export | Format | Known Limitations or Gaps |
|---|---|---|---|---|
| Conversations (tickets) | No | Yes (POST /conversations/list) |
JSON | Full message bodies require a separate call per conversation. The files array includes metadata only — file content requires per-file download. |
| Messages | No | Yes (POST /conversations/:id/messages/list) |
JSON | Must be fetched per-conversation. No bulk "all messages" endpoint. |
| Customers (Channels) | No | Yes (POST /customers/list; GET /customers/:id) |
JSON | Legacy model. Linked Slack channel metadata included, but autoresponder configs are nested objects that need parsing. |
| Accounts | No | Yes (POST /accounts/list) |
JSON | Newer model for external organizations. Custom fields returned as untyped arrays. External CRM metadata included if present. |
| Users (Agents) | No | Yes (POST /users/list) |
JSON | Pass includeExternalUsers: true to get non-workspace users. Returns Slack-linked user metadata but not role or permission assignments. |
| Tags | No | Yes (POST /tags/list) |
JSON | No export of tag-to-conversation link tables — reconstruct these from conversation records. |
| Ticket Types + Custom Fields | No | Yes (POST /ticket-types/list) |
JSON | Field definitions exported; field values live on each conversation's ticketTypeFields object, keyed by field UUID. |
| Knowledge Base Articles | No | Yes (POST /knowledge-base/articles/list) |
JSON | Article content, title, URL, status, and user group access included. Source sync metadata (crawled URLs) is not exposed. |
| Surveys + Responses | No | Yes (POST /surveys/list, POST /surveys/:id/responses/list) |
JSON | CSAT analytics available via GET /surveys/:id/analytics. |
| Outbound Messages | No | Yes (GET /outbounds) |
JSON | Includes Slack Block Kit content, recipients, and delivery status. |
| Reporting / Analytics | Yes (CSV from dashboard) | Yes (POST /reporting/time-series) |
CSV / JSON | UI exports aggregated analytics as CSV. API returns time-series metrics only — not raw per-ticket data. |
| Approval Requests | No | Yes (GET /conversations/:id/approval-requests) |
JSON | Per-conversation only; no bulk list endpoint across all conversations. |
| File Attachments | No | Yes (GET /conversations/:id/files/:fileId/full) |
Binary | One download per file. Max file size is 20MB. Slack enforces 10 files per message. |
| Webhook Subscriptions | No | Yes (GET /webhook-subscriptions) |
JSON | Covers configured webhook URLs and their enabled/disabled state. |
| User Groups | No | Yes (POST /user-groups/list) |
JSON | Filter definitions included. |
| Automations | No | No documented list endpoint | N/A | You can create automations via API, but there is no documented endpoint to list or export existing automation configurations. |
| Audit Logs | No | No | N/A | No documented API endpoint. Enterprise UI only. Coverage is incomplete — ticket type changes, tag changes, webhook changes, and escalation type changes are explicitly listed as not yet audited. |
| Saved Views / Filters | No | No | N/A | Dashboard-level saved views have no export path. |
| Integration Configs | No | No | N/A | Salesforce, HubSpot, Jira, Linear connection configs cannot be exported. |
| User Roles / Permissions | No | No | N/A | Role assignments and project-level permission mappings are not API-readable. |
| SLA Policy Definitions | No | No | N/A | SLA results (breach/met status) are available on conversation objects, but the policy definitions, countdown configurations, and business-hour rules are not exportable. |
[For engineering] Conversation records include computed fields like responseTime, responseTimeWorking, resolutionTime, and resolutionTimeWorking — all in seconds. These are available on the conversation object but not separately aggregated unless you use the /reporting/time-series endpoint.
Unthread's documented ticket-type field types are short-answer, long-answer, single-select, multi-select, checkbox, user-select, and multi-user-select. Knowing this matters when you map Unthread data into a target schema.
API Response Structure: What the Data Actually Looks Like
Before writing extraction code, it helps to understand what the API returns. Here is a representative (truncated and anonymized) conversation object from POST /conversations/list:
{
"data": [
{
"id": "conv_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"friendlyId": "TKT-1042",
"title": "SSO login failing for enterprise users",
"status": "closed",
"priority": 7,
"createdAt": "2025-11-14T09:23:41.000Z",
"updatedAt": "2025-11-15T14:02:19.000Z",
"closedAt": "2025-11-15T14:02:19.000Z",
"assignedToUserId": "usr_f8e7d6c5-b4a3-2109-8765-43210fedcba9",
"customerId": "cust_1a2b3c4d-5e6f-7890-abcd-ef0987654321",
"channelId": "C04ABCDEF12",
"responseTime": 342,
"responseTimeWorking": 342,
"resolutionTime": 103358,
"resolutionTimeWorking": 28800,
"tags": [
{ "id": "tag_aaa111", "name": "sso" },
{ "id": "tag_bbb222", "name": "enterprise" }
],
"ticketTypeId": "tt_xyz789",
"ticketTypeFields": {
"fld_abc123-def456": "Production",
"fld_ghi789-jkl012": ["High Impact"]
},
"summary": "User reported SSO login loop on Chrome 119...",
"files": [
{
"id": "file_001",
"name": "error-screenshot.png",
"size": 245760,
"filetype": "png",
"mimetype": "image/png"
}
],
"followers": ["usr_aaa111", "usr_bbb222"],
"metadata": {}
}
],
"totalCount": 4287,
"cursors": {
"hasNext": true,
"hasPrevious": false,
"next": "eyJjcmVhdGVkQXQiOiIyMDI1LTExLTE0VDA5OjIzOjQxLjAwMFoiLCJpZCI6ImNvbnZfYTFiMmMzZDQifQ==",
"previous": null
}
}Key observations from this structure:
ticketTypeFieldsis a flat object keyed by field UUID, not by human-readable label. You must join against the ticket type definitions fromPOST /ticket-types/listto make these meaningful.tagsis an inline array — there is no separate tag-assignment table to export.responseTimeandresolutionTimeare in seconds.responseTimeWorkingandresolutionTimeWorkingreflect business-hours calculations.filescontains metadata only. The binary content requires a separateGETcall per file.cursors.nextis a base64-encoded cursor token. Pass it back in your next request body to paginate forward.priorityuses numeric values:3(urgent),5(high),7(normal),9(low).
Export Methods: Every Way to Get Data Out of Unthread
| Method | Data Coverage | Volume Limit | Speed | Complexity | Best For |
|---|---|---|---|---|---|
| REST API (custom scripts) | All API-exposed objects | 100 records/page, no documented daily cap | Moderate | High | Full data export, migration, backup |
| Webhooks (real-time) | Conversations, messages | Event-driven, no bulk | Real-time | Medium | Ongoing sync, not historical export |
| BI Tool Integration (Looker, Tableau, Hex) | Analytics/reporting data | Aggregated metrics | Fast | Low–Medium | Executive dashboards, trend analysis |
| Third-Party ETL (Pipedream, n8n, Make) | Most API objects via connector | Connector-dependent | Varies | Medium | Teams without custom dev capacity |
| Analytics CSV (dashboard) | Metrics only | No documented row limit | Fast | Low | Reporting, board packs |
[For PMs] Zero-engineering options: analytics CSV for KPI reporting and BI tool integrations for dashboards. Every other method requires a developer or a third-party integration platform.
A practical rule: use analytics CSV when the question is "How did support perform?" Use the API when the question is "What exact conversations, replies, files, tags, and custom fields do we own?" Those are different jobs, and Unthread treats them differently in the product.
Native UI Export: What Exists and What Doesn't
Unthread's dashboard does not provide a "Download CSV" or "Export All" button for conversations, customers, or tickets.
What you can do from the UI:
- Analytics CSV: Go to Dashboard → Analytics, open a report, set your filters and date range, then click the download button in the top-right. The output is CSV. Analytics data can export to Looker, Tableau, Hex, and other business intelligence platforms.
- Analytics include total tickets, resolved count and resolution rate, open/in-progress breakdown, critical ticket count, mean and median response time, P95 response time, mean and median resolution time, weekly volume and resolved trend charts, response time trend by week, agent workload by tickets resolved, average resolution time per agent, priority breakdown, and channel mix.
What you cannot do from the UI:
- Bulk-export conversations, messages, or customers to CSV or JSON
- Export knowledge base articles in bulk
- Download attachments in bulk
- Export tag assignments or ticket type configurations
Plan-tier access differences:
| Capability | Free | Basic | Pro | Enterprise |
|---|---|---|---|---|
| Analytics dashboard + CSV export | Yes | Yes | Yes | Yes |
| API access (service accounts) | No | Yes | Yes | Yes |
| Audit log viewing (UI only) | No | No | No | Yes |
| Webhook subscriptions | No | Yes | Yes | Yes |
Verify current tier requirements against Unthread's pricing page before committing to an export approach.
If your requirement is "get all my conversations out of Unthread," the API is the only path.
API-Based Export: Endpoints, Pagination, and Rate Limits
Authentication
In order to authenticate, you must send an HTTP header with the key "X-Api-Key" and the value set to the service account key created in the previous section. Service accounts are created under Setup > Service Accounts (project-scoped) or Workspace Settings > Service Accounts (tenant-wide). There are no limits on the number of service accounts you can create. Copy the key immediately — it is shown only once.
API doc inconsistency: The auth section says X-Api-Key should contain the raw service-account key, while some multipart upload examples prefix the header value with Bearer. The customer object schema also uses replyTimeoutMinutes, while the customer update docs list replyTimeoutHours. Test both patterns in your tenant before wiring production export code.
Key Endpoints for Full Export
POST /conversations/list → All conversations (tickets)
POST /conversations/:id/messages/list → Messages per conversation
GET /conversations/:id → Single conversation detail
GET /conversations/:id/files/:fileId/full → Download attachment
POST /customers/list → All customers (channels)
POST /accounts/list → All accounts (companies)
POST /users/list → All users (agents + external)
POST /tags/list → All tags
POST /ticket-types/list → All ticket types + field defs
POST /knowledge-base/articles/list → All KB articles
POST /surveys/list → All surveys
POST /surveys/:id/responses/list → Survey responses
GET /outbounds → All outbound messages
GET /webhook-subscriptions → Webhook configs
POST /reporting/time-series → Aggregated analytics
POST /user-groups/list → User groups
GET /conversations/:id/approval-requests → Approval requests
Pagination
Responses are paginated by providing a cursor to the next or previous page. The limit per page is 100. All list endpoints use POST with a standardized request body that accepts select, order, where, limit, descending, and cursor fields. The response includes a totalCount and a cursors object with hasNext, hasPrevious, next, and previous fields.
Documented where operators include ==, !=, >, <, in, notIn, contains, notContains, and like. Conversation status values are open, in_progress, on_hold, and closed. Priority values are 3 (urgent), 5 (high), 7 (normal), and 9 (low).
Error Handling and Known API Behaviors
The API documentation does not publish a comprehensive error code taxonomy. Based on standard REST conventions and the documented behavior:
| HTTP Status | Meaning | Recommended Action |
|---|---|---|
| 400 | Bad request — malformed body, invalid where clause, or unrecognized select field |
Log the request body and response. Check field names against ticket type definitions. |
| 401 | Invalid or missing X-Api-Key header |
Verify the service account key is active and correctly formatted. |
| 404 | Resource not found — deleted conversation, invalid ID | Log and skip. Track orphaned references for validation. |
| 429 | Rate limit exceeded | Back off using Retry-After header if present, otherwise exponential backoff starting at 1 second. |
| 500+ | Server error — transient | Retry with exponential backoff, max 5 attempts. |
When a cursor becomes invalid (e.g., due to data changes during extraction), the API may return a 400. If this happens mid-extraction, restart from the beginning of the current page using a fresh cursor from the previous successful response.
Rate Limits
Unthread's API documentation does not publish explicit per-minute or per-day rate limits as of mid-2026. This is a real gap for teams planning bulk extraction. In practice:
- Implement exponential backoff on HTTP 429 responses
- Start with a conservative 2 requests per second as a baseline and adjust based on observed 429 responses
- Monitor the
Retry-Afterheader if returned - Handle HTTP 500+ responses with retry logic — transient server errors occur during large extractions
- Contact Unthread support (
support@unthread.io) for guidance if extracting more than 50,000 records or if you need temporary rate limit increases
Undocumented rate limits are a real risk. Without published limits, aggressive extraction scripts may hit silent throttling or get blocked entirely. Always build retry logic into your export pipeline. If you observe consistent 429s, reduce concurrency before increasing delay — Unthread may throttle per-key rather than per-IP.
Extraction Script
// Node.js — Export all Unthread conversations with retry + cursor pagination
const API_BASE = 'https://api.unthread.io/api';
const API_KEY = process.env.UNTHREAD_API_KEY;
const DELAY_MS = 500; // Baseline: 2 req/sec — increase if no 429s observed
const MAX_RETRIES = 5;
const CONCURRENCY = 1; // Single-threaded by default; see concurrency notes below
async function fetchWithRetry(path, options = {}, attempt = 0) {
if (attempt >= MAX_RETRIES) {
throw new Error(`Max retries (${MAX_RETRIES}) exceeded for ${path}`);
}
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers: {
'X-Api-Key': API_KEY,
'Content-Type': 'application/json',
...(options.headers || {})
}
});
if (res.status === 429 || res.status >= 500) {
const retryAfter = parseInt(res.headers.get('Retry-After') || '0', 10);
const delayMs = retryAfter
? retryAfter * 1000
: Math.min(30000, 1000 * 2 ** attempt);
console.warn(`${res.status} on ${path}. Retry ${attempt + 1}/${MAX_RETRIES} in ${delayMs}ms...`);
await sleep(delayMs);
return fetchWithRetry(path, options, attempt + 1);
}
if (res.status === 400) {
const body = await res.text();
console.error(`400 Bad Request on ${path}: ${body}`);
throw new Error(`Bad request: ${body}`);
}
if (!res.ok) throw new Error(`API error: ${res.status} on ${path}`);
return res.json();
}
async function fetchAllConversations() {
const allConversations = [];
let cursor = null;
let hasNext = true;
let page = 0;
while (hasNext) {
const body = {
select: [
'id', 'friendlyId', 'title', 'status', 'priority',
'createdAt', 'updatedAt', 'closedAt',
'assignedToUserId', 'customerId', 'channelId',
'responseTime', 'responseTimeWorking',
'resolutionTime', 'resolutionTimeWorking',
'tags.id', 'tags.name',
'ticketTypeId', 'ticketTypeFields',
'summary', 'files', 'metadata'
],
order: ['createdAt', 'id'],
limit: 100,
descending: false,
...(cursor ? { cursor } : {})
};
const json = await fetchWithRetry('/conversations/list', {
method: 'POST',
body: JSON.stringify(body)
});
allConversations.push(...json.data);
hasNext = json.cursors.hasNext;
cursor = json.cursors.next;
page++;
console.log(
`Page ${page}: ${json.data.length} records ` +
`(total: ${allConversations.length}/${json.totalCount})`
);
await sleep(DELAY_MS);
}
return allConversations;
}
async function exportWorkspace() {
// Phase 1: Export reference data (needed for ID resolution)
const [users, customers, accounts, tags, ticketTypes] = await Promise.all([
fetchAllPages('/users/list', { includeExternalUsers: true }),
fetchAllPages('/customers/list'),
fetchAllPages('/accounts/list'),
fetchAllPages('/tags/list'),
fetchAllPages('/ticket-types/list')
]);
// Phase 2: Export conversations
const conversations = await fetchAllConversations();
// Phase 3: Fetch messages + attachments per conversation
for (const convo of conversations) {
const msgs = await fetchAllPages(
`/conversations/${convo.id}/messages/list`
);
convo._messages = msgs;
// Download file attachments
if (Array.isArray(convo.files)) {
for (const file of convo.files) {
try {
const fileRes = await fetch(
`${API_BASE}/conversations/${convo.id}/files/${file.id}/full`,
{ headers: { 'X-Api-Key': API_KEY } }
);
// Stream fileRes.body to disk or object storage
} catch (err) {
console.error(`Failed to download file ${file.id}: ${err.message}`);
}
}
}
await sleep(DELAY_MS);
}
// Phase 4: Write everything to your archive format
return { conversations, users, customers, accounts, tags, ticketTypes };
}
// Generic paginator for any list endpoint
async function fetchAllPages(path, extraBody = {}) {
const all = [];
let cursor = null;
let hasNext = true;
while (hasNext) {
const json = await fetchWithRetry(path, {
method: 'POST',
body: JSON.stringify({ limit: 100, ...extraBody, ...(cursor ? { cursor } : {}) })
});
all.push(...json.data);
hasNext = json.cursors?.hasNext ?? false;
cursor = json.cursors?.next ?? null;
await sleep(DELAY_MS);
}
return all;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Concurrency Strategies for Large Exports
The script above is single-threaded. For datasets over 50,000 conversations, consider these approaches:
- Parallel message fetching: After retrieving all conversation IDs, split them into batches and fetch messages with a concurrency pool (e.g.,
p-limitwith concurrency of 3–5). Monitor for 429s and reduce concurrency dynamically. - Separate attachment workers: File downloads are I/O-bound and can run in parallel via a worker queue. Use a separate rate-limited pool for
GET /files/:id/fullcalls. - Checkpoint-based resumption: Write cursor state and completed conversation IDs to disk after each page. If the script crashes, resume from the last checkpoint rather than restarting.
At concurrency 5 with no throttling, message extraction for 10,000 conversations drops from ~90 minutes (single-threaded at 2 req/sec) to ~18 minutes. Adjust based on observed 429 frequency.
Throughput Math
At 100 records per page and a conservative 2 requests per second (single-threaded):
- 1,000 conversations: ~10 pages → under 10 seconds (conversation metadata only)
- 10,000 conversations: ~100 pages → under 2 minutes (conversation metadata only)
- 100,000 conversations: ~1,000 pages → under 10 minutes (conversation metadata only)
The bottleneck is message extraction. Each conversation requires a separate POST /conversations/:id/messages/list call. For 10,000 conversations averaging 8 messages each, that adds roughly 10,000 additional API calls — approximately 83 minutes at 2 requests per second (single-threaded). Attachments add another pass: 10,000 conversations × 1.5 average attachments = ~15,000 file downloads, adding approximately 125 minutes at 2 req/sec.
Total extraction time for 10,000 conversations with messages and attachments (single-threaded, 2 req/sec): approximately 3.5 hours. With concurrency 5: approximately 45 minutes.
Exporting Attachments, Files, and Media
Unthread stores file metadata on the conversation object in the files array. Each entry includes id, name, size, filetype, and mimetype. The actual file content must be downloaded separately.
Download endpoint:
GET /conversations/:conversationId/files/:fileId/full
Constraints:
- Maximum file size: 20MB per file
- Slack enforces 10 file attachments per message (not per conversation)
- Downloads are one file per request — there is no bulk zip download
- Inline images posted in Slack threads are captured if they were posted as file attachments; paste-in screenshots may only be available via Slack's own API
- Unthread does not publish a separate file manifest endpoint — build your own as you crawl conversations
[For engineering] If you have 5,000 conversations with an average of 1.5 attachments each, that's ~7,500 individual download requests. At 2 requests per second (single-threaded), that's roughly 62 minutes of download time before accounting for file transfer bandwidth. With concurrency 5 and no throttling: approximately 12 minutes. Plan storage accordingly — at an average of 500KB per file, 7,500 files ≈ 3.7GB.
What Data Cannot Be Exported from Unthread?
| Data Type | Exportable? | Workaround |
|---|---|---|
| Automation / workflow configurations | No — no list endpoint documented | Screenshot or manually document each automation. Keep automation JSON and custom-function code in your own Git repo as the source of truth. |
| Saved views / custom filters | No | Rebuild manually in the target platform. |
| Audit logs / activity history | No — no API access, Enterprise UI only, and coverage is incomplete (ticket type, tag, webhook, and escalation changes are not audited) | Capture evidence outside the platform if auditors need packaged deliverables. |
| User role assignments / permissions | No | users/list returns user metadata but not role or permission mappings. Document manually. |
| Integration configurations (Salesforce, Jira, HubSpot, Linear) | No | Document integration settings manually before decommissioning. |
| SLA policy definitions | No — no documented export endpoint | You can export SLA results from conversation fields (e.g., responseTime, resolutionTimeWorking), but the policy definitions, countdown configurations, and business-hour rules are not exportable. |
| Deleted or archived conversations | Unclear | API does not document a filter for archived/deleted status. Conversations with status: closed are accessible, but truly deleted records may not be retrievable. |
| Slack thread formatting / reactions | Partial | Message text is captured, but Slack emoji reactions, rich formatting, and threading structure may be lossy. Use Slack's own export tools alongside Unthread for full fidelity. |
| KB sync source metadata | No | Article content and URLs are available, but crawl configs and source URLs are not exposed. Recreate sync config in the target. |
The automation gap is the most operationally painful. If you've built complex routing, escalation, or auto-response workflows in Unthread, document them manually before starting a migration. There is no way to programmatically export and replay them. For a workspace with 20+ automations including conditional branching, escalation chains, and AI response rules, expect 2–4 weeks of manual reconstruction effort in the target platform.
Two data gaps that exist before Unthread even sees the data:
- Slack channel backfill is limited to 6 months when connecting a channel. Older history never enters Unthread.
- Email support only creates tickets for new emails received after setup, not past mailbox history.
If you rely on Unthread as your system of record, export legacy Slack and email history from the source systems separately.
Data Format, Encoding, and Cleanup After Export
The Unthread API follows RESTful conventions when possible, with most operations performed via GET, POST, PATCH, and DELETE requests. Request and response bodies are encoded as JSON.
All API responses are JSON (UTF-8). There is no native CSV or XML export from the API. The dashboard exports analytics as CSV — test a sample for UTF-8 BOM behavior before hard-coding a downstream loader.
Common post-export issues:
- Nested objects: Tags, custom field values, followers, and collaborators are nested arrays on conversation records. Flattening them for CSV or import into a destination platform requires explicit mapping logic. For example,
tagsmust be either pipe-delimited into a single column or exploded into separate rows. - Timestamps:
createdAt,updatedAt,closedAtare ISO 8601 strings (e.g.,"2025-11-14T09:23:41.000Z"). Slack message timestamps use Unix epoch format (e.g.,"1675183719.124879"). Normalize before import. - Slack mrkdwn in messages: Message
textfields contain Slack mrkdwn formatting, including user mentions like<@U012AB3CD>. If migrating to a platform that expects HTML or plain text, you need a conversion step. Those<@U...>references must be cross-referenced against your Slack workspace directory (via Slack'susers.infoAPI) to resolve to actual names or email addresses — otherwise they render as broken text in the destination. - Custom field key mapping: Conversation custom values live in
ticketTypeFieldskeyed by field UUID (e.g.,"fld_abc123-def456": "Production"), not by human-readable label. You must export ticket type definitions fromPOST /ticket-types/listand build a UUID → label lookup table. - Orphaned references: Conversations may reference a
customerIdorassignedToUserIdthat was deleted. Validate referential integrity post-export by checking all foreign key references against your exported user and customer datasets. - Dual IDs: Preserve both the UUID-style
id(e.g.,conv_a1b2c3d4...) and the human-friendlyfriendlyId(e.g.,TKT-1042) on conversations — you'll need both during migration and for user-facing audit trails. - Duplicate tag names: Tags are identified by UUID, but name collisions are possible if tags were renamed over time. Deduplicate by UUID, not by name.
Post-export validation checklist:
- Total conversation count in export matches
totalCountfrom the first API response - Every conversation has at least one message (fetch count from messages endpoint)
- All
customerIdreferences resolve to a record in your exported customers dataset - All
assignedToUserIdreferences resolve to a record in your exported users dataset - File attachment metadata count matches successfully downloaded file count
- Date ranges in export match expected historical range (check
createdAtmin/max) - Ticket type field values are present (not null) for conversations with a
ticketTypeId - Tag associations survive the flattening process — spot-check 10 conversations manually
Export for Migration vs. Backup vs. Compliance
Define your objective before writing extraction code. The scope changes significantly based on use case.
Migration
The goal is reshaping Unthread data for import into a destination platform (Zendesk, Freshdesk, Intercom, Jira Service Management, etc.). Key considerations:
- ID mapping: Unthread uses UUIDs for all objects. Your destination will assign its own IDs. Build and maintain a mapping table (
unthread_id → destination_id) for every object type. Persist this mapping — you will need it for post-migration validation and for resolving cross-references. - Relationship preservation: Conversation-to-customer, conversation-to-assignee, and conversation-to-tag relationships must be reconstructed using the mapping table.
- Message threading: Unthread stores messages per conversation, which maps well to most destination platforms' per-ticket threading — but message author attribution requires matching Unthread user IDs to destination user IDs.
- Custom fields: Unthread's
ticketTypeFieldsare key-value pairs keyed by field UUID. Map each UUID to the corresponding field in the destination. Field type compatibility matters: Unthread'smulti-user-selectmay not have a direct equivalent in every platform. - Automations rebuild: Plan to manually recreate routing rules, escalation workflows, and auto-response configurations. These cannot be exported.
- Destination API constraints: Each target platform imposes its own import limits. Zendesk's Ticket Import API caps at 100 tickets per request. Freshdesk's import API requires tickets to reference existing contacts. Jira Service Management uses a different threading model (comments, not messages). Plan your transformation layer around the destination's constraints, not just the source's structure.
For related migration guides, see Kustomer to Unthread Migration and How to Migrate Multiple Help Desks Into One.
Backup
For a full-fidelity backup, you need: all conversations with message histories, customer and account records, file attachments (downloaded as binary), tag definitions and assignments, ticket type definitions and field values, knowledge base articles, survey data, and user records.
Since there is no native scheduled export, you'll need a cron job or orchestration tool (GitHub Actions, AWS Lambda, Cloud Scheduler) running the extraction script on a recurring basis.
For incremental backup after the initial backfill, Unthread documents webhook events for conversation_created, conversation_updated, conversation_deleted, and message_created. Your webhook receiver must answer URL verification within 3 seconds and regular events within 30 seconds. Unthread retries failed deliveries up to 4 more times and eventually disables an endpoint after more than 100 consecutive failures or a 410 response.
Compliance (GDPR, DSAR)
Unthread's API supports filtering conversations by customerId, enabling per-individual data extraction for DSAR fulfillment. There is no pre-built DSAR workflow in the UI. You will need to:
- Identify the customer record by email or Slack ID using
POST /customers/listwith awherefilter - Extract all conversations linked to that
customerId - Extract all messages within those conversations
- Download associated file attachments
- Package the data in a machine-readable format (JSON is native; CSV requires transformation)
- Document the extraction process for your compliance audit trail
Data is always encrypted in transit and at rest, and is retained until a deletion request is made. Deletion requests can be made by emailing security@unthread.io.
Unthread is SOC 2 Type II compliant and HIPAA compliant with BAA for enterprise plans.
This compliance posture supports data export workflows but does not substitute for your own chain-of-custody documentation. For a deeper dive on compliance during migration, see our GDPR Compliant Data Migration guide.
Vendor Lock-In and Data Portability Assessment
Unthread scores well on data portability for a Slack-native helpdesk. The core data model is well-structured and fully API-accessible. The lock-in risk concentrates in configuration and workflow state, not in operational data.
| Dimension | Assessment |
|---|---|
| Core conversation + message data | Fully exportable via API. JSON format, well-structured, cursor-paginated. |
| Customer and account data | Fully exportable. Includes Slack channel metadata and custom fields. |
| File attachments | Exportable but slow. One-by-one download, 20MB cap per file, no bulk endpoint. |
| Analytics / reporting | Exportable as aggregates. Time-series endpoint covers key metrics. BI integrations cover dashboard-level data. |
| Knowledge base | Fully exportable. Article content, titles, URLs, status. No sync source metadata. |
| Automations / workflows | Not exportable. This is the primary lock-in vector. No list endpoint exists. |
| User roles and permissions | Not exportable. Must be reconfigured manually in the target. |
| SLA policy definitions | Not exportable. SLA results are on conversation objects; policy configs are not. |
| Integration configs | Not exportable. Salesforce, Jira, HubSpot, Linear connections must be rebuilt. |
[For CTOs] The real switching cost is not data extraction — it's automation and workflow reconstruction. If you've built 20+ automations with conditional branching, escalation steps, and AI response rules, plan 2–4 weeks of rebuilding effort in the target platform. The data itself moves in days. The portability gap is entirely in configuration state, not in operational data.
Unthread's terms of service do not impose restrictions on data export or portability. Data is retained until a deletion request is made, per the terms at unthread.io/terms. There are no documented API call quotas tied to plan tiers that would block a full export.
Decision Matrix: Which Export Method to Use
| Your Situation | Recommended Method | Why |
|---|---|---|
| Need KPI dashboards for leadership | Analytics CSV + BI tool integration | No engineering required. Covers all standard support metrics. |
| Migrating to another helpdesk | REST API extraction script | Only method that captures conversations, messages, attachments, and custom fields at full fidelity. |
| Regulatory compliance / DSAR | REST API with customerId filtering |
Per-individual extraction with audit trail. |
| Ongoing backup / disaster recovery | Webhooks (incremental) + periodic full API extraction | Webhooks catch real-time changes; periodic full pulls catch anything webhooks missed. |
| One-time archive before decommissioning | Full REST API extraction + Slack export | Combine Unthread API data with Slack's native export for complete coverage of thread formatting and reactions. |
| No engineering capacity available | Third-party ETL (Pipedream, n8n, Make) | Pre-built connectors reduce development effort. Coverage depends on connector maturity. |
Timeline and Resourcing for a Full Data Export
These estimates assume a single developer working on a standard workstation with stable network connectivity, running the extraction script single-threaded at 2 requests per second. Parallelism reduces extraction time proportionally (see concurrency section above).
| Dataset Size | Scoping | API Setup | Extraction (conversations + messages + attachments) | Validation + Cleanup | Total Estimate |
|---|---|---|---|---|---|
| Small (<10K conversations) | 2 hours | 1 hour | 2–4 hours | 2–4 hours | 1–2 days |
| Medium (10K–100K conversations) | 4 hours | 1 hour | 4–12 hours | 4–8 hours | 2–4 days |
| Large (100K+ conversations) | 1 day | 2 hours | 1–3 days | 1–2 days | 4–7 days |
[For PMs] If you only need conversation metadata (no message bodies, no files), cut extraction time by 60–80%. The validation phase is where most projects slip — budget accordingly. Common validation failures: orphaned customer references, missing messages on older conversations, and custom field values that don't match current ticket type definitions (indicating the ticket type was modified after the conversation was created).
Minimum team composition:
- API export: 1 developer with Node.js or Python experience and API integration skills
- Data transformation: Same developer, or a data engineer if the target schema is complex (e.g., migrating to a platform with a fundamentally different threading model)
- Validation: PM + 1 CS team member to spot-check record accuracy against the Unthread UI
Frequently Asked Questions
- Can I export all my data from Unthread?
- Most operational data is exportable through Unthread's REST API, including conversations, messages, customers, accounts, users, tags, ticket types, knowledge base articles, and surveys. The major exceptions are automation configurations, audit logs, saved views, and user role assignments — these have no documented export path. There is no one-click full workspace export from the dashboard.
- Does Unthread export include attachments?
- File attachment metadata is included on conversation objects in the files array. The actual file content must be downloaded individually using GET /conversations/:conversationId/files/:fileId/full. Maximum file size is 20MB per file. There is no bulk zip download — each file requires a separate API call.
- How long does it take to export data from Unthread?
- For a workspace with 10,000 conversations including messages and attachments, expect 2–4 days total covering scoping, extraction, and validation. The extraction phase itself takes 4–12 hours depending on message volume. Workspaces with 100,000+ conversations may take 4–7 days end to end.
- What are Unthread's API rate limits for bulk export?
- Unthread does not publish explicit API rate limits as of mid-2026. Pagination is capped at 100 records per page. Start at a conservative 2 requests per second, implement exponential backoff on HTTP 429 responses, and contact Unthread support if you plan to extract more than 50,000 records.
- Can I export Unthread data for GDPR compliance?
- Yes. The API supports filtering conversations by customerId, enabling per-individual data extraction for DSAR fulfillment. Unthread is SOC 2 Type II compliant and HIPAA compliant with BAA for enterprise plans. There is no automated DSAR workflow — extraction requires scripting. Deletion requests are handled via security@unthread.io.


