History Importer

A batch endpoint for backfilling from Amplitude, Mixpanel, PostHog, a GA4 BigQuery export, or a generic JSON export you shape with a small field-mapping config. This is an admin action — it authenticates with your secret key, never the publishable one.

Secret key only. Authorization uses Bearer drengr_sk_…— the same secret key you'd use for the CLI/MCP license, from /pro. Never embed it in an app or run it client-side; call this endpoint from your own backend or a one-off script.

5-minute quickstart

Envelope: { source, app_package?, mapping?, events: [...] }. source is one of amplitude, mixpanel, posthog, ga4, or generic. A minimal Amplitude import:

bash
curl -X POST function(){throw Error("Attempted to call FN_BASE() from the server but FN_BASE is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.")}/import-events \
  -H "Authorization: Bearer drengr_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "amplitude",
    "app_package": "com.example.myapp",
    "events": [
      {
        "event_type": "Order Completed",
        "event_time": "2026-06-01T12:00:00Z",
        "device_id": "abc-123",
        "user_id": "user_123",
        "event_properties": { "revenue": 42.5 },
        "os_name": "iOS",
        "os_version": "17.4"
      }
    ]
  }'

Endpoint & limits

POST
function(){throw Error("Attempted to call FN_BASE() from the server but FN_BASE is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.")}/import-events
  • 1,000 events per request, 5MB per request body — chunk a larger history across multiple calls
  • A bad row is skipped and counted, never fails the whole batch (fail-open, per event)
  • Up to 10 skip reasons are returned per request; the total skipped count is exact even when the list is truncated

Where to export, per vendor

Amplitude

Organization Settings → Exports and Access → Amplitude Export API (GET https://amplitude.com/api/2/export), or a saved cohort/segment export from the UI. The export is a zipped set of JSON files, one event per line — set source: "amplitude". Drengr reads event_type, event_time (or client_event_time), device_id, user_id, event_properties, user_properties, os_name/os_version, device_type/device_family, version_name, $insert_id, and session_id directly off each row.

Mixpanel

Project Settings → Export, or the Raw Data Export API (GET https://data.mixpanel.com/api/2.0/export), which streams newline-delimited JSON. Set source: "mixpanel". Drengr reads event and, from properties: distinct_id, $user_id, time (unix seconds), $insert_id, $os/$os_version, $model, $app_version_string. Every other $-prefixed Mixpanel-internal property is dropped; everything else becomes a dim or measure.

PostHog

Data management → Export, or the GET /api/projects/:id/export API. Set source: "posthog". Drengr reads event, distinct_id, person_id, timestamp, uuid, and from properties: $os/$os_version, $device_type, $app_version. Other $-prefixed properties are dropped the same way.

GA4 (via BigQuery export)

Admin → Product Links → BigQuery Links in the GA4 property, then query the events_* tables it creates. Export the query results as JSON (or JSONL) and set source: "ga4". Drengr reads event_name, event_timestamp (microseconds), user_pseudo_id, user_id, event_params (either already flattened or BigQuery's repeated [{key, value:{...}}] record shape — both are handled), and device.operating_system/.operating_system_version, device.mobile_model_name, app_info.version.

GA4's export has no canonical per-event id, so dedup falls back to sha256(user_pseudo_id + event_name + event_timestamp). Re-importing the exact same export twice is safe; re-running a query with any row-ordering or timestamp change is not guaranteed to dedup.

Generic (anything else)

Set source: "generic" and supply a mapping object telling Drengr which of your fields are which. Required: event_name_field, timestamp_field, timestamp_unit (one of iso8601/seconds/ms/us), user_id_field, anon_id_field. Optional: dedup_id_field, props_field (dot-path supported, e.g. "data.attrs"; omit it and every field not already consumed becomes a dim/measure).

json
{
  "source": "generic",
  "mapping": {
    "event_name_field": "type",
    "timestamp_field": "occurred_ms",
    "timestamp_unit": "ms",
    "user_id_field": "uid",
    "anon_id_field": "device_id",
    "dedup_id_field": "id"
  },
  "events": [
    { "id": "evt_1", "type": "checkout_completed", "occurred_ms": 1750000000000, "uid": "u_9", "device_id": "d_9", "amount": 19.99 }
  ]
}

Event mapping (all sources)

Source fieldsemantic_events fieldNotes
event name fieldevent_nameNormalized: lowercased, non-[a-z0-9_] runs collapsed to "_", capped 64 chars. Original kept in dims._source_event_name if changed.
event properties / user propertiesdims + measuresOne-level flatten (nested object → parent.child). Numeric → measures (≤20, revenue/total/price/quantity first); everything else → dims (≤40 keys, 64-char keys, 256-char values).
app_version / os / device model fieldsdims.app_version / dims.os / dims.device_modelVendor-specific field names, mapped per source (see table above).
anon id (device_id / distinct_id / user_pseudo_id / anon_id_field)install_id
user id (user_id / $user_id / person_id / user_id_field)external_id
email/phone/name/address/password/token/ssn/card/cvv-like keys(dropped)Same sensitive-key substring filter as every other door, applied before dims/measures are built.
dedup id (per-vendor, or dedup_id_field for generic)event_idFalls back to sha256(anon id + event name + raw timestamp) when the source has no canonical id (always true for ga4).

Response

json
200  { "imported": 118, "skipped": 2, "errors": [
       "event 4: missing event name",
       "event 17: timestamp out of range for event \"foo\""
     ] }

// GA4 imports also carry:
     { "imported": 118, "skipped": 0, "errors": [], "meta": {
         "dedup": "GA4 export has no canonical event id; dedup key = sha256(user_pseudo_id + event_name + event_timestamp)"
     } }

Troubleshooting

Check for a typo — the field is case-sensitive and must match exactly.