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.
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:
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
- 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.
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).
{
"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 field | semantic_events field | Notes |
|---|---|---|
| event name field | event_name | Normalized: lowercased, non-[a-z0-9_] runs collapsed to "_", capped 64 chars. Original kept in dims._source_event_name if changed. |
| event properties / user properties | dims + measures | One-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 fields | dims.app_version / dims.os / dims.device_model | Vendor-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_id | Falls back to sha256(anon id + event name + raw timestamp) when the source has no canonical id (always true for ga4). |
Response
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.