---
title: "How to Export Data from Podium: API Limits, Methods & Gaps"
slug: how-to-export-data-from-podium-api-limits-methods-gaps
date: 2026-08-24
author: Raaj
categories: [Migration Guide, CRM, Help Desk]
excerpt: "Podium exports contacts by CSV, but conversations, reviews, and payments require FTP access or the REST API v4. Here's every method, its limits, and what each one misses."
tldr: "Podium has no full-account export. Contacts export via CSV. Conversations, reviews, and payments require FTP or API v4 with custom scripting."
canonical: https://clonepartner.com/blog/how-to-export-data-from-podium-api-limits-methods-gaps/
---

# How to Export Data from Podium: API Limits, Methods & Gaps


# How to Export Data from Podium: API Limits, Methods & Gaps

*Last verified: June 2025, against Podium API v4 and Podium's official developer docs at docs.podium.com.*

If you're trying to get your data out of Podium — whether for a platform migration, reporting, compliance, or a pre-cancellation backup — the answer depends on which data you need. Contact records export as a CSV from the UI. Everything else (conversations, messages, reviews, payments, campaigns, surveys) requires either FTP access or custom API work. There is no native full-account export button.

Podium's architecture is **location-centric**. Contacts can belong to multiple locations, conversations carry a `locationUid`, and reviews and invoices reference specific locations. This creates data mapping challenges that standard ETL tools rarely handle cleanly.

This guide covers every extraction method, the specific data each returns, API constraints, and the gaps that trip up most teams.

## What Data Lives in Podium

**Podium** is a customer interaction platform for local businesses, built around SMS/text messaging, webchat, Google and Facebook review management, text-based payments, and marketing campaigns. Before planning an export, understand the data model.

| Entity | Description | CSV? | FTP? | API? |
|---|---|---|---|---|
| **Contacts** | Customer profiles: name, phone, email, status, address, tags, custom attributes | ✅ (partial) | ✅ | ✅ |
| **Conversations** | Threaded message containers tied to locations | ❌ | ✅ | ✅ |
| **Messages** | Individual items within conversations (SMS, email, webchat) | ❌ | ✅ | ✅ |
| **Reviews** | Google/Facebook reviews with ratings, body, author | ❌ | ✅ | ✅ |
| **Review Invites** | Outbound review request records | ❌ | ✅ | ✅ |
| **Payments / Invoices** | Invoice and payment transaction records with nested payments, refunds, fees | ❌ | ✅ | ✅ |
| **Campaigns** | Bulk messaging campaigns and interaction data | ❌ | ✅ | ✅ |
| **Campaign Contacts** | Opt-in/opt-out records per campaign | ❌ | ✅ | ❌ |
| **Feedback (Surveys)** | Customer survey interactions, scores, respondent messages | ❌ | ✅ | ✅ |
| **Locations** | Physical business locations with addresses and Podium numbers | ❌ | ✅ | ✅ |
| **Users** | Team members / agents | ❌ | ❌ | ✅ |

**Contacts are the only entity exportable without engineering effort.** Everything else requires FTP access or API calls.

## Method 1: In-App CSV Export (Contacts Only)

Podium's UI export is the simplest path — and the most limited.

**How to do it:** Log in as Account Owner, navigate to the **Contacts** page, apply any filters, and click **Export**. The CSV is emailed to the Account Owner's address.

**Fields included:** Name, Phone, Email, Contact Status, Address, Tags, and Date Added — seven fields total.

**What's missing:** Custom attributes, conversation history, message bodies, attachments, opt-in/opt-out timestamps, marketing consent records, and any relational link between contacts and their conversations or locations.

> [!WARNING]
> **Only Account Owners can trigger the CSV export.** If you're a team member or admin without Account Owner permissions, you need to request this from your org's owner. There is no role-based export delegation.

This works for small teams migrating a basic contact list to another CRM. It does **not** work for preserving conversation context, compliance records, or anything beyond flat contact data.

## Method 2: FTP Raw Data Access

Podium provides raw data through an **FTP server**. This is Podium's most comprehensive bulk export channel — but it requires a manual request and Account Owner involvement.

### How to Request FTP Access

