Prior to starting a historical data migration, ensure you do the following:
- Create a project on our US or EU Cloud.
- Sign up to a paid product analytics plan on the billing page (historic imports are free but this unlocks the necessary features).
- Set the
historical_migration
option totrue
when capturing events in the migration. This is automated if you are running a managed migration.
Managed migrations provide an automated way to migrate your historical data into PostHog without writing custom scripts.
With managed migrations, you can import data from multiple sources:
- Direct imports: Connect directly to Mixpanel or Amplitude using your API credentials
- S3 imports: Upload your event data to an S3 bucket in JSONL format for automatic ingestion
Getting started
- Go to the managed migrations page
- Choose your import method (details about methods below)
- Import data
Direct imports (Mixpanel & Amplitude)
Direct imports enable you to migrate data directly from Mixpanel or Amplitude without any manual data handling. PostHog automatically:
- Connects to your source platform using your API credentials
- Exports your data for the specified date range
- Transforms events to match PostHog's event schema
- Imports the data into your PostHog project
Each direct import job supports a maximum date range of one year. To migrate data spanning more than one year, run multiple jobs that cover smaller, consecutive ranges.
See the Event transformations section for full mapping details and examples. You should also consider running a small test migration first to preview transformations.
Direct imports are currently less reliable than S3 imports for some datasets and customers. Rate limits and data export size limits vary by customer and by Amplitude/Mixpanel account, making it hard to gracefully handle the full range of API export failure modes. We’re actively improving the robustness of direct imports, but if you encounter issues, consider using an S3 import.
S3 imports
S3 imports are the most reliable and flexible way to migrate your data to PostHog.
You can upload uncompressed JSONL files using any supported content type — posthog
, amplitude
, or mixpanel
. You may choose to run your own custom transformations and output PostHog‑shaped events (select the posthog
content type), but you don’t have to — if you upload Amplitude or Mixpanel exports and select the matching content type, we’ll apply the same mappings as direct imports.
This method gives you full control while still benefiting from automated ingestion. This method is ideal when you need:
- Custom event transformations
- To handle very large datasets
- To migrate from platforms not supported by direct imports
- Direct imports aren't working for your data set
If you upload Amplitude or Mixpanel events to S3, select the matching content type and PostHog will apply the event and property mappings described in Event transformations. If you upload PostHog events, they are ingested as-is without source-specific remapping.
Supported content types
amplitude
: Upload JSONL events that match Amplitude’s export schema. The event transformations for Amplitude are applied during import.mixpanel
: Upload JSONL events that match Mixpanel’s export schema. The event transformations for Mixpanel are applied during import.posthog
: Upload JSONL events that match PostHog’s event schema. Events are ingested as-is without source-specific remapping. Use this if you’ve already run your own transformation to produce PostHog-shaped events.
File format and compression
- Format: Files must be JSON Lines (
.jsonl
) with one valid JSON object per line. - Schema: Each line/object must conform to the selected content type’s schema (
amplitude
,mixpanel
, orposthog
). - Compression: Files must not be compressed. Some export endpoints return compressed archives (for example
.gz
or.zip
)—ensure you fully uncompress files to plain.jsonl
before importing.
Custom event transformations with S3
“Custom transformations” means you can run your own script over your source data to reshape it exactly how you want before importing:
- Export your events from your source.
- Transform them with your own script or pipeline.
- Option A: Output PostHog-shaped events and choose the
posthog
content type (no additional mapping is applied by PostHog). - Option B: Output events in the original Amplitude or Mixpanel export schema and choose the matching content type to reuse PostHog’s built-in mappings described in Event transformations.
- Option A: Output PostHog-shaped events and choose the
- Save as uncompressed JSONL and upload to S3.
Minimal example for posthog
content type (one line per event):
{"event":"$pageview","distinct_id":"user_123","properties":{"$current_url":"https://example.com"},"timestamp":"2024-01-01T00:00:00Z"}
Setting up an S3 import
- Prepare your data: Export your events and format them as JSONL (one JSON object per line)
- Upload to S3: Place your
.jsonl
files in an S3 bucket - Start import: Provide PostHog with:
- S3 region
- S3 bucket name
- AWS access key ID
- AWS secret access key
- Content Type (choose one of:
amplitude
,mixpanel
, orposthog
; your files must match this schema and be uncompressed.jsonl
)
When to use each method
Use direct imports when:
- You have a straightforward migration from Mixpanel or Amplitude
- Your data volume is moderate
- You're comfortable with PostHog's default event transformation from your schema
- You want the simplest setup process
Use S3 imports when:
- Your data volume is large
- You need custom event transformations or property mappings (for example, running your own scripts to reshape events before import)
- You're migrating from a platform not yet supported by direct imports
- You want to pre-process or clean your data before import
- Direct imports are failing for you
Monitoring your migration
Once started, you can monitor your migration progress:
- Migration dashboard: View real-time progress and status of your migration
- Event validation: Query for your events as they come in to ensure proper transformation and ingestion
If an unexpected error occurs during a migration, the job will be paused and the error message will be displayed in the migration dashboard. For transient issues (for example, upstream API rate limits or temporary availability problems), you can contact support using the Data ingestion topic to help resume or retry your job once the condition has cleared.
If the error indicates your job is attempting to import too much data (for example, exceeding export size limits), consider switching to an S3 import, which is better suited for large migrations.
Best practices
- Test first: Run a small test migration with a subset of your data against a new project. This ensures events have the desired schema without polluting your main project
Event transformations
When you use direct imports, PostHog transforms events from your source (Amplitude or Mixpanel) into PostHog’s schema. Below is an overview of what changes.
Amplitude → PostHog
- Event name mapping
session_start
: dropped (not imported)[Amplitude] Page Viewed
→$pageview
[Amplitude] Element Clicked
and[Amplitude] Element Changed
→$autocapture
- All other event names are preserved
- Distinct ID selection
- Prefer
user_id
, elsedevice_id
, else a generated UUID
- Prefer
- Timestamp
- Parse, in order:
event_time
,client_event_time
,server_received_time
(supports fractional seconds). If none parse, use current time
- Parse, in order:
- Added properties
- Always adds:
historical_migration: true
,analytics_source: "amplitude"
- If present:
$amplitude_user_id
,$amplitude_device_id
,$device_id
,$amplitude_event_id
,$amplitude_session_id
- Page context mapping from
event_properties
:[Amplitude] Page URL
→$current_url
[Amplitude] Page Domain
→$host
[Amplitude] Page Path
→$pathname
[Amplitude] Viewport Height
/Width
→$viewport_height
/$viewport_width
referrer
/referring_domain
→$referrer
/$referring_domain
- Device/browser/geo:
$browser
fromos_name
,$os
fromdevice_type
$browser_version
fromos_version
$device_type
:Mobile
foriOS
/Android
,Desktop
forWindows
/Linux
$ip
fromip_address
$geoip_city_name
,$geoip_subdivision_1_name
,$geoip_country_name
fromcity
/region
/country
- Always adds:
- Person updates
set_once
:$initial_referrer
,$initial_referring_domain
,$initial_utm_*
(filters out the literal string"EMPTY"
)set
:$browser
,$os
,$device_type
,$current_url
,$pathname
,$browser_version
,$referrer
,$referring_domain
, and geo fields
Example (simplified):
// Amplitude input{"event_type": "[Amplitude] Page Viewed","user_id": "user_123","event_properties": {"[Amplitude] Page URL": "https://example.com/page","[Amplitude] Page Domain": "example.com","[Amplitude] Page Path": "/page"}}// PostHog event produced{"event": "$pageview","distinct_id": "user_123","properties": {"$current_url": "https://example.com/page","$host": "example.com","$pathname": "/page","historical_migration": true,"analytics_source": "amplitude"}}
Mixpanel → PostHog
- Event name mapping
$mp_web_page_view
→$pageview
- All other event names are preserved
- Distinct ID selection
- Use
properties.distinct_id
unless it looks anonymous; if$distinct_id_before_identity
is present it can be used instead when:distinct_id
is empty- or starts with
$device:
- or is only uppercase letters, digits, and dashes
- If no ID remains and you choose to skip events without IDs, the event is dropped; otherwise a UUID is generated
- Use
- Timestamp
properties.time
is treated as seconds unless it’s > 10,000,000,000 (then it’s milliseconds). An optional offset can be applied if your data needs it
- Property cleanup and enrichment
- Always adds:
historical_migration: true
,analytics_source: "mixpanel"
- Geo mapping:
$city
→$geoip_city_name
,$region
→$geoip_subdivision_1_name
,mp_country_code
→$geoip_country_code
and derived$geoip_country_name
- Removes Mixpanel-specific fields like
$mp_api_endpoint
,mp_processing_time_ms
,$insert_id
,$geo_source
,$mp_api_timestamp_ms
- No
set
/set_once
updates are applied
- Always adds:
Example (simplified):
// Mixpanel input{"event": "$mp_web_page_view","properties": {"time": 1700000000000,"distinct_id": "abc123","$city": "Paris","mp_country_code": "FR"}}// PostHog event produced{"event": "$pageview","distinct_id": "abc123","properties": {"$geoip_city_name": "Paris","$geoip_country_code": "FR","$geoip_country_name": "France","historical_migration": true,"analytics_source": "mixpanel"}}
Start with a small date range as a test migration to a fresh project to preview the transformed events and validate your mappings before running a full migration.