Skip to content

Podium to Freshservice Migration: A Technical Guide

A complete technical guide for migrating data from Podium to Freshservice, covering API constraints, data model mapping, ETL architecture, and edge cases.

Nachi Nachi · · 28 min read
Podium to Freshservice Migration: A Technical Guide
TALK TO AN ENGINEER

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

Migrating from Podium to Freshservice means moving data between two fundamentally different platforms. Podium is a customer interaction platform built around messaging, reviews, and payments for local businesses. Freshservice is an ITIL-aligned IT service management (ITSM) platform designed for internal ticketing, asset management, and change control. There is no native migration path between them, and their data models share almost no structural overlap.

If you're looking for the fastest path from Podium to Freshservice, the answer is a custom API-based ETL pipeline or a managed migration service. CSV exports from Podium cover contacts and campaign data but miss conversation threading, attachments, and relational context. Middleware tools like Zapier can sync individual records but cannot handle bulk historical migration at scale. (support.freshservice.com)

For related platform migrations, see:

Why Teams Move from Podium to Freshservice

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

  • Operational maturity. Podium is designed for external customer messaging — SMS, webchat, and review management. When support operations grow beyond "reply to texts" into structured incident management, SLA enforcement, and change control, Freshservice is the natural destination.
  • ITSM requirements. Podium has no concept of incidents, problems, changes, or releases. Freshservice provides full ITIL lifecycle support with configurable workflows, approval chains, and a built-in CMDB.
  • Consolidation. Teams using Podium alongside a separate ITSM tool often consolidate into Freshservice to reduce tool sprawl, especially after acquisitions or operational restructuring. MSPs and agencies using Podium for client communication frequently outgrow it, needing Freshservice's Department (Company) and Asset (CI) management.
  • SLA management. Podium lacks strict SLA policies, escalation matrices, and routing rules required for mature support operations.

The core architectural difference is state management. Podium conversations are open-ended messaging threads. Freshservice tickets have strict lifecycles (Open → Pending → Resolved → Closed) and require explicit status mapping during migration.

If your main need is review invites, bulk text marketing, or payment collection, Freshservice is not a product substitute for Podium. Treat those as archive or integration scope, not ticket scope. (podium.com)

Core Data Model Differences

Concept Podium Freshservice
Primary object Conversation (messaging thread) Ticket (incident/service request)
Customer record Contact (name, phone, email, tags) Requester (name, email, department, custom fields)
Organizational unit Location → Organization Department → Company (MSP)
Communication SMS, email, webchat messages Ticket replies, notes, conversations
Pipeline/Sales Leads (inbound conversations) No sales pipeline — service-oriented
Reviews/Feedback Review Invites, Reviews, Feedback Surveys No equivalent — archive or discard
Payments Invoices, payment records No equivalent — archive externally
Assets None Full CMDB with asset lifecycle

Podium is location-centric — almost every API action requires a locationUid. Freshservice is requester-centric — tickets are tied to requesters who belong to departments. This structural mismatch is the single biggest challenge in the migration.

Migration Approaches

1. CSV Export + Manual Import

How it works: Export contacts and campaign data from Podium via the admin UI or FTP raw data tables, transform CSVs to match Freshservice's requester import format, then import via Freshservice's CSV upload. (support.freshservice.com)

When to use it: Small datasets (under 1,000 contacts), contact-only migrations with no conversation history needed.

Pros:

  • No coding required
  • Works immediately
  • Good for a quick contact migration

Cons:

  • Podium's CSV exports exclude conversation history, attachments, and threaded messages
  • No relationship preservation (contact ↔ conversation links are lost)
  • Manual field mapping and transformation required
  • Freshservice CSV import for requesters supports limited fields

Complexity: Low Estimated effort: 2–4 hours for under 1,000 contacts. Scalability: Small datasets only. Breaks down past a few thousand records due to manual transformation effort.

Warning

Freshservice custom fields are not included in ticket exports by default. When importing, you must pre-create all custom fields in the Freshservice Field Manager before CSV upload, or values will be silently dropped.

2. API-Based Migration (Podium API → Freshservice API)

How it works: Build a custom extraction pipeline using Podium's REST API (v4, OAuth 2.0) to pull contacts, conversations, messages, and leads. Transform the data to match Freshservice's schema. Load into Freshservice via the v2 REST API using Basic Auth. (docs.podium.com)

Key Podium API v4 endpoints used in extraction:

  • GET /v4/organizations/{orgUid}/locations — enumerate locations
  • GET /v4/contacts?locationUid={uid} — list contacts per location (cursor-paginated)
  • GET /v4/conversations?locationUid={uid} — list conversations per location
  • GET /v4/conversations/{uid}/items — retrieve individual messages within a conversation

When to use it: Any migration that needs conversation history, message threading, or relational integrity. This is the recommended approach for most teams with engineering resources.

Pros:

  • Full fidelity: conversations, messages, timestamps, and contact metadata preserved
  • Relationships maintained through ID mapping
  • Scriptable and repeatable for test runs
  • Complete control over data mapping

Cons:

  • Podium API requires developer account approval (allow 2–5 business days)
  • Podium's message endpoints are capped at 10 requests per minute; most other endpoints have higher but not publicly documented limits
  • Freshservice rate limits vary by plan (100–500 requests/min account-wide)
  • Requires significant engineering effort to build and maintain

Complexity: High Estimated effort: 40–80 engineering hours for a mid-market dataset (10K–50K contacts, 50K–200K messages), including test runs and validation. Scalability: Enterprise-ready with proper batching, pagination, and retry logic.

3. FTP Raw Data Export + ETL Pipeline

How it works: Podium provides raw data access via FTP. Account Owners can request access to download flat-file tables including Contacts, Messages, Leads, Reviews, Review Invites, Feedback, Payments, Campaigns, Campaign Contacts, and Locations. These files feed into a custom ETL pipeline that transforms and loads data into Freshservice.