1. The Account Owner submits a request to Podium support (or through their account manager) to enable FTP access.
2. Podium provisions credentials for the organization. The connection uses **SFTP** (SSH File Transfer Protocol) — not plain FTP — which means you'll need an SFTP client (FileZilla, Cyberduck, `sftp` CLI) or an SFTP library in your pipeline.
3. Data is delivered as **pipe-delimited flat files** (`.txt` or `.csv`), one file per table, in a provisioned directory. The directory structure is `/org_uid/table_name/` with dated filenames.
4. Tables refresh on a **daily schedule** (typically overnight UTC). There is no sub-daily or on-demand refresh option.

### Available FTP Tables

Podium exposes **10 data tables** through FTP:

- **Feedback** — Survey interactions, scores, customer responses, flow metadata
- **Messages** — Conversation items with body text, sender, delivery status, timestamps, attachment content types (but not attachment binaries)
- **Payments** — Invoice records with amounts, customer names, contact identifiers, location context
- **Leads** — Inbound conversation metadata, first response times (adjusted and unadjusted for business hours), webchat URLs, lead conversion data
- **Campaign Contacts** — Contact opt-in/opt-out records, marketing consent sources, tags
- **Campaigns** — Campaign definitions, messages, statuses, per-contact interaction events (sent, failed, opt_out, link_clicked, response)
- **Contacts** — Contact channels, phone numbers, opt-out timestamps, marketing consent records
- **Locations and Organizations** — Org/location hierarchy, addresses, Podium numbers, creation dates
- **Reviews** — Review text, ratings, authors, publication dates, listing sites, attributed review invite UIDs
- **Review Invites** — Invite channels, sender info, click timestamps, phone numbers, integration flags

> [!TIP]
> The FTP Messages table includes `conversation_item_body` — the actual message text. This is the only non-API method that gives you message content. If you need full conversation history but can't build an API integration, FTP is your path.

### FTP Limitations

- **Daily refresh only.** If you need data from the last 24 hours, use the API. FTP is a snapshot, not a stream.
- **No incremental sync.** You get full table dumps, not change feeds. For large orgs, these files can be several gigabytes.
- **No relational joins built in.** You'll need to join tables yourself (e.g., join reviews to review_invites on `first_attributed_review_invite_uid = review_invite_uid`).
- **Flat format.** You lose the nested object structure that the API preserves. Invoice payment arrays, for example, are flattened to one row per payment — requiring reconstruction on your end.
- **No attachment binaries.** The Messages table includes `conversation_item_attachment_content_type` (MIME type) but not the actual file. For attachment content, the API is the only path.
- **Account Owner gating.** The provisioning request can take several business days.

## Method 3: Podium REST API v4

The API is the only method that gives you **full programmatic control** over extraction — including filtering, relational traversal, and nested object structure (conversation → messages → attachments).

### Authentication: OAuth 2.0

Podium's API uses OAuth 2.0 exclusively. No API keys or basic auth.

**Setup:**

1. Apply for a Developer Account at `developer.podium.com`.
2. Wait for approval — Podium reviews applications manually, which can take several days.
3. Create an OAuth Application in the Developer Portal to get your `client_id` and `client_secret`.
4. Implement the OAuth 2.0 authorization code flow to obtain access and refresh tokens.
5. All requests go over HTTPS to `https://api.podium.com/v4/`.

**Token lifespan:** Podium access tokens expire after **1 hour**. Refresh tokens are longer-lived and must be used to obtain new access tokens automatically. Any extraction script running longer than 60 minutes — which is nearly all of them — must implement token refresh logic before starting. A silent expiry mid-export will silently return 401 errors; without checkpointing, you lose progress from the last successful page.

```bash
curl --request GET \
  --url 'https://api.podium.com/v4/contacts' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --header 'podium-version: <pinned-version>'
```

> [!WARNING]
> **Save your Client Secret immediately.** Podium's developer portal does not let you retrieve it after you leave the page. You can regenerate it, but existing OAuth tokens won't be revoked — which creates confusion during long-running migration scripts.

