DocsMigrations

Migrations

Migrate from Amplitude

3 min readReviewed July 2026

Amplitude’s raw Export API is the appropriate starting point for a historical migration. It returns ZIP archives containing JSON files with one event per line. Build the export into a durable staging step, then transform each line into your GraphJSON event contract.

Read the general migration workflow before using this provider-specific mapping.

Export a bounded interval#

Use Amplitude’s official Export API and process time in UTC. The API selects data using Amplitude’s documented export-time behavior, which may differ from the occurrence-time field you ultimately preserve.

Export small intervals first. Amplitude documents a 4 GB response-size limit and a maximum request window; busy projects may need hourly requests. For every response, retain:

  • requested UTC interval
  • archive checksum and byte size
  • file names
  • decoded row count
  • earliest and latest event_time
  • export failure and retry log

Keep the raw archives immutable. Write transformed output to a separate path so mapping changes never destroy the original.

Map the fields#

Start with this mapping, then remove anything your product does not need:

Amplitude field GraphJSON target Notes
event_type event Normalize only with an explicit rename map
event_time request timestamp Parse as UTC and convert to whole Unix seconds
user_id user_id Preserve your application ID when this is one
device_id anonymous_id Do not treat it as a known user automatically
event_properties selected event fields Allowlist, rename, and type-check
user_properties selected snapshot fields Include only if event-time semantics are useful
groups account_id or named group IDs Map each group type deliberately
$insert_id source_insert_id and/or event_id Preserve for duplicate analysis

Amplitude may contain both merged identities and original identifiers. Decide whether historical GraphJSON metrics should follow the exported row identity or your current application identity map. Document the choice; GraphJSON does not automatically reproduce Amplitude’s identity resolution.

Transform one record#

function toGraphJSON(source) {
  if (!source.event_type || !source.event_time) {
    throw new Error("missing event_type or event_time");
  }

  const eventTime = Date.parse(source.event_time);
  if (!Number.isFinite(eventTime)) {
    throw new Error("invalid event_time");
  }

  const props = source.event_properties || {};

  return {
    timestamp: Math.floor(eventTime / 1000),
    body: {
      event: source.event_type,
      event_id: source.$insert_id
        ? `amplitude:${source.$insert_id}`
        : undefined,
      user_id: source.user_id || undefined,
      anonymous_id: source.device_id || undefined,
      account_id: source.groups?.account_id || undefined,
      plan: typeof props.plan === "string" ? props.plan : undefined,
      amount:
        typeof props.amount === "number" ? props.amount : undefined,
      schema_version: 1,
      migration_source: "amplitude",
    },
  };
}

Remove undefined values before serialization. In a real transform, validate against the same schema used by new production instrumentation and write rejected rows to quarantine with a reason code.

Avoid spreading all event_properties or user_properties into the target. That can copy sensitive data, inconsistent types, and payloads beyond GraphJSON’s 10,000-character event limit.

Handle identity explicitly#

Amplitude distinguishes user_id and device_id, and its analysis may merge activity according to Amplitude’s identity rules. GraphJSON stores those values without a hidden merge.

Choose one of these approaches:

  • Keep anonymous and authenticated activity separate.
  • Emit or import an identity_linked event and join it in SQL.
  • Rewrite historical anonymous rows using a verified identity map from your own system.

The third option changes historical records and is hard to undo. Use it only when the application database provides an authoritative mapping. See Users, accounts, and identity.

Reconcile the result#

Compare:

  • raw export rows versus accepted and quarantined rows
  • counts by event_type and toDate(event_time)
  • unique $insert_id versus unique imported event_id
  • missing user_id and device_id rates
  • sums for important numeric properties
  • several complete user and account histories

Do not expect an Amplitude dashboard total to match a raw import without matching its filters, project timezone, identity behavior, and historical transformation settings.

Cutover checklist#

  • Direct GraphJSON instrumentation uses the approved target names and types.
  • The export interval and Amplitude project region are correct.
  • Event time comes from event_time, not archive or upload time.
  • Event and user properties use explicit allowlists.
  • Device and user identity behavior is documented.
  • $insert_id is preserved where available.
  • A representative pilot day reconciles before the full run.
  • Staged archives have access controls and a deletion date.
Need a hand?

Tell us what you’re building and we’ll point you in the right direction.

Contact support