---
title: "Podium to Unthread Migration: The CTO's Technical Guide"
slug: podium-to-unthread-migration-the-ctos-technical-guide
date: 2026-07-27
author: Abdul
categories: [Migration Guide, Unthread]
excerpt: "Technical guide to migrating from Podium to Unthread. Covers API extraction, data model mapping, conversation threading, rate limits, and validation."
tldr: "Podium and Unthread share no structural overlap. Migration requires custom API-based ETL, conversation threading reconstruction, and an archival strategy for data with no Unthread equivalent."
canonical: https://clonepartner.com/blog/podium-to-unthread-migration-the-ctos-technical-guide/
---

# Podium to Unthread Migration: The CTO's Technical Guide


*Last verified: July 27, 2026, against Podium API v4 and Unthread's public docs/API.* ([docs.podium.com](https://docs.podium.com/reference/authentication?utm_source=openai))

Migrating from Podium to Unthread means moving data between two platforms that share almost no structural overlap. **Podium** is a customer interaction platform for local businesses built around SMS messaging, webchat, reviews, payments, and campaigns. **Unthread** is a Slack-native AI helpdesk that converts Slack conversations into tracked, routed, and resolved tickets. There is no native migration path between them. Every record requires deliberate schema translation.

The fastest viable path is a custom API-based ETL pipeline or a managed migration service. Podium's CSV exports cover contacts but miss conversation threading, message history, and relational context. Middleware tools like Zapier can forward new records but cannot handle bulk historical migration at scale.

This guide covers the technical architecture, data mapping rules, API constraints, edge cases, and step-by-step process for executing a high-fidelity migration from Podium to Unthread.

