This page is for one situation: your product is already instrumented with Mixpanel's SDKs, and you are moving it to Analytics. Rewriting every track call before you can see a single event would be a bad first week, so the service accepts Mixpanel's ingest surface at https://t.intelligent.page/mixpanel. Change the host and the token, and your existing instrumentation reports into a Project the same day, while you replace it with our own SDKs at your own pace.
This is a bridge, not a destination. New instrumentation belongs on Web, Node and other servers, Mobile or the HTTP API. The bridge is frozen at what Mixpanel's own SDKs send: it gains no feature of ours, and anything we add later lands on our SDKs first.
At a glance
Move in this order
- Create a Project and take its Project token and one Server key. See Tokens and keys.
- Point your current SDK at the bridge. One line:
api_hoston web,serverURLon the mobile libraries, the consumer's API host on the server libraries. Use the Project token in place of the Mixpanel project token. Ship it. - Watch the events arrive on
GET /v1/liveor the Live view, and check that the names and the distinct ids are what you expect. - Backfill history through
/mixpanel/importwith a Server key, or through/v1/importif you are exporting to our own shape. Timestamps are kept exactly as sent, and a stable$insert_idper record makes a re-run idempotent. - Replace the SDK. Swap Mixpanel's client for
@intelligent-page/analytics-web,@intelligent-page/analytics-nodeor the mobile package for your platform, one surface at a time. The events are identical on the wire, so a half-migrated app reports one consistent stream. - Rebuild the reports you care about. Report definitions do not transfer; the events do.
Run both destinations in parallel for a week if the numbers matter. Because $insert_id maps to our id, replaying the same export twice changes nothing.
The host override
mixpanel.init('ipa_pub_REPLACE_ME', {
api_host: 'https://t.intelligent.page/mixpanel',
});
mixpanel.track('Signed Up', { plan: 'free' });
Replace the Mixpanel project token with your Project token and the host with ours. Nothing else in the instrumentation you already have changes: event names, property names and the people and group calls all keep working.
The host must include /mixpanel. The bare root paths that Mixpanel's own API uses (/track, /engage, /groups, /decide) were removed on 2026-09-29: they are another vendor's URL design, and our API root belongs to our own routes. Every SDK with a host override reaches the bridge at the prefixed host.
Routes
Each path is registered with and without a trailing slash, because mixpanel-js posts to <api_host>/track/ while the mobile and server libraries post to /track. GET is accepted on the event routes because the pixel transport and older mixpanel-js builds put data= on the query string.
Encodings
All three of the encodings in the wild are read, so no SDK needs to be configured differently:
application/x-www-form-urlencodedbodydata=<base64 of the JSON>. Used by mixpanel-js over XHR and by the iOS, Android, Flutter and Unity SDKs.- The same
data=pair as atext/plainbody (thesendBeacontransport) or on the query string. - A plain JSON body: one object or an array of them. Used by the server libraries and by
/import.
data= is decoded whether it is standard base64, url-safe base64, padded or not, or percent-encoded JSON. A form decode that turned an unescaped + into a space is undone before the base64 read, which is the usual reason a hand-built request fails elsewhere.
Query flags are honoured: verbose=1 (JSON answer), ip=1, strict=1 and project_id= on /import, _= cache busters, and api_key= as the legacy /import credential.
Authentication
/mixpanel/track, /mixpanel/engage and /mixpanel/groups accept a Project token or a Server key. /mixpanel/import requires a Server key: a Project token there is refused, because an import may write historical timestamps.
If a Project pins its allowed origins, a browser call carrying a Project token from another Origin is refused, exactly as on /v1/ingest.
Answers
Mixpanel's own answer shapes, so an SDK's retry logic behaves as it expects.
A rejected /mixpanel/track is 200 with the body 0, which is Mixpanel's own behaviour: SDKs have no retry path for a 4xx and would drop the batch. Our own SDKs do not work this way; they get real statuses from /v1/ingest.
/mixpanel/import with HTTP basic auth answers the modern JSON shape:
{ "code": 200, "num_records_imported": 1500, "status": "OK" }
and on failure { "code": 401, "error": "Unauthorized, invalid project token or secret", "status": "Unauthorized" }. With strict=1, a request containing any unreadable record is rejected whole rather than partially accepted. With the legacy api_key= credential, /mixpanel/import answers 1 / 0 like /mixpanel/track.
/mixpanel/decide answers a fixed, empty configuration so SDKs stop asking:
{
"status": 1,
"error": null,
"notifications": [],
"automatic_events": false,
"config": { "enable_collect_everything": false },
"flags": {}
}
In-app messages, A/B tests and Mixpanel's own autocapture are Mixpanel features we do not implement. The empty answer turns them off cleanly rather than leaving an SDK retrying. Our own autocapture is a separate thing and is on by default in the web SDK.
Property mapping
A Mixpanel record becomes one of our events. Everything below is applied by the service; you do not rename anything.
Arrays and nested objects, which Mixpanel allows and our event contract does not, are kept as compact JSON strings rather than discarded. Strings are cut at 512 characters, and at most 40 properties per event survive.
Read the table the other way round when you rewrite a call on our SDKs: what you send as props there is what arrives in props here, so the two paths land in the same columns and a report does not care which one produced an event.
Identity and profile calls
We store the operation rather than applying every Mixpanel semantic: $set and $set_once both merge into the person's profile, and $add, $union, $append, $remove and $unset are recorded with their op so the intent is not lost, but they do not increment or union server-side. If you depend on $add as a counter, count in a report instead. See Reserved events.
What the bridge does not carry across
- Reads. Mixpanel's query, export and JQL APIs have no equivalent here. Read through our own API instead.
/decidefeatures. In-app messages, surveys, A/B tests and Mixpanel autocapture: off, by the fixed empty answer above.- Server-side profile arithmetic.
$addand$unionare recorded, not applied. - Group analytics reports. Group events are stored; the reports over them are ours.
- Our own features. Super properties as we define them, the consent gate, the persisted offline queue and autocapture belong to our SDKs. That is the reason step 5 exists.