When to use it: Large-scale migrations where you need complete historical data, including review and campaign history that the REST API may not fully expose.

Pros:

  • Access to all Podium data tables in flat-file format
  • No API rate limit concerns on the extraction side
  • Full historical data including campaigns, reviews, and feedback

Cons:

  • FTP access must be requested by an Account Owner — not self-service
  • Data arrives as denormalized flat files requiring significant transformation
  • Still subject to Freshservice API rate limits on the load side
  • No real-time sync capability

Complexity: Medium–High Estimated effort: 30–60 engineering hours, depending on transformation complexity. Extraction is faster than API-based, but transformation of denormalized flat files adds overhead. Scalability: Excellent for extraction. Load-side throughput depends on Freshservice plan.

4. Middleware Platforms (Zapier, Make)

How it works: Use pre-built Podium and Freshservice connectors to create automated workflows that sync data between platforms. Zapier currently exposes both Podium and Freshservice apps. During research, a public official Podium app was not found in Make's directory, so Podium work in Make may require generic HTTP modules. (zapier.com)

When to use it: Ongoing sync of new contacts or conversations during a transition period. Not suitable for bulk historical migration.

Pros:

  • No-code setup
  • Good for parallel-run periods
  • Handles incremental data flow

Cons:

  • Cannot migrate historical data in bulk
  • Limited field mapping capabilities
  • Per-task pricing becomes expensive at scale (Zapier's Professional plan at $49/month includes 2,000 tasks; at 10K+ records/month, costs escalate quickly with overage pricing)
  • No support for complex relationship rebuilding

Complexity: Low Scalability: Small volumes only.

5. Managed Migration Service

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

When to use it: When internal engineering bandwidth is limited, data volumes are large, or the migration must be zero-downtime.

Pros:

  • No internal engineering burden
  • Expert handling of edge cases (duplicates, orphaned records, data type mismatches)
  • Built-in validation and rollback procedures
  • Typically completed in 3–10 business days depending on dataset size and complexity

Cons:

  • External cost (typical range: $2,000–$15,000+ depending on data volume and complexity)
  • Requires sharing platform credentials (mitigated by scoped API keys and NDAs)

Complexity: Low (for your team) Scalability: Enterprise-ready.

Approach Comparison

Approach Complexity Historical Data Relationships Scale Cost Estimated Duration
CSV Export/Import Low Contacts only Lost Small Free 2–4 hours
API-Based ETL High Full Preserved Enterprise 40–80 eng hours 1–3 weeks
FTP Raw Data + ETL Medium–High Full + analytics Requires rebuild Enterprise 30–60 eng hours 1–2 weeks
Middleware (Zapier/Make) Low No historical Partial Small $49–$200+/month Ongoing
Managed Service Low (your side) Full Preserved Enterprise $2K–$15K+ 3–10 business days

Recommendations by scenario:

  • Small business, contacts only: CSV export/import.
  • Mid-market, full history needed, has dev team: API-based ETL.
  • Enterprise, large dataset, no dev bandwidth: Managed migration service.
  • Transition period, ongoing sync: Middleware for new records plus API or managed service for historical backfill.

When to Use a Managed Migration Service

Build in-house when you have dedicated engineers who understand both APIs, time to run multiple test migrations, and a small enough dataset that rate limits won't create multi-day load windows.

Choose a managed service when:

  • Engineering bandwidth is the bottleneck. Your team is shipping product, not writing one-off migration scripts.
  • Data relationships are complex. Podium's location-centric model and polymorphic contact identifiers (conversation UID, email, or phone) create mapping challenges that multiply with scale.
  • Zero downtime is non-negotiable. A managed service can run delta syncs during cutover, keeping both systems live until the switch.
  • You can't afford a failed migration. Duplicate requesters, orphaned tickets, and broken conversation threads are expensive to fix after go-live.

The hidden cost of DIY migration is not the initial build — it's the debugging. Podium's API returns contacts that can be identified by phone number, email, or conversation UID interchangeably. If your script doesn't deduplicate across these identifiers, you end up with duplicate requesters in Freshservice that break ticket routing and reporting.

Freshservice explicitly notes that its public APIs are primarily intended for integrations, not large-volume migration traffic. It also documents a gated bulk migration path that requires a migration token and only covers tickets and notes. (support.freshservice.com)

ClonePartner specializes in complex platform migrations with mismatched data models — including helpdesk, CRM, and ITSM systems. For a Podium-to-Freshservice move, that means handling location-to-department mapping, conversation-to-ticket threading, and contact deduplication across polymorphic identifiers, delivered in days with full validation.

Pre-Migration Planning

A successful migration is won in the planning phase.

Data Audit

Before extracting anything, inventory what exists in Podium and what needs to move:

Podium Object Typical Volume Migrate? Notes
Contacts 100s–100,000s Yes Core records — map to Freshservice Requesters
Conversations/Messages 1,000s–1,000,000s Selective Map active/recent conversations to Tickets
Leads 100s–10,000s Selective Map to Tickets with category tag
Reviews 100s–10,000s Archive only No Freshservice equivalent
Review Invites 100s–10,000s Archive only No Freshservice equivalent
Feedback/Surveys 100s–1,000s Archive only Export for records, don't migrate
Payments 100s–10,000s Archive only No Freshservice equivalent
Campaigns 10s–100s Archive only Rebuild marketing in another tool
Campaign Contacts 100s–100,000s Archive only Opt-in/opt-out records for compliance
Locations/Organizations 1–100s Yes Map to Departments
Tip

Archive reviews, payments, and campaign data to a data warehouse or CSV backup before migration. This data has no target in Freshservice, but you may need it for compliance or analytics.

Define Migration Scope

  1. What moves: Contacts, active conversations, leads, locations.
  2. What gets archived: Reviews, payments, campaigns, feedback.
  3. What gets rebuilt: Automations, workflows, canned responses, SLAs.
  4. Cutover strategy: Big bang (single switch) vs. phased (contacts first, then conversations) vs. parallel run (both systems live during transition).

Security and Compliance Considerations

Migration scripts handle credentials and customer data across two platforms. Implement these controls:

  • Credential management. Store Podium OAuth tokens and Freshservice API keys in a secrets manager (AWS Secrets Manager, HashiCorp Vault, or environment variables — never hardcode in scripts or commit to source control).
  • Encryption in transit. Both Podium API (HTTPS) and Freshservice API (HTTPS) enforce TLS. If using FTP for Podium raw data export, confirm SFTP is available; if not, transfer files over a VPN or encrypted tunnel.
  • Data residency. Confirm where Podium and Freshservice store data. Freshservice offers US, EU, IN, and AU data centers. If your migration staging database is on a different continent from either platform, you may introduce data residency compliance issues under GDPR or SOC 2.
  • Audit logging. Log every API call with timestamp, source ID, target ID, and HTTP status. This log serves as both a debugging artifact and a compliance record.
  • Least-privilege access. Use scoped API keys. In Freshservice, create a dedicated migration agent with admin permissions, then revoke after cutover. In Podium, request OAuth scopes limited to read-only access for the entities you need.
  • TCPA/GDPR. Podium tracks marketing opt-in/opt-out timestamps. Preserve these in Freshservice custom requester fields or an external compliance database. Failing to carry opt-out records forward can create legal exposure.

Risk Mitigation

  • Backup everything from Podium before starting. Request FTP access as a safety net.
  • Run at least two test migrations against a Freshservice sandbox before production. Freshservice sandboxes are available on Pro and Enterprise plans. If you're on Growth or Starter, use a separate trial instance for testing.
  • Document the ID mapping table (Podium UID → Freshservice ID) for every migrated record.
  • Set up a rollback plan: Freshservice does not support easy bulk deletion via API. Tag all migrated records with podium-migration during import so you can filter and delete if validation fails.

For broader cutover prep, use our help desk migration checklist and pair launch-week validation with the post-migration QA checklist.

Data Model and Object Mapping

This is where Podium-to-Freshservice migrations get hard. The platforms don't share a common schema.

Contacts → Requesters

Podium contacts are identified by a combination of contact_uid, phone number, and email — any of which can be used as the primary identifier depending on the API endpoint. Freshservice requesters require a unique email address. (docs.podium.com)

Key challenges:

  • Podium contacts may have phone numbers but no email. Freshservice strongly prefers email for requester identification and ticket routing. For phone-only contacts, generate a placeholder email (e.g., +15551234567@podium-migrated.internal) or use the phone number field and accept limited functionality.
  • Podium stores contacts per location. A single customer interacting with multiple locations appears as separate contact records. Deduplicate by email or phone number before loading into Freshservice.
  • Freshservice email uniqueness is enforced across both agents and requesters. If a Podium contact's email already exists as an agent in Freshservice, the requester creation call will fail silently or return a non-obvious error. Pre-check the agent list before bulk importing.

Locations/Organizations → Departments

Podium's location hierarchy (Organization → Location) maps conceptually to Freshservice's Department structure. Each Podium location becomes a Freshservice department.

For Freshservice MSP accounts, Podium Organizations map to Freshservice Clients (using workspace_id), and Locations map to Departments within each client workspace.

Conversations → Tickets

This is the most complex mapping. A Podium conversation is a messaging thread between a business location and a contact, potentially spanning days or weeks across SMS, email, and webchat. A Freshservice ticket is a structured service request with defined status, priority, and category.

Mapping decisions:

  • Each Podium conversation becomes one Freshservice ticket.
  • The first message becomes the ticket description; subsequent messages become ticket replies (public) or notes (private).
  • conversation_channel_type maps to the ticket source field (email = 1, portal = 2, phone = 3).
  • conversation_is_closed maps to ticket status (closed = 5, open = 2).
  • conversation_assigned_user_uid maps to agent_id after building an agent mapping table.

Leads → Tickets (Tagged)

Podium leads are inbound-initiated conversations. In Freshservice, these become tickets with a specific category or tag (e.g., category: "Lead" or tag: ["podium-lead"]). There is no sales pipeline concept in Freshservice.

Opportunities → Custom Objects or Archive

Freshservice is not a CRM pipeline. Podium's public API documentation is contact/conversation-centric, not opportunity-centric. If your Podium account has opportunity-like data, you have two options: map it to Freshservice Custom Objects (available on Enterprise plans), or archive it externally. Do not force sales pipeline data into ticket queues.

Info

Freshservice supports Custom Objects on Enterprise plans, but they are narrower than a CRM schema. Identity fields must be text, lookups are constrained, and paragraph fields cannot be used as workflow filters. Plan capacity before adding Podium-specific metadata fields. (support.freshservice.com)

Reviews, Feedback, Payments → No Migration Target

Freshservice has no equivalent objects. Export these to CSV or a data warehouse for archival. Do not attempt to shoehorn review data into Freshservice tickets — reviews are not support requests. The same applies to payment and campaign data.

Sample Data Mapping Table

Podium Field Freshservice Field Transformation
contact_uid requester.id (auto-generated) Store mapping: Podium UID → FS ID in custom field podium_contact_uid
contact_name requester.first_name, requester.last_name Split on first space; handle single-name contacts
channel_unique_identifier (phone) requester.mobile_phone_number Normalize to E.164 format
channel_unique_identifier (email) requester.primary_email Lowercase, trim whitespace, deduplicate
organization_name / location_name department.name Create departments before requesters
conversation_uid ticket custom field podium_conversation_uid Store mapping for reruns
conversation_item_body ticket.description (first) / ticket.note.body (rest) HTML-encode if needed
conversation_inserted_at ticket.created_at ISO 8601 — must be explicitly passed in API payload
conversation_is_closed ticket.status true → 5 (Closed), false → 2 (Open)
conversation_channel_type ticket.source phone → 3, email → 1, webchat/secure → 2
conversation_assigned_user_uid ticket.agent_id Requires agent UID → Freshservice agent ID mapping
contact_channel_marketing_opted_in_source Custom requester field Preserve for TCPA/GDPR compliance
review_rating N/A Archive — no Freshservice equivalent
invoice_amount_dollars N/A Archive — no Freshservice equivalent

Migration Architecture

Data Flow: Extract → Transform → Load

┌─────────────┐     ┌──────────────┐     ┌────────────────┐
│   Podium     │     │  Transform   │     │  Freshservice  │
│              │     │              │     │                │
│ REST API v4  │────▶│  Mapping &   │────▶│  REST API v2   │
│ (OAuth 2.0)  │     │  Dedup &     │     │  (Basic Auth)  │
│   — or —     │     │  Validation  │     │                │
│ FTP Raw Data │     │              │     │                │
└─────────────┘     └──────────────┘     └────────────────┘

Podium API Constraints

  • Authentication: OAuth 2.0 with developer account approval (may take 2–5 business days). Register at developer.podium.com. (docs.podium.com)
  • Rate limits: Most endpoints are rate-limited to approximately 300 requests per minute. Message sending endpoints are capped at 10 requests per minute. (docs.podium.com)
  • Pagination: Cursor-based for most list endpoints. Handle next_cursor tokens to iterate through large datasets.
  • Location scoping: Nearly every query requires a locationUid. Enumerate locations first.
  • No bulk export API: There is no single endpoint to dump all data. You must paginate through each entity per location.
  • API versioning: Podium's API is currently at v4. No public deprecation schedule or versioning policy is documented. If your migration spans multiple weeks, verify endpoint stability at the start of each phase.

Freshservice API Constraints

  • Authentication: Basic Auth with API key (recommended for server-to-server).
  • Rate limits by plan: Starter: 100/min, Growth: 200/min, Pro: 400/min, Enterprise: 500/min. Add-on packs available up to 2,000/min. These limits are account-wide, regardless of how many agents or IP addresses make calls. (api.freshservice.com)
  • Rate limit handling: Freshservice returns 429 Too Many Requests when limits are breached. Read the Retry-After header and pause execution.
  • Pagination: Page-based, max 100 records per page.
  • Email uniqueness: Requester emails must be globally unique across both agents and requesters in the same account.
  • Bulk migration APIs: Freshservice documents a gated bulk migration path for migration partners that requires a migration token and only covers tickets and notes. (support.freshservice.com)

Common API Error Codes and Handling Strategies

HTTP Status Source Meaning Handling Strategy
400 Freshservice Validation error (missing required field, invalid format) Log payload, fix field mapping, retry
401 Either Authentication failure Refresh OAuth token (Podium) or verify API key (Freshservice)
403 Either Insufficient permissions or scope Check OAuth scopes (Podium) or agent role (Freshservice)
404 Either Resource not found Log and skip — may indicate deleted record on source side
409 Freshservice Conflict (duplicate email on requester creation) Look up existing requester by email, use existing ID in mapping
422 Freshservice Unprocessable entity (e.g., custom field value doesn't match dropdown options) Validate against allowed values before submission
429 Either Rate limit exceeded Read Retry-After header, exponential backoff, resume from checkpoint
500/502/503 Either Server error Retry with exponential backoff (max 3 retries), then log and skip
Danger

Freshservice email uniqueness is enforced across both agents and requesters. If a Podium contact's email already exists as an agent in Freshservice, the requester creation call will fail silently or return a non-obvious error. Pre-check the agent list before bulk importing requesters.

Warning

Freshservice's partner bulk APIs do not turn off workflows for you. Some end-user notifications are suppressed, but workflow rules still run unless you guard them first. (support.freshservice.com)

Handling Large Datasets

  • Batch extractions: Pull data per-location from Podium to avoid timeout issues.
  • Intermediate staging: Extract all Podium data into a local database (PostgreSQL) or structured JSON files. Perform transformations locally to prevent network instability from breaking the transformation logic.
  • Queue-based loading: Use a job queue (Redis, SQS) to manage Freshservice API writes with rate-limit-aware backoff.
  • Checkpoint and resume: Log every successful write with source ID → target ID mapping. If the script crashes, resume from the last checkpoint.

Loading Time Estimates by Dataset Size and Freshservice Plan

Dataset Size Starter (100/min) Growth (200/min) Pro (400/min) Enterprise (500/min)
1,000 records ~10 min ~5 min ~3 min ~2 min
10,000 records ~1.7 hrs ~50 min ~25 min ~20 min
50,000 records ~8.3 hrs ~4.2 hrs ~2.1 hrs ~1.7 hrs
100,000 records ~16.7 hrs ~8.3 hrs ~4.2 hrs ~3.3 hrs
500,000 records ~3.5 days ~1.7 days ~21 hrs ~16.7 hrs

Assumes 1 API call per record. Actual load times will be higher due to retry logic, rate limit pauses, and multi-call entities (tickets + notes). Multiply by 1.3–1.5× for realistic estimates.

Step-by-Step Migration Process

Step 1: Extract Data from Podium

Option A — API extraction:

import requests
import time
 
BASE_URL = "https://api.podium.com/v4"
HEADERS = {
    "Authorization": "Bearer <ACCESS_TOKEN>",
    "Content-Type": "application/json"
}
 
def get_locations(org_uid):
    resp = requests.get(f"{BASE_URL}/organizations/{org_uid}/locations", headers=HEADERS)
    resp.raise_for_status()
    return resp.json()["data"]
 
def get_contacts(location_uid, cursor=None):
    params = {"locationUid": location_uid}
    if cursor:
        params["cursor"] = cursor
    resp = requests.get(f"{BASE_URL}/contacts", headers=HEADERS, params=params)
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return get_contacts(location_uid, cursor)
    resp.raise_for_status()
    return resp.json()
 
def get_conversations(location_uid, cursor=None):
    params = {"locationUid": location_uid}
    if cursor:
        params["cursor"] = cursor
    resp = requests.get(f"{BASE_URL}/conversations", headers=HEADERS, params=params)
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return get_conversations(location_uid, cursor)
    resp.raise_for_status()
    return resp.json()
 
def get_conversation_items(conversation_uid, cursor=None):
    params = {}
    if cursor:
        params["cursor"] = cursor
    resp = requests.get(
        f"{BASE_URL}/conversations/{conversation_uid}/items",
        headers=HEADERS, params=params
    )
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return get_conversation_items(conversation_uid, cursor)
    resp.raise_for_status()
    return resp.json()

Option B — FTP raw data: Request FTP access from your Podium Account Owner. Download all available tables (Contacts, Messages, Leads, Conversations, Reviews, etc.) as flat files. FTP access is not self-service — it requires Account Owner involvement.

Danger

Podium message attachment URLs expire after 7 days. Download attachment binaries during extraction, not after mapping QA. (docs.podium.com)

Step 2: Transform Data

Map the Podium schema to Freshservice's ITSM schema. Deduplicate contacts across locations before loading.

def transform_contact_to_requester(podium_contact, dept_map):
    name_parts = (podium_contact.get("name") or "Unknown").split(" ", 1)
    email = podium_contact.get("email")
    phone = podium_contact.get("phone")
 
    # Generate placeholder email for phone-only contacts
    if not email and phone:
        email = f"{phone.replace('+', '')}@podium-migrated.internal"
 
    return {
        "first_name": name_parts[0],
        "last_name": name_parts[1] if len(name_parts) > 1 else "",
        "primary_email": email.lower().strip() if email else None,
        "mobile_phone_number": phone,
        "department_ids": [dept_map.get(podium_contact.get("locationUid"))],
        "custom_fields": {
            "podium_uid": podium_contact.get("uid"),
            "podium_location": podium_contact.get("locationName")
        }
    }
 
def transform_conversation_to_ticket(podium_convo, requester_map):
    requester_id = requester_map.get(podium_convo["contact"]["uid"])
 
    source_map = {"phone": 3, "email": 1, "webchat": 2, "secure": 2}
    source = source_map.get(podium_convo.get("channelType"), 2)
 
    return {
        "requester_id": requester_id,
        "subject": f"Podium conversation {podium_convo['uid'][:8]}",
        "description": podium_convo.get("firstMessage", "Migrated from Podium"),
        "status": 5 if podium_convo.get("isClosed") else 2,
        "priority": 1,
        "source": source,
        "created_at": podium_convo.get("insertedAt"),  # ISO 8601 — preserves historical timestamp
        "tags": ["podium-migration"],
        "custom_fields": {
            "podium_conversation_uid": podium_convo["uid"]
        }
    }

Step 3: Load into Freshservice

Create departments first, then requesters, then tickets, then conversation items as notes. Maintain an ID mapping table throughout.

FS_BASE = "https://yourdomain.freshservice.com/api/v2"
FS_AUTH = ("<API_KEY>", "X")  # API key as username, any string as password
 
def create_requester(requester_data):
    resp = requests.post(
        f"{FS_BASE}/requesters",
        json=requester_data,
        auth=FS_AUTH,
        headers={"Content-Type": "application/json"}
    )
    if resp.status_code == 429:
        time.sleep(int(resp.headers.get("Retry-After", 60)))
        return create_requester(requester_data)
    if resp.status_code == 409:
        # Duplicate email — look up existing requester
        email = requester_data.get("primary_email")
        lookup = requests.get(
            f"{FS_BASE}/requesters?email={email}",
            auth=FS_AUTH
        )
        if lookup.status_code == 200 and lookup.json().get("requesters"):
            return lookup.json()["requesters"][0]["id"]
    resp.raise_for_status()
    return resp.json()["requester"]["id"]
 
def create_ticket(ticket_data):
    resp = requests.post(
        f"{FS_BASE}/tickets",
        json=ticket_data,
        auth=FS_AUTH,
        headers={"Content-Type": "application/json"}
    )
    if resp.status_code == 429:
        time.sleep(int(resp.headers.get("Retry-After", 60)))
        return create_ticket(ticket_data)
    resp.raise_for_status()
    return resp.json()["ticket"]["id"]
 
def add_ticket_note(ticket_id, note_data):
    resp = requests.post(
        f"{FS_BASE}/tickets/{ticket_id}/notes",
        json=note_data,
        auth=FS_AUTH,
        headers={"Content-Type": "application/json"}
    )
    if resp.status_code == 429:
        time.sleep(int(resp.headers.get("Retry-After", 60)))
        return add_ticket_note(ticket_id, note_data)
    resp.raise_for_status()
    return resp.json()

Step 4: Rebuild Relationships

After loading requesters and tickets:

  1. Add conversation items as ticket replies/notes using POST /api/v2/tickets/{id}/notes. Use conversation_item_inserted_at timestamps to preserve chronological order.
  2. Associate requesters with departments using PUT /api/v2/requesters/{id} with department_ids.
  3. Map Podium user assignments to Freshservice agent assignments.

Step 5: Validate

Run validation queries against both systems to confirm record counts, field-level accuracy, and relationship integrity. See the Validation section below.

Full Script Outline

A production migration script typically follows this structure:

# migration.py — Podium to Freshservice Migration
import logging
import json
from podium_client import PodiumAPI
from freshservice_client import FreshserviceAPI
from transformer import Transformer
from validator import Validator
 
logging.basicConfig(level=logging.INFO, filename="migration.log")
 
# Persistent mapping tables — write to disk for checkpoint/resume
MAPPING_FILE = "id_mappings.json"
 
def load_mappings():
    try:
        with open(MAPPING_FILE, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        return {"departments": {}, "requesters": {}, "tickets": {}}
 
def save_mappings(mappings):
    with open(MAPPING_FILE, "w") as f:
        json.dump(mappings, f)
 
def main():
    podium = PodiumAPI(client_id="...", client_secret="...", access_token="...")
    fs = FreshserviceAPI(domain="yourdomain", api_key="...")
    transformer = Transformer()
    validator = Validator()
    mappings = load_mappings()
 
    # Phase 1: Extract and create departments
    locations = podium.get_locations()
    dept_map = mappings["departments"]
    for loc in locations:
        if loc["uid"] in dept_map:
            continue  # Already migrated — checkpoint resume
        dept = fs.create_department({"name": loc["name"], "description": loc["address"]})
        dept_map[loc["uid"]] = dept["id"]
        save_mappings(mappings)
        logging.info(f"Dept created: {loc['uid']} -> {dept['id']}")
 
    # Phase 2: Extract, deduplicate, and create requesters
    all_contacts = []
    for loc_uid in dept_map:
        contacts = podium.get_all_contacts(loc_uid)
        all_contacts.extend(contacts)
 
    deduped = transformer.deduplicate_contacts(all_contacts)
    requester_map = mappings["requesters"]
    for contact in deduped:
        if contact["uid"] in requester_map:
            continue
        req_data = transformer.contact_to_requester(contact, dept_map)
        try:
            req_id = fs.create_requester(req_data)
            requester_map[contact["uid"]] = req_id
            save_mappings(mappings)
        except Exception as e:
            logging.error(f"Failed: {contact['uid']}{e}")
 
    # Phase 3: Extract and create tickets from conversations
    ticket_map = mappings["tickets"]
    for loc_uid in dept_map:
        conversations = podium.get_all_conversations(loc_uid)
        for convo in conversations:
            if convo["uid"] in ticket_map:
                continue
            ticket_data = transformer.conversation_to_ticket(convo, requester_map)
            try:
                ticket_id = fs.create_ticket(ticket_data)
                ticket_map[convo["uid"]] = ticket_id
                save_mappings(mappings)
                for item in convo.get("items", []):
                    note_data = transformer.message_to_note(item)
                    fs.add_ticket_note(ticket_id, note_data)
            except Exception as e:
                logging.error(f"Failed: {convo['uid']}{e}")
 
    # Phase 4: Validate
    validator.compare_counts(podium, fs)
    validator.sample_field_check(requester_map, podium, fs)
 
if __name__ == "__main__":
    main()

The key architectural decisions: ordered dependency loading, per-record error handling with logging, checkpoint/resume via persistent mapping files, and a persistent mapping table for relationship reconstruction.

Delta Sync for Zero-Downtime Cutover

If you need both Podium and Freshservice running simultaneously during transition, you need a delta sync strategy to capture records created or modified in Podium after the initial bulk extraction.

Approach 1: Webhook-Based Delta Sync

Podium supports webhooks for events including new messages, new contacts, and conversation status changes. Configure a webhook consumer that:

  1. Receives Podium webhook events at an HTTPS endpoint
  2. Transforms incoming records using the same mapping logic as the bulk migration
  3. Creates or updates corresponding records in Freshservice

Caveats:

  • Podium webhook events sit in queue for approximately 10 days before expiring. If your consumer is down for more than 10 days, you will lose events. (docs.podium.com)
  • Webhook response timeout is 5 seconds. Your consumer must acknowledge receipt quickly and process asynchronously (e.g., write to a queue, then process from queue).
  • Webhook payloads may not include the full record. You may need to make follow-up API calls to Podium to fetch complete data.

Approach 2: Polling-Based Delta Sync

If webhook reliability is a concern, poll Podium's API at regular intervals (e.g., every 5–15 minutes) for records modified since the last poll. Use updated_after or cursor-based filtering to identify new or changed records.

Implementation pattern:

import datetime
 
LAST_SYNC_FILE = "last_sync_timestamp.txt"
 
def get_last_sync():
    try:
        with open(LAST_SYNC_FILE, "r") as f:
            return f.read().strip()
    except FileNotFoundError:
        return datetime.datetime(2020, 1, 1).isoformat()
 
def save_last_sync(timestamp):
    with open(LAST_SYNC_FILE, "w") as f:
        f.write(timestamp)
 
def delta_sync(podium, fs, transformer, mappings):
    since = get_last_sync()
    for loc_uid in mappings["departments"]:
        new_contacts = podium.get_contacts_since(loc_uid, since)
        for contact in new_contacts:
            if contact["uid"] not in mappings["requesters"]:
                req_data = transformer.contact_to_requester(contact, mappings["departments"])
                req_id = fs.create_requester(req_data)
                mappings["requesters"][contact["uid"]] = req_id
 
        new_convos = podium.get_conversations_since(loc_uid, since)
        for convo in new_convos:
            if convo["uid"] not in mappings["tickets"]:
                ticket_data = transformer.conversation_to_ticket(convo, mappings["requesters"])
                ticket_id = fs.create_ticket(ticket_data)
                mappings["tickets"][convo["uid"]] = ticket_id
 
    save_last_sync(datetime.datetime.utcnow().isoformat())

Trade-offs: Polling is more reliable than webhooks but introduces latency (records appear in Freshservice 5–15 minutes after creation in Podium). It also consumes API quota continuously. At 4 polls/hour across 10 locations, budget approximately 40 API calls/hour just for delta checks.

Cutover Sequence

  1. Run bulk migration (initial load)
  2. Start delta sync (webhook or polling)
  3. Parallel-run for 1–2 weeks — agents use both systems
  4. Validate record parity between systems
  5. Cut over: redirect all new requests to Freshservice
  6. Stop delta sync
  7. Set Podium to read-only for 30–90 days (historical reference)
  8. Decommission Podium

Edge Cases and Challenges

Duplicate Contacts Across Locations

Podium stores contacts per location. A customer who visits two business locations appears twice in Podium's data. Loading both records into Freshservice creates duplicate requesters that break ticket routing and reporting. Solution: Deduplicate by email or phone number before loading. Choose the most recently active record as the primary. Podium also documents contact merge events — account for these in your dedup logic. (docs.podium.com)

Contacts Without Email Addresses

Podium is SMS-first. Many contacts have only a phone number. Freshservice strongly prefers email for requester identification and ticket routing. Solution: For phone-only contacts, generate a placeholder email (e.g., +15551234567@podium-migrated.internal) or use the phone number as the primary identifier and accept limited functionality in Freshservice.

Polymorphic Contact Identifiers

Podium's API allows contact lookup by conversation UID, email, or phone number interchangeably. Your extraction script must normalize all three identifier types to a single canonical key before creating the Freshservice requester. Failure to do this is one of the most common causes of duplicate requesters.

Multi-Message Conversations

A single Podium conversation can contain dozens or hundreds of messages. Each message becomes a note or reply on the corresponding Freshservice ticket. Ordering matters — use conversation_item_inserted_at timestamps to preserve chronology.

Attachments

Podium conversation items may include attachments. The FTP raw data export includes metadata but not the actual attachment files. To migrate attachments, use the API to download them, then upload via Freshservice's multipart/form-data ticket attachment endpoint. You cannot simply pass a Podium URL — the source URLs expire after 7 days, and the Podium instance may be deprecated after migration.

Freshservice ticket and note attachments are capped at 40 MB total per ticket or entity path. The bulk partner API uses public URLs rather than multipart upload. (docs.podium.com)

Multi-Location Contacts in Freshservice

If a Podium Contact belongs to multiple Locations, Freshservice requires specific configuration. Requesters typically belong to a single primary Department unless secondary departments are explicitly enabled.

Marketing Opt-In/Opt-Out Data

Podium tracks granular opt-in and opt-out timestamps for marketing communications. Freshservice has no marketing module. Store this data in custom requester fields or archive externally for TCPA/GDPR compliance.

Webhook Edge Cases

Podium webhook events sit in queue for about 10 days and responses time out after 5 seconds. Do not rely on a fragile synchronous consumer for your delta sync. (docs.podium.com)

Limitations and Constraints

Freshservice Limitations

  • Rigid ITSM structure. You will lose the fluid, CRM-like feel of Podium. Freshservice forces a structured ticket lifecycle.
  • API rate limits are account-wide. A Starter plan at 100 requests/min means approximately 6,000 records/hour (assuming one API call per record). For 100K records, that's roughly 16 hours of continuous loading. See the loading time estimates table above for all plan tiers.
  • Custom field limits. Freshservice limits the number of custom fields per module by plan tier. Pre-plan capacity before adding Podium-specific metadata fields.
  • Timestamp handling. Freshservice allows setting created_at timestamps via the API for historical accuracy, but this must be explicitly passed in the payload. Otherwise, all tickets will appear as created on the day of migration. Verify historical timestamp preservation in a test run.
  • Bulk migration APIs are gated. They cover tickets and notes only and require a migration token.
  • Field sanitization. Freshservice sanitizes some user and organization fields, which can change punctuation or special characters during import. (api.freshservice.com)
  • Sandbox availability. Freshservice sandboxes are available on Pro and Enterprise plans only. Growth and Starter plans must use a separate trial account for test migrations.

Podium Limitations

  • Developer account approval: Not instant. Plan for 2–5 business days.
  • FTP access is request-only: Requires Account Owner involvement.
  • No bulk export API: There is no single endpoint to dump all data. You must paginate through each entity per location.
  • Conversation history depth: The API may not expose the full history for very old conversations. Verify coverage before committing to API-only extraction.
  • Attachment URL expiration: Message attachment URLs expire after 7 days.
  • No public deprecation policy: Podium's API is currently v4 with no documented versioning or sunset schedule. If your migration spans multiple weeks, re-verify endpoint availability periodically.

Data That Will Not Migrate

  • Reviews and feedback: No Freshservice equivalent. Archive separately.
  • Payment records: No Freshservice equivalent.
  • Campaign analytics: Campaign interaction data (sent, failed, opted out) has no ITSM equivalent.
  • Conversation-level analytics: Webchat URLs, lead conversion timestamps, and first-response-time metrics are Podium-specific and don't map to Freshservice fields.

Validation and Testing

Do not assume a 201 Created response means the data is correct.

Record Count Comparison

Entity Podium Count Freshservice Count Expected Delta
Contacts / Requesters X Y Should be ≤ Podium (deduplication reduces count)
Conversations / Tickets X Y Should match 1:1
Messages / Ticket conversations X Y Should match 1:1
Locations / Departments X Y Should match 1:1

Field-Level Validation

Sample at least 5% of migrated records (minimum 50 records) and compare field-by-field:

  • Contact name → Requester first_name + last_name
  • Phone number format (E.164)
  • Email case normalization
  • Department assignment
  • Ticket status mapping (open/closed)
  • Conversation chronological order
  • created_at timestamp accuracy (compare Podium insertedAt vs. Freshservice created_at)

UAT Process

  1. Agent walk-through: Have 2–3 agents open migrated tickets and verify conversation history is readable and correctly ordered.
  2. Search test: Search for 10 known contacts by name, email, and phone in Freshservice. Confirm all are findable.
  3. Workflow test: Trigger a test ticket creation and confirm automations (assignment rules, SLA timers) work correctly with migrated data.
  4. Report validation: Run a Freshservice report on migrated tickets. Verify counts match expectations.
  5. Edge case spot-check: Specifically verify: phone-only contacts (placeholder emails), multi-location contacts (single requester), and conversations with 50+ messages (note ordering).

Rollback Planning

Freshservice does not support bulk deletion via API easily. If the migration fails validation:

  • Tag all migrated records with podium-migration during import.
  • Use API filters to identify and delete tagged records.
  • Use a Freshservice sandbox for test migrations and only proceed to production after validation passes.

Post-Migration Tasks

Rebuild Automations

Podium automations (auto-responses, review invite triggers, lead assignments) have no migration path. Rebuild equivalent workflows in Freshservice's Workflow Automator:

  • Auto-assignment rules based on department or category.
  • SLA policies to replace Podium's first-response-time tracking.
  • Notification rules for ticket updates.
Warning

If you did not disable workflow automations before loading migrated data, check for unintended notifications or auto-assignments triggered by the import.

User Training

Podium users are accustomed to a messaging-first interface. Freshservice is a structured ticketing system. Key training areas:

  • Ticket lifecycle (Open → Pending → Resolved → Closed)
  • Using the self-service portal vs. SMS-based communication
  • ITIL concepts (incidents vs. service requests) if new to the team
  • How historical Podium context appears inside notes or custom fields

Monitor for Data Inconsistencies

For the first 2–4 weeks post-migration:

  • Monitor for duplicate requester creation (may indicate incomplete dedup).
  • Check for tickets without requester association (orphaned records).
  • Verify that ticket conversations appear in correct chronological order.
  • Keep Podium in read-only mode for 30–90 days to allow agents to verify historical context if a mapped record looks incorrect.

Best Practices

  1. Back up everything before migration. Export all Podium data via FTP and store offline.
  2. Run test migrations. Use a Freshservice sandbox (Pro/Enterprise) or trial instance (Growth/Starter). Run at least two full test cycles before production.
  3. Migrate in dependency order. Departments → Requesters → Tickets → Conversation items. Dependencies must exist before dependent records are loaded.
  4. Preserve source IDs. Store Podium UIDs in Freshservice custom fields for traceability and debugging.
  5. Tag everything. Apply a podium-migration tag to all migrated tickets for filtering and rollback.
  6. Validate incrementally. Check counts and field accuracy after each entity type, not just at the end.
  7. Plan for what doesn't migrate. Explicitly document which Podium data types are archived vs. discarded. Get stakeholder sign-off.
  8. Guard your workflows. Disable or scope Freshservice workflow automations before loading migrated data to prevent unintended notifications.
  9. Download attachments immediately. Podium attachment URLs expire after 7 days. Don't defer this to a later step.
  10. Secure your pipeline. Store API credentials in a secrets manager, use HTTPS/SFTP for all data transfer, and maintain an audit log of every API call for compliance.

Making the Call

A Podium-to-Freshservice migration is not a standard helpdesk-to-helpdesk move. You're crossing architectural boundaries — from a messaging-first customer interaction platform to a structured ITSM system. The data models don't align natively, there's no vendor-provided migration tool, and significant data categories (reviews, payments, campaigns) simply have no destination in Freshservice.

For small teams with under 1,000 contacts and no conversation history needs, a CSV export gets the job done in an afternoon. For anything larger — especially when you need threaded conversation history, location-to-department mapping, and contact deduplication across polymorphic identifiers — you need either a dedicated engineering effort (budget 40–80 hours) or a migration partner who has handled this kind of cross-platform mismatch before.

For a full breakdown of what to migrate and what to leave behind, see the Help Desk Data Migration Playbook. For zero-downtime cutover strategies, see our zero-downtime migration guide.

Frequently Asked Questions

Can I migrate Podium conversations to Freshservice tickets?
Yes, but only through the Podium REST API or FTP raw data export. Each Podium conversation maps to one Freshservice ticket, with the first message becoming the ticket description and subsequent messages becoming ticket notes or replies. CSV exports do not include conversation history.
What Podium data cannot be migrated to Freshservice?
Reviews, review invites, feedback surveys, payment records, and campaign analytics have no equivalent objects in Freshservice. These should be archived to CSV or a data warehouse before migration.
What are the Freshservice API rate limits for data migration?
Freshservice enforces account-wide minute-level rate limits: 100/min on Starter, 200/min on Growth, 400/min on Pro, and 500/min on Enterprise. Add-on packs can increase limits up to 2,000/min. A separate gated bulk migration API exists for migration partners but only covers tickets and notes.
How do I handle Podium contacts with only phone numbers in Freshservice?
Freshservice strongly prefers email for requester identification and ticket routing. For phone-only Podium contacts, generate a placeholder email (e.g., +15551234567@podium-migrated.internal) or use the phone number field and accept limited ticket routing functionality.
How long does a Podium to Freshservice migration take?
For contacts-only via CSV, a few hours. For full API-based migration with conversations, expect 2–5 days of engineering work for script development plus load time dependent on data volume and Freshservice API rate limits. A managed migration service typically completes in 3–7 days.

More from our Blog

Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration
Help Desk

Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration

Ensure your help desk migration is a success with this comprehensive 20-point post-migration QA checklist. This expert guide details the 20 essential tests needed to validate your data integrity, system functionality, user-friendliness, and performance . Learn exactly how to check everything from ticket data, attachments, and knowledge base articles to critical workflows, automations, and integrations before you go live. This process is your final line of defense against lost tickets, broken workflows, and unhappy customers.

Raaj Raaj · · 8 min read
Zero-Downtime Help Desk Data Migration: How to Keep Support Running During the Move
Help Desk

Zero-Downtime Help Desk Data Migration: How to Keep Support Running During the Move

This guide details the 3-stage technical process for a zero-downtime help desk migration. Learn how to use an initial bulk data transfer, a continuous delta migration (Change Data Capture), and a seamless final cutover to move platforms without any service interruption. Discover how an engineer-led approach can guarantee a 100% accurate, 50x faster migration.

Raaj Raaj · · 6 min read