For related Unthread migrations, see:
- **[Freshchat to Unthread Migration](https://clonepartner.com/blog/blog/freshchat-to-unthread-migration-the-ctos-technical-guide/):** Covers Freshchat-specific extraction patterns and Unthread's API import behavior.
- **[Kustomer to Unthread Migration](https://clonepartner.com/blog/blog/kustomer-to-unthread-migration-the-ctos-technical-guide/):** For teams coming from a CRM-centric helpdesk.
- **[Gladly to Unthread Migration](https://clonepartner.com/blog/blog/gladly-to-unthread-migration-the-ctos-technical-guide/):** Handles people-centric data model translation.

## Why Teams Move from Podium to Unthread

The shift is architectural, not incremental. Teams migrate for these reasons:

- **Channel realignment.** Podium is built for external-facing SMS, webchat, and review management for local businesses. Teams whose support operations have shifted to Slack-based B2B customer communication or internal IT/HR support need a platform built for that workflow. Among Slack-native helpdesks (Halp, Thena, Foqal, ClearFeed), Unthread differentiates by offering full ticket lifecycle management with SLAs, AI agents, and native email/in-app chat channels — making it suitable as a primary helpdesk rather than a Slack add-on.
- **Operational maturity.** Podium has no concept of SLAs, ticket types, escalation paths, or structured routing. When support operations grow beyond "reply to texts" into tracked, prioritized ticket management, Unthread is purpose-built for that.
- **AI-native support.** Unthread's AI agents operate directly inside Slack channels for auto-classification, suggested responses, and ticket resolution. Podium's AI features are add-ons focused on review responses and receptionist functionality, not ticket resolution.
- **Cost structure.** Podium's published plans start at $399/month (Core) and scale to $599+/month (Pro) with per-location fees and add-ons. Unthread starts at $50/agent/month (Basic) or $75/agent/month (Pro). For a 5-agent team, that's $250–$375/month on Unthread vs. $399–$599+/month on Podium — a 35–60% reduction for teams that primarily need ticketing and conversation tracking. Verify current pricing directly with each vendor before planning, as both have changed pricing in the past 12 months.

## Core Data Model Differences

Understanding how these platforms model data is the prerequisite for any migration plan.

| Concept | Podium | Unthread |
|---|---|---|
| **Primary entity** | Conversation (SMS/webchat thread) | Conversation (ticket) |
| **Contact/Customer** | Contact (name, phone, email, tags, custom fields) | Customer (Slack channel, email domains, tags) + Account |
| **Organization** | Organization → Location (multi-level hierarchy) | Account (flat, with support assignees) |
| **Messages** | Conversation Items (SMS, email, webchat, internal notes) | Messages (Slack messages, email, in-app chat) |
| **Categories** | Inboxes (custom, reviews) | Ticket Types (with custom fields) |
| **Tags** | Contact tags, campaign opt-in tags | Tags (applied to conversations and customers) |
| **Reviews** | Review invites, review ratings, review body | No equivalent — archive or discard |
| **Payments** | Invoices, payment records | No equivalent — archive or discard |
| **Campaigns** | Campaign messages, interactions, opt-in/opt-out | No equivalent — Unthread outbound is Slack-only |
| **Custom fields** | Up to 8 contact custom fields | Ticket Type Fields (short-answer, multi-select, checkbox, user-select) |
| **Statuses** | Open / Closed (binary) | Open / In Progress / On Hold / Closed |

Podium's data model is conversation-first and channel-agnostic (SMS, email, webchat, Apple Chat, Facebook). Unthread's model is ticket-first and Slack-native, with email and in-app chat as secondary channels. Reviews, payments, and campaign data have **no native Unthread equivalent** — plan to archive these before migration.

## Migration Approaches

### 1. CSV Export + Manual Import

**How it works:** Export contacts from Podium as CSV (available via the Contacts tab or by requesting an export from Podium support). Manually create corresponding Customer and Account records in Unthread via the web UI or API. ([podium.com](https://www.podium.com/whats-new/25-2?utm_source=openai))

**When to use it:** Small datasets (<500 contacts) where conversation history is not required.

**Pros:**
- No engineering effort for the contact portion
- Quick for contact-only migrations

**Cons:**
- Podium's CSV export only includes Name, Phone, Email, Contact Status, Address, Tags, and Date Added — no conversation history, no messages, no attachments
- Unthread has no bulk CSV import UI; you still need API calls to create Customer/Account records
- No relationship preservation between contacts and conversations

**Complexity:** Low (contacts only) / Not viable (full migration)

### 2. FTP Raw Data Export + Custom ETL

**How it works:** Request access to Podium's FTP raw data export (requires Account Owner approval). Podium exposes flat data tables: Messages, Contacts, Leads, Campaigns, Campaign Contacts, Reviews, Review Invites, Payments, Feedback, and Locations/Organizations. Download these tables, transform them into Unthread's schema, and load via the Unthread API.

**When to use it:** Mid-size to large datasets where you need conversation history and can invest engineering time.

**Pros:**
- Access to the full Podium dataset including message bodies, conversation metadata, and lead data
- Flat table structure is straightforward to parse
- No API rate limit concerns on the extraction side

**Cons:**
- FTP access must be requested and approved by Podium — budget 3–5 business days
- Data is denormalized; you need to reconstruct conversation threads from `conversation_uid` groupings
- Message attachments are not included in FTP exports — binary files require separate handling via the API
- Requires significant transformation logic to map Podium's continuous conversation model to Unthread's discrete ticket model

**Complexity:** Medium–High

### 3. API-Based Migration (Podium REST API → Unthread REST API)

**How it works:** Extract data programmatically from Podium's REST API using OAuth 2.0 authentication, transform in-memory or via intermediate storage, and load into Unthread's API using service account API keys. ([docs.podium.com](https://docs.podium.com/reference/authentication?utm_source=openai))

**When to use it:** Full-fidelity migrations where you need conversation threading, message history, and relational context preserved.

**Pros:**
- Most granular control over what gets migrated
- Can preserve conversation→message relationships
- Real-time access to current data state
- Can handle incremental/delta migrations

**Cons:**
- Podium's API enforces rate limits (the docs specify HTTP `429` responses for exceeded limits but do not publish an exact requests-per-second ceiling — empirically, sustained extraction rates above ~2 requests/second trigger throttling). Extraction of 10K+ conversations with associated messages can take 2–4 days of continuous API calls. ([docs.podium.com](https://docs.podium.com/reference/http-status-codes?utm_source=openai))
- Podium developer account requires application and multi-day approval
- OAuth 2.0 tokens need refresh logic (tokens expire; build automatic refresh before expiry)
- Unthread API pagination is capped at 100 records per page; Unthread's write endpoint rate limits are not published in the public docs — start conservatively at ~1 request/second and adjust based on `429` response frequency

**Complexity:** High

### 4. Middleware Platforms (Zapier, Make)

**How it works:** Use iPaaS connectors to bridge Podium triggers/actions to Unthread actions. Typically limited to forwarding new records as they arrive.

**When to use it:** Ongoing sync of new conversations during a transition period — not for historical data migration.

**Pros:**
- No code required for simple forwarding
- Useful as a bridge during cutover

**Cons:**
- Cannot migrate historical data in bulk
- No native Unthread connector on most middleware platforms as of mid-2026
- Limited transformation capabilities for schema translation
- Per-task pricing makes high-volume operations expensive
- Podium webhooks time out after 5 seconds and queued events live for around 10 days ([docs.podium.com](https://docs.podium.com/docs/webhooks?utm_source=openai))

**Complexity:** Low (but limited scope)

### 5. Managed Migration Service

**How it works:** A migration partner handles extraction, transformation, loading, validation, and rollback planning end-to-end.

**When to use it:** When engineering bandwidth is limited, the dataset is large, or the migration is business-critical with zero tolerance for data loss.

**Pros:**
- Fastest time-to-completion
- Expert handling of edge cases (duplicate contacts, orphaned messages, encoding issues)
- Validation and QA built into the process
- No internal engineering hours consumed

**Cons:**
- External cost (typically $5K–$25K depending on dataset size and complexity)
- Requires sharing data access with a third party

**Complexity:** Low (for your team)

### Migration Approach Comparison

| Approach | Historical Data | Conversation Threading | Engineering Effort | Typical Timeline | Best For |
|---|---|---|---|---|---|
| CSV Export | Contacts only | ❌ | Low | 1–2 days | Contact-only moves |
| FTP Raw Data + ETL | ✅ Full tables | ⚠️ Requires reconstruction | Medium–High | 2–4 weeks | Mid-size with dev team |
| API-to-API | ✅ Full fidelity | ✅ Preserved | High | 3–6 weeks | Full migration with dev team |
| Middleware (Zapier/Make) | ❌ New records only | ❌ | Low | 1–2 days setup | Transition bridge |
| Managed Service | ✅ Full fidelity | ✅ Preserved | Low (outsourced) | 1–3 weeks | Business-critical migrations |

**Recommendations by scenario:**
- **Small business, contacts only:** CSV export + Unthread API script
- **Mid-size, full history needed, dev team available:** FTP export + custom ETL pipeline
- **Enterprise or business-critical:** Managed migration service
- **Ongoing sync during transition:** Middleware bridge for net-new data + API migration for historical data

## When to Use a Managed Migration Service

Building a Podium-to-Unthread migration pipeline in-house is straightforward to start and deceptively hard to finish. The common failure modes:

- **Conversation thread reconstruction.** Podium's FTP export delivers flat message rows keyed by `conversation_uid`. Reassembling these into threaded Unthread conversations with correct chronological ordering, sender attribution, and status mapping requires careful deduplication logic.
- **Contact deduplication.** Podium contacts are identified by phone number (`channel_unique_identifier`). The same person can have multiple contact channel entries across different channel types (phone, email, secure). Merging these into a single Unthread Customer or Account without creating duplicates requires fuzzy matching on name + phone + email combinations.
- **Rate limit math.** Podium's API throttle (~2 req/s empirically) combined with Unthread's 100-record pagination limit mean a dataset of 25K+ conversations with associated messages can take 3–5 days of continuous API calls. Most internal teams underestimate this by 3–5x because they calculate based on conversation count alone, forgetting that each conversation requires N+1 API calls (1 conversation + N messages).
- **Accidental re-sending.** Historical messages replayed into Unthread can trigger outbound notifications to customers if conversation type and note visibility are not configured correctly. This is a non-recoverable mistake — you cannot unsend a message that was delivered to a customer's Slack channel or email inbox. ([docs.podium.com](https://docs.podium.com/reference/the-message-object?utm_source=openai))
- **Hidden engineering cost.** Internal migrations consistently consume 80–160 engineer-hours for mid-size datasets (5K–25K conversations). At a fully-loaded engineering cost of $150–$200/hour, that's $12K–$32K in opportunity cost — often more than a managed service.

Do **not** build this in-house if: you need attachments, you have more than one Podium location, you rely on adjacent CRM relationships, or you need a short cutover window. The expensive part is not writing HTTP calls. It is identity matching, archive strategy, replay design, QA, and rollback.

**Why ClonePartner:** We have executed 1,500+ migrations across helpdesk platforms, including complex schema translation projects where source and target share no structural overlap. For Podium migrations specifically, we handle the FTP/API extraction, conversation threading reconstruction, contact deduplication, and Unthread API loading — with validation at every stage. If your migration involves more than 5,000 conversations or requires zero downtime, [talk to us](https://cal.com/clonepartner/meet?duration=30).

## Pre-Migration Planning

### Compliance and Data Residency

Before extracting any customer PII from Podium:

- **GDPR:** If you process data of EU residents, your staging database constitutes a new processing activity. Update your Records of Processing Activities (ROPA). If your staging environment is hosted outside the EU, ensure you have appropriate transfer mechanisms (SCCs or adequacy decisions) in place.
- **CCPA/CPRA:** California consumer data must be handled per your privacy policy's stated purposes. Migration is generally covered under "business purposes," but verify your specific disclosures.
- **Data residency:** Confirm where Podium stores your data (US by default) and where Unthread stores data. If your staging database is in a different region, you may be creating a cross-border transfer. PostgreSQL on a cloud provider in the same region as your Unthread instance minimizes risk.
- **Data minimization:** Only extract and stage what you intend to migrate. Purge the staging database within 30 days of migration completion.
- **Third-party access:** If using a managed migration service, execute a Data Processing Agreement (DPA) before granting access.

### Data Audit Checklist

Before extracting anything, audit what exists in Podium and what matters:

| Data Category | Podium Source | Migrate? | Notes |
|---|---|---|---|
| Contacts | Contacts table | ✅ Yes | Map to Unthread Customers or Accounts |
| Conversations | Messages table | ✅ Yes (selective) | Decide on date cutoff — migrate last 12–24 months |
| Message History | Messages table (`conversation_item_body`) | ✅ Yes | Reconstruct threads from `conversation_uid` |
| Locations | Location and Organizations table | ✅ Yes | Map to Unthread Accounts or Tags |
| Reviews | Reviews + Review Invites tables | ❌ Archive | No Unthread equivalent |
| Payments | Payments table | ❌ Archive | No Unthread equivalent |
| Campaigns | Campaigns + Campaign Contacts tables | ❌ Archive | Unthread outbound is Slack-only |
| Feedback/Surveys | Feedback table | ❌ Archive | No Unthread equivalent |
| Custom Fields | Contact custom fields (up to 8) | ⚠️ Map | Map to Unthread Ticket Type Fields |
| Attachments | Via API (expiring URLs) | ✅ Yes | Download immediately — URLs expire after 7 days ([docs.podium.com](https://docs.podium.com/reference/the-message-object?utm_source=openai)) |

### Define Migration Scope

1. **Date cutoff.** Migrating all historical data is rarely necessary. Most teams migrate the last 12–24 months of conversations and all active contacts.
2. **Channel filter.** Podium supports SMS, email, webchat, Apple Chat, Facebook. Decide which channel conversations are worth migrating vs. archiving.
3. **Status filter.** Migrate only open/active conversations as live tickets. Closed conversations can be imported as closed tickets for reference or archived externally.
4. **Identifier resolution.** Podium relies heavily on phone numbers. Unthread uses emails and Slack IDs. Decide how to handle Podium contacts that lack an email address — options include enrichment from a CRM or generating deterministic placeholder emails (e.g., `{phone}@podium-archive.local`).

### Migration Strategy

- **Big bang:** Extract everything, transform, load, cut over in one weekend. Best for datasets under 10K conversations.
- **Phased:** Migrate contacts and accounts first, then conversations in batches by date range or location. Best for 10K–100K conversations.
- **Incremental:** Migrate historical data in phases while running both platforms in parallel, using middleware or webhooks to sync new records. Best for enterprise datasets or zero-downtime requirements.

If you need a broader prep template, start with our [Help Desk Data Migration Checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-checklist/).

## Data Model and Object Mapping

### Contacts → Customers + Accounts

Podium Contacts map to either Unthread **Customers** (channel-level entities tied to Slack channels) or **Accounts** (organization-level entities with support assignees and email domains).

**Decision framework:**
- If the Podium contact represents a company/organization → create an Unthread **Account**
- If the Podium contact represents an individual end-user → create an Unthread **Customer** linked to an Account
- If both → create an Account first, then associate the Customer

> [!NOTE]
> Unthread's public API exposes `emailDomains` on Customers, not a direct phone field. Keep exact phone numbers in ticket fields, metadata, or your CRM if agents need them post-migration. ([docs.unthread.io](https://docs.unthread.io/docs/api-docs/api-reference))

### Locations → Accounts

Podium's Organization → Location hierarchy maps to flat Unthread **Accounts**. Each Podium Location becomes a separate Unthread Account with the location's name. The Podium Organization name can be stored in a custom field or tag.

### Conversations → Conversations (Tickets)

A Podium conversation is a continuous thread. You must decide how to segment these into discrete Unthread tickets. Two common approaches:

1. **Import the entire Podium thread as a single closed ticket** — simplest, works for archival purposes.
2. **Group messages by time decay** (e.g., a gap of 7+ days creates a new ticket) — more useful if agents need to search by incident.

Key field mappings:

| Podium Field | Unthread Field | Transformation |
|---|---|---|
| `conversation_uid` | `metadata.podium_conv_uid` | Store for traceability |
| `conversation_inserted_at` | `createdAt` | ISO 8601 format |
| `conversation_is_closed` | `status` | `true` → `closed`, `false` → `open` |
| `conversation_assigned_user_uid` | `assignedToUserId` | Map to Unthread user ID |
| `inbox_name` | `ticketTypeId` | Map inbox to Ticket Type |
| `conversation_channel_type` | `type` | Map to `email` or `triage` |
| First `conversation_item_body` | `markdown` (initial message) | Plain text or Slack-compatible markdown |

> [!WARNING]
> Unthread conversations are typed (`slack`, `email`, `triage`). Historical Podium conversations without an active Slack channel should use `triage`. The Unthread public docs do not show timestamp override fields for imported conversations or messages, so exact historical chronology may require transcript-style preservation (e.g., prepending `[2025-03-15 14:32 UTC] Agent Name:` to each message body) rather than native backdating. ([docs.unthread.io](https://docs.unthread.io/docs/api-docs/api-reference))

### Messages → Messages

Each `conversation_item` in Podium's Messages table becomes a Message in Unthread:

| Podium Field | Unthread Field | Notes |
|---|---|---|
| `conversation_item_body` | `markdown` | Text content of the message |
| `conversation_item_inserted_at` | Chronological ordering | Unthread auto-timestamps; original timestamp should be embedded in message body |
| `conversation_item_sender_name` | `onBehalfOf.name` | For email-type conversations |
| `conversation_item_type` | `isPrivateNote` | Internal notes → `true` |
| Attachment content type | Attachment via multipart form | 20MB max per file, 10 files per message |

**Replay history as private notes** for closed conversations. This prevents Unthread from accidentally sending historical messages as outbound customer-visible replies.

### Tags → Tags

Podium contact tags and campaign opt-in tags map directly to Unthread **Tags**. Create tags in Unthread first via `POST /tags`, then associate them with conversations or customers using the link endpoints.

### Custom Fields → Ticket Type Fields

Podium supports up to 8 contact custom fields with types including `BOOLEAN`, `DATETIME`, `FLOAT`, `INTEGER`, `STRING`, `SINGLE_SELECT`, and `MULTI_SELECT`. Unthread Ticket Type Fields support short-answer, long-answer, multi-select, single-select, checkbox, date, and user-select. Type mapping:

| Podium Type | Unthread Type | Notes |
|---|---|---|
| `STRING` | `short-answer` | Direct |
| `BOOLEAN` | `checkbox` | Direct |
| `DATETIME` | `date` | Strip time component if Unthread date field is date-only |
| `FLOAT`, `INTEGER` | `short-answer` | Convert to string; Unthread has no numeric field type |
| `SINGLE_SELECT` | `single-select` | Normalize picklist values before loading |
| `MULTI_SELECT` | `multi-select` | Normalize picklist values before loading |

Map by type, not by label alone. ([docs.podium.com](https://docs.podium.com/reference/the-contact-entity-attribute-object?utm_source=openai))

### Reviews, Payments, and Campaigns

Unthread has no equivalent for these Podium objects. Export them to a separate data store (S3, data warehouse, or spreadsheet archive) before migration. Do not attempt to force this data into Unthread's schema — if reviews, campaigns, or payments still matter to the business, leave them in Podium or another system of record.

## Migration Architecture

The architecture follows a standard **Extract → Stage → Transform → Load → Validate** pipeline. An intermediate database (PostgreSQL recommended) prevents data loss during API timeouts and provides a staging layer for joins and deduplication.

### Extraction Layer

**Option A: FTP Export**
- Request FTP access from your Podium Account Owner (budget 3–5 business days for approval)
- Download all available tables as flat files
- Parse into a staging database

**Option B: REST API**
- Register an OAuth 2.0 app at `developer.podium.com` (requires approval — plan for several business days)
- Authenticate via OAuth 2.0 flow to obtain access tokens with refresh logic (set a timer to refresh tokens ~5 minutes before expiry)
- Podium's API returns `429 Too Many Requests` when rate limits are exceeded but does not publish the exact threshold. Empirically, sustained rates above ~2 requests/second trigger throttling. Build a fixed-delay pacer (500ms between requests) or token bucket into your extraction script rather than reacting to `429` responses with retry logic. Proactive throttling creates predictable throughput — expect roughly 5,000–7,000 contacts/hour or 3,000–5,000 conversations/hour depending on payload size. ([docs.podium.com](https://docs.podium.com/reference/authentication?utm_source=openai))
- The contacts endpoint supports `updated_at` filtering for delta runs ([docs.podium.com](https://docs.podium.com/reference/contactindex?utm_source=openai))

**Critical for both approaches:** Download all attachments during extraction. Podium attachment URLs are time-limited signed URLs that expire after 7 days. If you delay, you lose them. ([docs.podium.com](https://docs.podium.com/reference/the-message-object?utm_source=openai))

### Transformation Layer

1. **Deduplicate contacts** by `contact_uid` — Podium contacts can have multiple channel entries (phone, email, secure) for the same person. Without deduplication, you'll create 2–3x the expected Customer/Account count.
2. **Reconstruct conversation threads** by grouping `conversation_item` rows by `conversation_uid`, ordered by `conversation_item_inserted_at`.
3. **Map statuses** from Podium's binary open/closed to Unthread's four-state model (open, in_progress, on_hold, closed).
4. **Convert Podium Locations** to Unthread Accounts.
5. **Map inbox names** to Unthread Ticket Types.
6. **Strip or convert message formatting** — Podium messages are plain text; Unthread expects Slack-compatible markdown (mrkdwn). Convert any HTML entities, strip unsupported formatting.
7. **Generate placeholder emails** for SMS-only contacts to satisfy Unthread's schema requirements (e.g., `{phone}@podium-archive.local`).

### Loading Layer

**Unthread API endpoints:**

| Operation | Endpoint | Method | Required Fields |
|---|---|---|---|
| Create Account | `POST /accounts` | POST | `name` |
| Create Customer | `POST /customers` | POST | `name` or `emailDomains` |
| Create Tag | `POST /tags` | POST | `name` |
| Create Ticket Type | `POST /ticket-types` | POST | `name` |
| Create Conversation | `POST /conversations` | POST | `type`, `markdown` |
| Add Message | `POST /conversations/:id/messages` | POST | `markdown` |
| Link Tag to Conversation | `POST /tags/:id/conversations/create-links` | POST | `conversationIds` |
| Link Tag to Customer | `POST /tags/:id/customers/create-links` | POST | `customerIds` |

Authentication: `X-Api-Key` header with a service account key generated from Unthread's Setup > Service Accounts.

**Idempotency:** Unthread's create endpoints are **not idempotent** — calling `POST /conversations` twice with the same payload creates two conversations. To handle partial failures and script re-runs:
1. Maintain a `source_id → target_id` crosswalk table in your staging database
2. Before each create call, check the crosswalk — if a target ID already exists for that source ID, skip the create
3. Wrap each create + crosswalk-insert in a database transaction
4. On script restart, resume from the last uncommitted source ID

> [!WARNING]
> **Unthread API constraints:** All list endpoints cap at 100 records per page. File attachments are limited to 20MB per file and 10 files per Slack message. Unthread does not publish write endpoint rate limits in its public docs — start at ~1 request/second and increase gradually, monitoring for `429` responses. Based on migration experience, Unthread's API sustains ~60–90 write operations/minute without throttling. Plan your validation queries and attachment splitting accordingly. ([docs.unthread.io](https://docs.unthread.io/docs/api-docs/api-reference))

### Error Handling

Both APIs return standard HTTP error codes. Key responses to handle:

| Code | Podium Meaning | Unthread Meaning | Action |
|---|---|---|---|
| `400` | Malformed request | Malformed request / validation failure | Log payload, fix schema, retry |
| `401` | Token expired | Invalid API key | Refresh OAuth token (Podium) / check key (Unthread) |
| `404` | Resource not found | Resource not found | Log and skip — likely deleted |
| `422` | Unprocessable entity | Validation error (missing required field) | Log, inspect response body for field-level errors, fix and retry |
| `429` | Rate limit exceeded | Rate limit exceeded | Back off exponentially (start 1s, max 60s) |
| `500/502/503` | Server error | Server error | Retry with exponential backoff, max 3 attempts |

Both Podium and Unthread return JSON error bodies. Podium's format: `{"error": {"code": "...", "message": "..."}}`. Log the full response body for every non-2xx response.

### Delta Sync

If you are not doing a hard cutover, let Podium webhooks feed a queue and push only new or changed records into Unthread after the historical backfill completes. Note that Podium webhooks time out after 5 seconds and queued events live for around 10 days — if your migration takes longer than 10 days, events from early in the process may be lost. ([docs.podium.com](https://docs.podium.com/docs/webhooks?utm_source=openai))

## Step-by-Step Migration Process

### Step 1: Extract Data from Podium

**For FTP approach:**
Request FTP access through your Podium Account Owner. Download all tables. Load into a staging database.

**For API approach:**

```python
import requests
import time
import json

PODIUM_BASE = "https://api.podium.com/v4"
RATE_LIMIT_DELAY = 0.5  # 500ms between requests (~2 req/s)

def get_podium_headers(access_token):
    return {"Authorization": f"Bearer {access_token}"}

def extract_contacts(access_token, staging_db):
    """Extract all contacts and persist raw JSON to staging DB."""
    url = f"{PODIUM_BASE}/contacts"
    page = 0
    while url:
        resp = requests.get(url, headers=get_podium_headers(access_token))
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 5))
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        data = resp.json()
        for contact in data.get("data", []):
            staging_db.upsert("raw_contacts", {
                "contact_uid": contact["uid"],
                "raw_json": json.dumps(contact),
                "extracted_at": "NOW()"
            })
        page += 1
        url = data.get("links", {}).get("next")
        time.sleep(RATE_LIMIT_DELAY)
    return page

def extract_conversations(access_token, staging_db):
    """Extract conversations with messages. ~3,000-5,000 conversations/hour."""
    url = f"{PODIUM_BASE}/conversations"
    while url:
        resp = requests.get(url, headers=get_podium_headers(access_token))
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 5)))
            continue
        resp.raise_for_status()
        data = resp.json()
        for conv in data.get("data", []):
            staging_db.upsert("raw_conversations", {
                "conversation_uid": conv["uid"],
                "raw_json": json.dumps(conv),
                "extracted_at": "NOW()"
            })
            # Download attachments immediately - URLs expire in 7 days
            for item in conv.get("items", []):
                for attachment in item.get("attachments", []):
                    download_attachment(attachment["url"], staging_db)
        url = data.get("links", {}).get("next")
        time.sleep(RATE_LIMIT_DELAY)
```

Do not attempt to transform data on the fly during extraction. Store raw JSON in your staging database and download attachments immediately — attachment URLs expire.

### Step 2: Transform Data

```python
def transform_contact_to_account(podium_contact, location_map):
    """Map a Podium contact + location to an Unthread Account."""
    location = location_map.get(podium_contact.get("location_uid", ""), {})
    email = podium_contact.get("email")
    phone = podium_contact.get("phoneNumber", "")

    # Generate deterministic placeholder for SMS-only contacts
    if not email and phone:
        email = f"{phone.replace('+', '')}@podium-archive.local"

    return {
        "name": location.get("location_name", podium_contact.get("contact_name", "Unknown")),
        "emailsAndDomains": [email] if email else [],
        "customFields": [
            {"key": "podium_org", "value": location.get("organization_name", "")},
            {"key": "podium_contact_uid", "value": podium_contact.get("contact_uid", "")},
            {"key": "podium_phone", "value": phone}
        ]
    }

def transform_conversation(podium_conv, customer_id_map):
    """Map a Podium conversation to an Unthread conversation."""
    status = "closed" if podium_conv.get("conversation_is_closed") else "open"
    original_ts = podium_conv.get("conversation_inserted_at", "")
    contact_name = podium_conv.get("contact_name", "Unknown")
    first_body = podium_conv.get("first_message_body", "(migrated conversation)")

    # Embed original timestamp in message body since Unthread
    # does not support backdating
    markdown = f"**[Migrated from Podium | {original_ts}]**\n\n{first_body}"

    return {
        "type": "triage",  # No live channel for historical imports
        "markdown": markdown,
        "status": status,
        "title": f"[Migrated] {contact_name}",
        "customerId": customer_id_map.get(podium_conv.get("contact_channel_uid")),
        "metadata": {
            "podium_conversation_uid": podium_conv.get("conversation_uid"),
            "podium_channel_type": podium_conv.get("conversation_channel_type"),
            "podium_inserted_at": original_ts
        }
    }

def transform_message(podium_item, is_closed_conversation=True):
    """Transform a Podium message to an Unthread message.
    Replay as private note if conversation is closed to prevent
    accidental outbound delivery."""
    original_ts = podium_item.get("conversation_item_inserted_at", "")
    sender = podium_item.get("conversation_item_sender_name", "Unknown")
    body = podium_item.get("conversation_item_body", "")
    is_internal = podium_item.get("conversation_item_type") == "internal_note"

    return {
        "markdown": f"**[{original_ts} — {sender}]**\n{body}",
        "isPrivateNote": is_internal or is_closed_conversation
    }
```

Key transformation tasks:
- Cleanse invalid characters and format phone numbers to E.164 standard
- Resolve user mappings (Podium agent IDs → Unthread user IDs)
- Generate placeholder emails for SMS-only contacts
- Normalize picklist and custom field values by type

### Step 3: Load into Unthread

Load in strict relational order to maintain referential integrity:

1. Accounts
2. Customers (linked to Accounts)
3. Tags and Ticket Types
4. Conversations (with initial message body)
5. Messages (replayed via `POST /conversations/:id/messages`)
6. Attachments

```python
import requests
import time

UNTHREAD_BASE = "https://api.unthread.io/api"
WRITE_DELAY = 1.0  # ~1 req/s conservative start; increase to ~60-90/min after testing

def get_unthread_headers(api_key):
    return {
        "X-Api-Key": api_key,
        "Content-Type": "application/json"
    }

def create_with_crosswalk(api_key, endpoint, payload, source_id, entity_type, crosswalk_db):
    """Idempotent create: check crosswalk before creating to handle re-runs."""
    existing = crosswalk_db.get(entity_type, source_id)
    if existing:
        return existing  # Already created in a previous run

    resp = requests.post(
        f"{UNTHREAD_BASE}/{endpoint}",
        headers=get_unthread_headers(api_key),
        json=payload
    )
    if resp.status_code == 429:
        time.sleep(int(resp.headers.get("Retry-After", 5)))
        return create_with_crosswalk(api_key, endpoint, payload, source_id, entity_type, crosswalk_db)
    if resp.status_code >= 500:
        time.sleep(5)
        resp = requests.post(f"{UNTHREAD_BASE}/{endpoint}",
                           headers=get_unthread_headers(api_key), json=payload)
    resp.raise_for_status()
    result = resp.json()
    crosswalk_db.insert(entity_type, source_id, result["id"])
    time.sleep(WRITE_DELAY)
    return result

def create_account(api_key, account_data, source_id, crosswalk_db):
    return create_with_crosswalk(api_key, "accounts", account_data, source_id, "account", crosswalk_db)

def create_conversation(api_key, conv_data, source_id, crosswalk_db):
    return create_with_crosswalk(api_key, "conversations", conv_data, source_id, "conversation", crosswalk_db)

def add_message(api_key, conversation_id, message_data):
    resp = requests.post(
        f"{UNTHREAD_BASE}/conversations/{conversation_id}/messages",
        headers=get_unthread_headers(api_key),
        json=message_data
    )
    if resp.status_code == 429:
        time.sleep(int(resp.headers.get("Retry-After", 5)))
        return add_message(api_key, conversation_id, message_data)
    resp.raise_for_status()
    time.sleep(WRITE_DELAY)
    return resp.json()
```

**Replay closed conversations as private notes** to prevent Unthread from sending historical messages as outbound replies to customers.

Error handling that matters:
- Checkpoint every page cursor and every created target ID in the crosswalk table
- Retry `429` and `5xx` with exponential backoff (initial delay 1s, max 60s, max 3 retries for 5xx)
- Keep a dead-letter queue for failed records — log the full request payload and response body
- Store source-to-target ID crosswalks in a durable PostgreSQL table, not in-memory
- Checksum or size-check attachments after upload (compare source file size to uploaded file size)

### Step 4: Rebuild Relationships

After creating all Accounts, Customers, and Conversations:
1. Link Customers to Accounts using `customerId` references
2. Apply Tags to Conversations via `POST /tags/:tagId/conversations/create-links`
3. Apply Tags to Customers via `POST /tags/:tagId/customers/create-links`
4. Set support assignees on Accounts via `PATCH /accounts/:accountId`

### Step 5: Validate

See the Validation section below.

### Step 6: Delta Sync and Cutover

Run a final sync to capture any messages created in Podium during the migration window. Push them to Unthread before switching your routing rules, DNS, and webhooks. If your migration window exceeds 10 days, use the API for delta extraction rather than webhooks (webhook events expire after ~10 days).

## Edge Cases and Challenges

### Duplicate Contacts
Podium contacts can have multiple channel entries (phone, email, secure) for the same person. Podium can also automatically merge contacts, so merged UIDs need alias handling in your crosswalk — when a contact is merged, the old UID becomes an alias that should resolve to the surviving UID. ([docs.podium.com](https://docs.podium.com/changelog/new-webhooks-contacts?utm_source=openai)) Deduplicate by matching on `contact_uid` before creating Unthread records. Without this step, expect 2–3x the expected Customer/Account count.

### Multi-Channel Conversations
A single Podium conversation can span SMS and webchat. Unthread conversations are typed (`slack`, `email`, `triage`). You must choose one type per migrated conversation — use `triage` for historical imports since there is no live channel to connect.

### Attachments
Podium's FTP export does not include binary attachments. The API exposes attachment URLs, but these are time-limited signed URLs that expire after 7 days. ([docs.podium.com](https://docs.podium.com/reference/the-message-object?utm_source=openai)) Your script must download binary files during extraction and re-upload them to Unthread as multipart form data. Unthread limits attachments to 20MB per file and 10 files per Slack message — split oversized payloads accordingly. If a single Podium message has >10 attachments, split across multiple Unthread messages with a `[continued]` marker.

### Conversation Threading
Podium stores messages as flat rows. To reconstruct a threaded conversation in Unthread:
1. Group all `conversation_item` rows by `conversation_uid`
2. Sort by `conversation_item_inserted_at`
3. Use the first message as the conversation's `markdown` body
4. Replay subsequent messages via `POST /conversations/:id/messages`

### Internal Notes vs. Customer Messages
Podium's `conversation_item_type` distinguishes between customer-facing messages and internal notes. Map internal notes to Unthread's `isPrivateNote: true` parameter. For historical messages on closed conversations, replay **all** messages as private notes to prevent accidental outbound delivery.

### Agent Attribution
If an agent has left the company and does not exist in Unthread, their historical messages will fail to import unless mapped to a fallback "System User" or "Archived Agent" account. Establish these fallback mappings before starting the load phase. Create a dedicated Unthread user named "Archived Agent" and use it as the default `assignedToUserId` for any unresolvable Podium agent UIDs.

### Deprecated Webhook Fields
Podium marks `contact` and `sender` in the message object as deprecated in webhook payloads. Parse stable IDs from the currently documented fields, not fields scheduled for removal. ([docs.podium.com](https://docs.podium.com/reference/the-message-object?utm_source=openai))

### SMS-Only Contacts
Unthread expects B2B identifiers (email or Slack ID). Use a deterministic email generation pattern for SMS-only contacts: `{e164_phone_without_plus}@podium-archive.local` (e.g., `14155551234@podium-archive.local`). This satisfies schema requirements while remaining clearly distinguishable from real email addresses. Document this convention so agents understand what these addresses mean.

### Multi-Level Relationships
The same person can interact across multiple Podium locations and channels. If you key only on phone or only on email domain, you will over-merge contacts. Use `contact_uid` as the primary deduplication key. ([docs.podium.com](https://docs.podium.com/reference/the-location-object?utm_source=openai))

### Character Encoding
Podium SMS messages may contain emoji, non-ASCII characters, or special Unicode sequences. Ensure your staging database uses UTF-8 encoding and that your transformation layer handles emoji correctly — Slack's mrkdwn format supports standard Unicode but not all HTML entities.

## Limitations and Constraints

### Podium-Side
- **FTP access requires Account Owner request** and Podium approval — budget 3–5 business days
- **API rate limits apply** — empirically ~2 requests/second sustained; `429` responses when exceeded; no published ceiling ([docs.podium.com](https://docs.podium.com/reference/http-status-codes?utm_source=openai))
- **Developer account approval** takes several business days per Podium's documentation
- **No bulk export API** — data must be extracted record-by-record or via FTP
- **Custom fields limited to 8** per contact
- **OAuth tokens expire** — build automatic refresh logic

### Unthread-Side
- **No native historical import** from external platforms — all ingestion goes through the API
- **Slack channel history backfill** covers 6 months of Slack-native messages only — not useful for Podium data
- **Pagination capped at 100 records** per page on all list endpoints
- **File attachments limited to 20MB** per file and 10 per Slack message
- **Conversation types are fixed** (`slack`, `email`, `triage`) — historical Podium conversations should use `triage`
- **No true custom objects** — custom data lives in Ticket Type Fields, not arbitrary entities
- **No timestamp override** — the public docs do not show fields for backdating imported conversations or messages; original timestamps must be embedded in message body text ([docs.unthread.io](https://docs.unthread.io/docs/api-docs/api-reference))
- **Email connector is forward-only** — Unthread only creates tickets from new inbound email after setup, not historical email
- **Write endpoint rate limits not published** — empirically sustains ~60–90 writes/minute before throttling
- **Create endpoints are not idempotent** — duplicate records will be created if a script is re-run without crosswalk checking
- **Bulk delete not available** via public API — rollback requires record-by-record `DELETE` calls, which at 100 records/page listing + individual deletes can take hours for large datasets

### Structural Data Loss
- Reviews, payments, campaign data, and feedback surveys have no Unthread equivalent — structurally lost if not archived separately
- Podium's channel type granularity (phone, email, secure, Apple Chat, Facebook) collapses into Unthread's three conversation types
- Podium's lead-specific fields (`conversation_first_response_sent_at`, `time_to_convert_seconds`) have no mapping in Unthread
- Historical messages imported via API are accessible in the Unthread web dashboard but **cannot be retroactively injected into Slack channels**
- Original message timestamps are lost — Unthread auto-timestamps at import time; original timestamps can only be preserved as text within message bodies
- If using HubSpot or Salesforce sync, account changes can take up to an hour to reflect inside Unthread ([docs.unthread.io](https://docs.unthread.io/docs/integrations/salesforce))

## Validation and Testing

### Record Count Comparison

After loading, verify counts at each level:

| Entity | Source Count (Podium) | Target Count (Unthread) | Expected Match | Verification Method |
|---|---|---|---|---|
| Accounts/Locations | Count from Locations table | `POST /accounts/list` (paginate all pages) | 1:1 | Automated |
| Customers/Contacts | Count from Contacts table (deduplicated by `contact_uid`) | `POST /customers/list` (paginate all pages) | 1:1 (after dedup) | Automated |
| Conversations | Distinct `conversation_uid` count (within date scope) | `POST /conversations/list` (paginate all pages) | 1:1 (within scope) | Automated |
| Messages | All `conversation_item` rows (within scope) | Sampled via `POST /conversations/:id/messages/list` | 1:1 per conversation | Sample 5% |
| Tags | Distinct tag values | `POST /tags/list` | 1:1 | Automated |
| Attachments | Count from staging DB downloads | Size-check uploaded files | 1:1 (file count + total bytes within 1%) | Automated |

### Field-Level Validation

Sample 5–10% of records across each entity type (minimum 50 records per entity, maximum 500). For each sampled record:
- Compare contact name, email, and phone against source
- Verify conversation status mapping (open/closed)
- Confirm message body text integrity (character-for-character comparison of first 500 chars)
- Check that tag associations are correct
- Validate custom field values in Ticket Type Fields
- Verify attachment accessibility — open files, check sizes match source within 1%
- Confirm `metadata.podium_conversation_uid` matches the source conversation UID

### UAT Process

1. **Pilot migration:** Run the full pipeline against a test Unthread project with 100–500 records. Verify end-to-end before touching production.
2. **Agent walkthrough:** Have 2–3 support agents review 20+ migrated conversations for completeness, readability, and correct attribution.
3. **Edge case spot-check:** Specifically verify multi-message conversations (10+ messages), conversations with attachments, conversations with internal notes, and SMS-only contacts with placeholder emails.
4. **Rollback test:** Verify that migrated records can be deleted via `DELETE` endpoints without affecting native Unthread data. Note: Unthread does not offer bulk delete — deletion is record-by-record. For a 10K conversation migration, full rollback via API takes approximately 3–5 hours at sustained delete rates.

For a comprehensive post-migration testing framework, see our [Post-Migration QA Checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/).

## Post-Migration Tasks

### Rebuild Automations
Podium's automations (review invites, campaign triggers, data feed workflows) do not transfer. Rebuild in Unthread using:
- **Automations API** (`POST /automations`) for event-triggered workflows
- **Ticket Type Fields** with conditions for dynamic form behavior
- **Support Steps** on Customers for escalation paths and SLA enforcement

### Configure Slack Channels
Unthread's primary intake is Slack. After migration:
- Connect customer Slack channels to Unthread Customers
- Configure autoresponder settings per customer
- Set up triage channels for internal routing
- Enable AI agents on appropriate channels

### Update External Integrations
- Update your website widgets and SMS webhooks to point to Unthread instead of Podium
- Reconnect CRM integrations (HubSpot, Salesforce) and verify account/domain matching — note the documented sync lag of up to one hour
- Revoke Podium API tokens and OAuth credentials to secure your legacy environment
- If using Podium for payments or reviews, confirm those systems remain operational independently

### Purge Staging Data
Delete your staging database within 30 days of successful migration completion. If it contains customer PII, treat this as a compliance requirement, not a best practice.

### User Training
Podium users are accustomed to a web/mobile inbox for SMS and webchat. Unthread operates primarily in Slack. Plan for:
- Slack workflow training for agents (1–2 hours)
- Ticket type and custom field usage
- SLA and escalation path awareness
- Explanation of migrated data format (embedded timestamps, `[Migrated]` prefixes, placeholder emails)

### Monitor for Data Inconsistencies
Run validation queries daily for the first two weeks post-migration. Watch for:
- Orphaned conversations (no customer linked)
- Missing messages in long conversation threads (compare message counts per conversation against source)
- Incorrect tag assignments
- Duplicate accounts created by parallel processes
- Placeholder emails being treated as real contact information by agents

## Best Practices

1. **Back up everything.** Export Podium's full FTP dataset and store it in a separate archive (S3, GCS) before starting any migration work. This is your rollback insurance.
2. **Never mutate the source.** Treat Podium data as read-only throughout the migration.
3. **Run a test migration first.** Create a separate Unthread project for testing. Migrate 500 records, validate, iterate on your transformation logic, then run the full migration.
4. **Validate incrementally.** Don't wait until the end to check data quality. Validate after each entity type is loaded (Accounts → Customers → Conversations → Messages).
5. **Preserve source IDs.** Store Podium UIDs in Unthread's `metadata` field on conversations and `customFields` on accounts. This creates an audit trail and simplifies debugging.
6. **Log everything.** Maintain a crosswalk table of `Podium_UID` → `Unthread_ID` for every entity type. If a script fails, you need this table to resume exactly where you left off without creating duplicates.
7. **Handle rate limits by construction.** Build a token bucket or fixed-delay pacer into your script. Reacting to `429` responses with retry logic creates unpredictable throughput. Start at 500ms delay (Podium) and 1s delay (Unthread), then tune.
8. **Set a date cutoff.** Unless there's a compliance reason, migrate only the last 12–24 months of conversations. Older data rarely justifies the engineering cost.
9. **Make creates idempotent.** Since Unthread's API is not natively idempotent, implement crosswalk-based idempotency in your loading script. This is the single most important engineering decision for migration reliability.
10. **Communicate.** Warn your support team that historical tickets will look structurally different — timestamps are embedded in text, conversations are typed as `triage`, and placeholder emails replace phone numbers for SMS-only contacts.

## Sample Data Mapping Table

| Podium Table | Podium Field | Unthread Object | Unthread Field | Notes |
|---|---|---|---|---|
| Contacts | `contact_uid` | Account | `customFields [podium_id]` | String, for traceability |
| Contacts | `contact_name` | Account | `name` | Direct map |
| Contacts | `channel_unique_identifier` (email) | Account | `emailsAndDomains []` | Array |
| Contacts | `contact.phoneNumber` | Account | `customFields [podium_phone]` | No first-class phone field in Unthread customer docs |
| Contacts | `contact_opt_in_tags` | Tag | `name` | Create via `POST /tags`, then link |
| Location and Organizations | `location_name` | Account | `name` | Direct map |
| Location and Organizations | `organization_name` | Account | `customFields [org]` | String |
| Messages | `conversation_uid` | Conversation | `metadata.podium_conv_uid` | For traceability |
| Messages | `conversation_inserted_at` | Conversation | Embedded in `markdown` body | No timestamp override available |
| Messages | `conversation_is_closed` | Conversation | `status` | `true` → `closed` |
| Messages | `conversation_item_body` | Message | `markdown` | Text content; set `isPrivateNote: true` for closed conversations |
| Messages | `conversation_item_sender_name` | Message | `onBehalfOf.name` | For email-type conversations |
| Messages | `conversation_assigned_user_uid` | Conversation | `assignedToUserId` | Map to Unthread user ID; use fallback for departed agents |
| Messages | `inbox_name` | Conversation | `ticketTypeId` | Map to Ticket Type via crosswalk |
| Leads | `conversation_first_webchat_url` | Conversation | `metadata.source_url` | Preserve in metadata |

## Making the Right Call

A Podium-to-Unthread migration is a platform architecture change, not a data copy. You are moving from an SMS-and-reviews platform designed for local businesses to a Slack-native ticketing system designed for B2B and internal support.

If your team has fewer than 1,000 conversations and a developer available for a week, the API approach outlined above is viable. For datasets above 10K conversations, multi-location organizations, or teams that cannot dedicate 80–160 engineer-hours, a managed service will get you there faster and with fewer data integrity risks.

If you only need active contacts and a clean cutover, keep it simple: export contacts, rebuild workflow settings in Unthread, and don't pretend you migrated history when you didn't. If you need real history, the safest pattern is API extraction, immediate attachment capture, staged transformation with crosswalk-based idempotency, closed-conversation import as private notes, then a short webhook delta window before final cutover. If reviews, campaigns, or payments still matter to the business, leave them in Podium or another system of record.

For more migration planning resources, see our [Help Desk Data Migration Checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-checklist/) and [How We Run Migrations at ClonePartner](https://clonepartner.com/blog/blog/how-we-run-migrations-at-clonepartner/).

> Need help migrating from Podium to Unthread? ClonePartner handles the full pipeline — extraction, transformation, loading, and validation — so your team can focus on what matters. Book a 30-minute call to scope your migration.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I migrate Podium conversation history to Unthread with CSV alone?

No. Podium's native CSV export covers contacts only — Name, Phone, Email, Contact Status, Address, Tags, and Date Added. Full conversation history, messages, and attachments require API-based extraction or Podium's FTP raw data export combined with Unthread API ingestion.

### What Podium data cannot be migrated to Unthread?

Reviews, review invites, payment/invoice records, campaign data, and feedback surveys have no Unthread equivalent. These should be archived separately before migration. Podium's lead-specific analytics fields (first response time, conversion time) also have no mapping in Unthread.

### How long does a Podium to Unthread migration take?

For small datasets under 5K conversations, expect 1–2 weeks including planning, scripting, and validation. For mid-size datasets (10K–50K conversations), budget 2–4 weeks with a dedicated developer. Podium's API rate limits are the primary bottleneck for API-based extraction.

### Will historical Podium messages show up in Slack via Unthread?

No. Historical messages imported via API are accessible and searchable within the Unthread web dashboard, but they cannot be retroactively injected into Slack channels. Unthread's Slack backfill only covers 6 months of Slack-native messages.

### Can I run Podium and Unthread in parallel during cutover?

Yes. The common pattern is to complete the historical backfill first via API or FTP, then run a short webhook-based delta sync to capture new records in Podium until you finalize the cutover to Unthread.