The user authorizing the app must have permission to the underlying product area, or requests fail with `403 Forbidden`. ([docs.podium.com](https://docs.podium.com/docs/oauth?utm_source=openai))

### OAuth Scopes for Data Export

Request only the scopes you need. Over-requesting scopes is the most common reason Podium rejects app reviews.

| Scope | What It Unlocks |
|---|---|
| `read_contacts` | Contact profiles, attributes, tags |
| `read_messages` | Conversation messages and message objects |
| `read_reviews` | Review data, review invites, summaries |
| `read_campaigns` | Campaign definitions and interactions |
| `read_feedback` | Survey/feedback data |
| `read_payments` | Invoice and payment records |
| `read_locations` | Location metadata |
| `read_organizations` | Organization-level data |
| `read_users` | Team member / agent data |

> [!TIP]
> Podium provides **developer test accounts**. Use them before pointing your exporter at production — it's a cheap way to verify scope coverage, pagination logic, and rate-limit behavior without touching live customer records. ([docs.podium.com](https://docs.podium.com/docs/podium-test-accounts?utm_source=openai))

### Key API Endpoints for Extraction

| Endpoint | Method | Description |
|---|---|---|
| `/v4/contacts` | GET | List all contacts (paginated) |
| `/v4/contacts/{email}` | GET | Retrieve a single contact by email |
| `/v4/conversations` | GET | List conversations (paginated, scoped by `locationUid`) |
| `/v4/conversations/{uid}` | GET | Get a single conversation |
| `/v4/conversations/{uid}/messages` | GET | Retrieve messages within a conversation |
| `/v4/reviews` | GET | List all reviews |
| `/v4/reviews/invites` | GET | List review invites |
| `/v4/reviews/{uid}/responses` | GET | List review responses |
| `/v4/campaigns` | GET | List all campaigns |
| `/v4/campaign_interactions` | GET | List campaign interaction events |
| `/v4/feedback` | GET | List feedback/survey data |
| `/v4/invoices` | GET | List invoices (includes nested payments, refunds, fees) |
| `/v4/locations` | GET | List all locations |
| `/v4/users` | GET | List all users (includes AI agents by default via `includeAgents`) |

Extracting conversations and messages requires a **nested loop**: list conversations via `/v4/conversations`, then for each conversation, fetch its messages via `/v4/conversations/{uid}/messages`. If you have 50,000 conversations, that's at least 50,000 additional API calls for messages alone — not counting pagination within conversations.

### Rate Limits

This is where most export scripts break.

Podium enforces per-minute rate limits on API requests. Write endpoints like message sending cap at **10 requests per minute**. Read endpoints allow higher throughput, but the exact ceilings are not publicly documented for all endpoints. When you hit the limit, Podium returns `HTTP 429 Too Many Requests` with a `Retry-After` header (in integer seconds or HTTP-date format). ([docs.podium.com](https://docs.podium.com/reference/http-status-codes))

Podium also enforces a shared **daily quota**. The ceiling is not published. Hitting the per-minute limit aggressively can exhaust the daily quota well before end of day, at which point every endpoint returns hard errors until UTC midnight.

**Common HTTP error codes in export pipelines:**

| Code | Meaning | Common cause in exports |
|---|---|---|
| `400` | Bad Request | Malformed query parameter or missing required field |
| `401` | Unauthorized | Expired access token; trigger refresh flow |
| `403` | Forbidden | OAuth scope missing or user lacks permission to resource |
| `404` | Not Found | Object UID doesn't exist or was deleted |
| `422` | Unprocessable Entity | Parameter values are syntactically valid but semantically rejected |
| `429` | Too Many Requests | Per-minute or daily quota exceeded; respect `Retry-After` |
| `500` | Internal Server Error | Podium-side failure; retry with backoff |

> [!CAUTION]
> **Naive retry loops will burn your entire daily quota.** A `while status == 429: retry` pattern will stampede the per-minute window repeatedly. By mid-morning, your daily quota can be fully consumed and every endpoint goes hard-down until UTC midnight. Always implement exponential backoff with jitter and respect the `Retry-After` header.

**Practical throughput math:** A dataset of 50,000 conversations where each conversation averages 8 messages requires a minimum of 50,000 message-fetch calls (one per conversation), plus pagination overhead if any conversation exceeds your page size. Even at generous read-endpoint rate limits, plan for **many hours** of continuous API calls — and schedule extraction during off-peak hours to avoid competing with production webhook delivery traffic.

**Recommended retry pattern:**

```python
import time
import requests

def podium_get(url, token, max_retries=5):
    for attempt in range(max_retries):
        resp = requests.get(
            url,
            headers={"Authorization": f"Bearer {token}",
                     "Content-Type": "application/json",
                     "podium-version": "<pinned-version>"}
        )
        if resp.status_code == 200:
            return resp.json()
        elif resp.status_code == 401:
            token = refresh_access_token()  # implement your refresh logic
        elif resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            jitter = attempt * 2
            time.sleep(retry_after + jitter)
        else:
            resp.raise_for_status()
    raise Exception(f"Max retries exceeded for {url}")
```

### Pagination

Podium API v4 uses **cursor-based pagination** on collection endpoints. You pass `limit` (1–100) and `cursor` query parameters. ([docs.podium.com](https://docs.podium.com/reference/pagination))

```bash
# First page
curl -G 'https://api.podium.com/v4/contacts' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'podium-version: <pinned-version>' \
  --data-urlencode 'limit=100'

# Subsequent pages — use the cursor from the previous response
curl -G 'https://api.podium.com/v4/contacts' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'podium-version: <pinned-version>' \
  --data-urlencode 'limit=100' \
  --data-urlencode 'cursor=<NEXT_CURSOR>'
```

Two important caveats:

1. **When you send a `cursor`, other filters on that request are ignored.** If you're filtering by date and paginating, apply your filters only on the first request. Subsequent cursor-based requests inherit the original filter context. ([docs.podium.com](https://docs.podium.com/reference/pagination))
2. **Cursors can expire.** Podium documents `invalid_cursor` as a possible error. Resumable exporters should checkpoint by dataset and timestamp instead of assuming a stale cursor will work the next day.

> [!WARNING]
> **Pin a version header on every request.** Requests without one default to the newest API version, which can introduce breaking changes mid-export. ([docs.podium.com](https://docs.podium.com/reference/pagination))

### Parameter Naming Inconsistencies

This trips engineers on the first pass: Podium does **not** use a consistent naming convention for date filters across endpoints. ([docs.podium.com](https://docs.podium.com/reference/contactindex))

| Endpoint | Date filter parameter names |
|---|---|
| Contacts | `updated_at` (snake_case) |
| Locations | `updatedAfter` (camelCase) |
| Reviews | `createdAt`, `updatedAt` (camelCase) |
| Review Invites | `createdAt`, `updatedAt`, `senderUid` |
| Invoices | `dateCreated`, `datePaid`, `dateRefunded` |
| Conversations | `locationUid`, `limit`, `cursor` only — **no date filter supported** |

The absence of date filtering on conversations is significant: you cannot request "conversations updated after date X." The only workaround is to paginate through all conversations for a given `locationUid` and filter client-side by the `updatedAt` timestamps in the response payload. For large accounts, this means walking every conversation regardless of age.

Build your API client per endpoint. Do not assume one naming convention applies across the API.

### Attachment Handling

Customers frequently send images and documents via Podium SMS. The message payload contains attachment references, but **attachment URLs expire after 7 days**. ([docs.podium.com](https://docs.podium.com/reference/conversationindex-1))

Your extraction script must download the binary file immediately during extraction, store it in your own cloud storage (e.g., AWS S3), and rewrite the URL in your database. Failing to do this means broken images once URLs expire or your Podium contract ends.

The FTP Messages table includes `conversation_item_attachment_content_type` (the MIME type) but not the actual file binary. For attachment file content, the API is the only path.

### Invoice Nested Object Structure

The `/v4/invoices` endpoint returns invoices with nested arrays that require explicit deserialization. A truncated example of the structure:

```json
{
  "uid": "inv_abc123",
  "locationUid": "loc_xyz",
  "contactName": "Jane Smith",
  "totalAmount": 15000,
  "status": "paid",
  "dateCreated": "2025-03-01T10:00:00Z",
  "datePaid": "2025-03-02T14:23:00Z",
  "payments": [
    {
      "uid": "pay_001",
      "amount": 15000,
      "method": "card",
      "last4": "4242",
      "processedAt": "2025-03-02T14:23:00Z"
    }
  ],
  "refunds": [],
  "fees": [
    {
      "type": "processing",
      "amount": 465
    }
  ]
}
```

`totalAmount` and `fees [].amount` are in **cents** (integer). Divide by 100 for display. `refunds` follows the same array structure as `payments`. When flattening to tabular format (e.g., for SQL ingestion), emit one row per payment and one row per refund, joining back to the invoice `uid`.

### Deprecated Fields to Avoid

Podium marks the `contact` and `sender` nested objects on messages as **deprecated** — they are currently only available in webhooks. For API exports, key off stable fields: message `uid`, `conversation.uid`, `senderUid`, `contactName`, channel identifiers, and timestamps. ([docs.podium.com](https://docs.podium.com/reference/conversationindex-1))

## Using Webhooks for Rolling Sync

For ongoing sync after an initial backfill, webhooks are the right tool. Podium documents webhook event types for contacts, messages, reviews, and invoices — including `message.sent`, `message.received`, `contact.updated`, `review.created`, and `invoice.payment_created`. ([docs.podium.com](https://docs.podium.com/reference/the-webhook-object?utm_source=openai))

**Example `message.received` webhook payload (truncated):**

```json
{
  "event": "message.received",
  "uid": "msg_abc987",
  "conversationUid": "conv_xyz456",
  "locationUid": "loc_001",
  "body": "I'd like to reschedule my appointment.",
  "channel": "sms",
  "senderUid": "contact_111",
  "contactName": "John Doe",
  "sentAt": "2025-03-15T09:12:44Z",
  "attachments": []
}
```

Webhooks are not a replacement for historical extraction. They capture events going forward. The pattern for migration:

1. Use the REST API for the historical backfill.
2. Enable webhooks to capture real-time events during the migration window.
3. Run a final delta sync using timestamp-filtered API calls at cutover.

> [!WARNING]
> **Webhook reliability boundaries.** Podium's webhook endpoint must respond within **5 seconds** or the delivery is marked failed. Failed deliveries retry approximately **15 times over roughly 8 hours**. Webhooks disabled for more than approximately **10 days** may lose queued events permanently. Verify delivery using the `Podium-Signature` and `podium-timestamp` headers — ignore requests that fail signature verification. ([docs.podium.com](https://docs.podium.com/docs/webhooks?utm_source=openai))

## What You Can't Export from Podium

No extraction method covers everything. Known gaps and their partial workarounds:

| Gap | Hard loss or partial workaround? |
|---|---|
| **Webchat widget configuration** (styling, routing rules) | Hard loss — not exposed through any export channel. Document manually before cancellation. |
| **Automation / workflow rules** | Hard loss — configurations don't export. Screenshot and rebuild in target system. |
| **Team inbox routing configuration** | Partial — inbox names appear in FTP data, but full routing logic isn't exportable. Reconstruct from names + your own documentation. |
| **Conversation assignee history** | Partial — current assignee is available via API. Full reassignment audit trail is not accessible through any export method. |
| **Phone call recordings** (Podium Phones) | Hard loss — call audio files are not included in FTP or standard API responses. Contact your account manager before cancellation; retrieval may be possible case-by-case. |
| **Conversation date filtering** | Workaround only — no `createdAt`/`updatedAt` filter on `/v4/conversations`. Paginate all conversations per `locationUid` and filter client-side. |

## GDPR and CCPA Data Portability Requests

If you're extracting data for compliance rather than migration, a **data subject access request (DSAR)** is a separate path.

Podium directs data requests to `privacy-requests@podium.com`. Under GDPR Article 20, Podium must provide personal data in a structured, machine-readable format within one month. Under CCPA/CPRA, California residents can request their data with a 45-day response window.

> [!NOTE]
> **This is for individual data subjects, not bulk account exports.** A DSAR gets you one person's data. It won't replace an API-based migration. But it's a useful backstop if you're being stonewalled on FTP access during contract termination.

One critical detail from Podium's terms: after subscription termination, Podium may **delete all Client Data**. If you're leaving Podium, extract everything before your subscription ends. Do not assume you'll have access after cancellation.

## Data Portability Challenges

Getting data out of Podium is only half the work. Transforming it for a target system introduces structural challenges.

### The Location-Based Model

Podium ties almost all records to a `location_id`. If you're migrating to a platform that uses a workspace, website, or app-centric model, you must decide how to map locations. Typically, `location_id` translates into a custom field, a tag, or a distinct inbox in the target platform.

### Continuous SMS Threads vs. Closed Tickets

Traditional helpdesks operate on a ticket model: an issue is opened, resolved, and closed. Podium operates on a continuous messaging model. An SMS thread with a customer might span years with dozens of unrelated questions in a single thread.

When exporting, you must decide:

1. **Export the entire thread as a single ticket.** Preserves the exact Podium experience but makes reporting in the new system nearly impossible.
2. **Programmatically split the thread.** Write logic to split threads based on time gaps (e.g., a 7-day silence between messages creates a new ticket boundary). This requires timestamp parsing on `sentAt` fields but yields a far cleaner dataset in the target system.

### Contacts and Multi-Location Complexity

The contacts list endpoint documents only `cursor`, `limit`, and `updated_at` as query parameters — no location filter. Yet contact objects can carry multiple location references. Multi-location exports often require a full contact pull followed by post-filtering on the `locations []` array in each contact object. ([docs.podium.com](https://docs.podium.com/reference/contactindex))

### Mapping to Target Architectures

| Target system type | Recommended mapping strategy |
|---|---|
| **CRM (Salesforce, HubSpot)** | Compile Podium SMS threads into a single rich-text Note or Activity logged against the Contact record. Map `location_id` to an Account field. |
| **Helpdesk (Zendesk, Freshdesk)** | Split continuous threads into discrete tickets using time-gap logic. Map `location_id` to a custom ticket field or tag for routing. |
| **Messaging platform ([Unthread](https://clonepartner.com/blog/blog/podium-to-unthread-migration-the-ctos-technical-guide/), [Crisp](https://clonepartner.com/blog/blog/podium-to-crisp-migration-a-technical-guide/), [Helpshift](https://clonepartner.com/blog/blog/podium-to-helpshift-migration-a-technical-guide/))** | Map conversations to threads or channels. Map `location_id` to inbox or team routing rules. Preserve `contactName` and channel (SMS/webchat) as metadata. |

## Building a Full Export Pipeline

For teams building a complete extraction pipeline, here's the recommended sequence:

1. **Request FTP access as a safety net** — even if you plan to use the API. FTP gives you a baseline backup. Allow several business days for provisioning.
2. **Register a developer account** at `developer.podium.com` and create an OAuth app. Request the `read_*` scopes you need.
3. **Build token refresh into your client before writing any extraction logic.** Access tokens expire after 1 hour. Implement refresh before you need it, not after a 3am export fails silently.
4. **Extract locations and users first.** Locations are the top-level container in Podium's data model. You need `location_uid` values to scope subsequent queries. Users give you assignment context.
5. **Extract contacts.** Use the UI CSV only for quick one-offs. For full extraction, paginate through `/v4/contacts` with `updated_at` checkpoints.
6. **Extract conversations per location, then messages per conversation.** This is the most API-call-intensive step. Download attachment binaries as you go — URLs expire after 7 days.
7. **Extract reviews, review invites, campaigns, feedback, and invoices** as separate passes. Deserialize nested payment/refund arrays in invoices at this stage.
8. **Enable webhooks for delta sync.** Add contact, message, review, and invoice events after the backfill so source and destination stop drifting.
9. **Build an ID mapping table.** Map Podium UIDs to your target system's IDs. This is essential for preserving relational context during import.
10. **Validate record counts.** Compare extraction counts against Podium UI totals. Spot-check individual records for content accuracy, timestamp integrity, and attachment presence.

```bash
# Step 4: Extract locations
curl -G 'https://api.podium.com/v4/locations' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'podium-version: <pinned-version>'

# Step 5: Extract contacts with date checkpoint
curl -G 'https://api.podium.com/v4/contacts' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'podium-version: <pinned-version>' \
  --data-urlencode 'limit=100' \
  --data-urlencode 'updated_at=2025-01-01T00:00:00Z'

# Step 6a: Extract conversations scoped to a location
curl -G 'https://api.podium.com/v4/conversations' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'podium-version: <pinned-version>' \
  --data-urlencode 'locationUid=<LOCATION_UID>' \
  --data-urlencode 'limit=100'

# Step 6b: Extract messages for a conversation
curl -G 'https://api.podium.com/v4/conversations/<CONVERSATION_UID>/messages' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'podium-version: <pinned-version>'
```

> [!TIP]
> The exact encoding of date filters like `createdAt` and `updatedAt` varies by endpoint. Verify against the docs for your pinned API version before automating. Run extraction during off-peak hours to reduce competition with production webhook traffic and lower your risk of hitting the unpublished daily quota.

## Common Pitfalls

- **Not implementing token refresh before starting.** Access tokens expire after 1 hour. A long-running export will hit a 401 mid-way and silently fail unless you've built automatic refresh.
- **Ignoring the daily quota.** Even if you pace under the per-minute ceiling, the daily quota is a separate wall. Track your call count and plan multi-day extractions accordingly.
- **Assuming the CSV export is complete.** The CSV includes only 7 fields. Custom attributes, conversation links, opt-in sources, and all relational data are absent.
- **Not exporting before cancellation.** Podium's terms explicitly state they may delete all Client Data after subscription termination. Extract first, cancel second.
- **Treating FTP data as real-time.** FTP tables refresh daily (overnight UTC). If you need data from the last 24 hours, use the API.
- **Not pinning the API version.** Requests without a version header default to the newest version, which can introduce breaking changes during a multi-day export.
- **Relying on stale cursors.** Long-running exports that pause overnight may find cursor values expired. Checkpoint with timestamps, not cursors alone.
- **Skipping attachment downloads.** Attachment URLs expire after 7 days. If you don't download binaries during extraction, they'll be inaccessible before you finish the migration.
- **Applying filters on cursor pages.** When paginating, filters are ignored on cursor-based requests. Set filters only on the first page request.
- **Not handling the conversations date-filter gap.** There is no server-side date filter for conversations. Build client-side filtering into your pipeline from the start, or you'll extract far more data than you need.

## Choosing the Right Extraction Method

| Scenario | Best Method | Why |
|---|---|---|
| Quick contact list for another CRM | In-app CSV | No engineering needed; delivers in minutes |
| Full data backup before cancellation | FTP + API | FTP for bulk tables, API for nested/relational data and attachments |
| Migration to another helpdesk or messaging platform | API (custom ETL) | Only method that preserves conversation threading and contact-message relationships |
| One-time analytics or reporting snapshot | FTP | Structured flat files ready for SQL/BI tools without API pagination overhead |
| Ongoing data sync to external system | API + Webhooks | Webhooks for real-time events, API for historical backfill |
| Individual customer data request (GDPR/CCPA) | Privacy request | Email privacy-requests@podium.com; 30-day GDPR / 45-day CCPA response window |

## Summary: Key Operational Facts

For quick reference, the most commonly needed technical parameters:

- **OAuth access token lifespan:** 1 hour
- **Pagination max page size:** 100 records
- **Pagination type:** Cursor-based; filters ignored on cursor pages
- **Attachment URL expiry:** 7 days
- **FTP refresh cadence:** Daily (overnight UTC)
- **FTP protocol:** SFTP
- **FTP file format:** Pipe-delimited flat files
- **Message send rate limit:** 10 requests/minute
- **Read endpoint rate limits:** Endpoint-specific; not all published
- **Rate limit response code:** HTTP 429 with `Retry-After` header
- **Daily quota:** Exists; ceiling not published
- **Webhook response timeout:** 5 seconds
- **Webhook retry count:** ~15 retries over ~8 hours
- **Webhook event queue loss:** After ~10 days of webhook being disabled
- **Privacy request email:** privacy-requests@podium.com
- **GDPR response window:** 1 month (Article 20)
- **CCPA response window:** 45 days

## Frequently asked questions

### Can you export conversations and messages from Podium?

Not via the UI. Podium's in-app CSV export only covers contacts. To export conversations and messages, you need either FTP raw data access (requested through your Account Owner) or the Podium REST API v4, which lets you retrieve messages per conversation programmatically.

### What are Podium's API rate limits?

Podium enforces per-minute rate limits that vary by endpoint — write operations like message sending are capped tighter than read operations. Exceeding limits returns HTTP 429 with a Retry-After header. There is also an unpublished daily quota. Implement exponential backoff with jitter and respect the Retry-After header.

### How do I get FTP access to Podium's raw data?

An Account Owner must submit a request to Podium support to enable FTP access. Once provisioned, you get 10 structured data tables covering messages, contacts, reviews, payments, campaigns, leads, feedback, locations, and review invites. Approval can take days.

### Does Podium delete data after you cancel your subscription?

Yes. Podium's terms state that following termination, they may delete all Client Data. Always complete your data extraction via FTP and API, validate your backup, and then cancel.

### How do I export reviews and payments from Podium?

Reviews use GET /v4/reviews, GET /v4/reviews/invites, and GET /v4/reviews/{uid}/responses. Payments are under GET /v4/invoices, where invoice objects include nested payment, refund, and fee data. Neither is available via CSV export.
