Developers
Importing historical data
Bring the analytics you collected before VisitTrack — pageviews and custom events, with their original dates — so your charts start where your history does.
If you tracked your site with another tool before installing VisitTrack, you can import that history. Each event keeps its original timestamp, visitors and sessions are rebuilt the same way live traffic builds them (sessions split after 30 minutes of inactivity unless you send your own session id), and referrers are classified into the same channels — search, social, AI, referral, direct. Imports are idempotent: sending the same events again inserts nothing new.
History stops where live tracking starts
Events at or after your site's first live event are skipped, so imported data never double-counts days the tracking script already covers. You can set an explicit cutoff with until if you need a different boundary.
The event format
One JSON object per event. id, timestamp, type and visitorId are required; name is required for type "event", path for type "pageview". Everything else is optional.
{"id":"evt_1","timestamp":"2025-03-01T12:00:00Z","type":"pageview","path":"/pricing","visitorId":"anon_42","sessionId":"s_9","referrer":"https://www.google.com/","utmSource":"newsletter","utmMedium":"email","utmCampaign":"launch","countryCode":"US","region":"California","city":"San Francisco","device":"desktop","browser":"Chrome","os":"macOS"}
{"id":"evt_2","timestamp":"2025-03-01T12:04:10Z","type":"event","name":"signup","path":"/register","visitorId":"anon_42","userId":"usr_8f2a1","props":{"plan":"pro"}}| Field | Rules |
|---|---|
| id | Required. Your unique id for the event, up to 200 characters. Together with source it makes the import idempotent. |
| timestamp | Required. ISO-8601 with a timezone (Z or ±hh:mm). Not in the future, not before 2000, and before the cutoff. |
| type | Required. "pageview" or "event". |
| name | Required for "event", up to 120 characters — the same name your goals and funnels match. |
| path | Required for "pageview". Starts with "/", up to 500 characters. |
| visitorId | Required. Your anonymous visitor or device id, up to 200 characters. One visitor per id. |
| userId | Optional. Your own user id — shown as the visitor's external id, like identify(). |
| sessionId | Optional. Without it, sessions are split after 30 minutes without activity. |
| referrer, utmSource, utmMedium, utmCampaign | Optional. The visitor's first event sets their first touch. |
| countryCode, country, region, city | Optional. countryCode is ISO 3166-1 alpha-2 (US, DE…); country is filled in from it when omitted. |
| device, browser, os | Optional. device is desktop, mobile or tablet. |
| props | Optional, events only. Up to 10 keys ([a-z0-9_-]), values cut to 255 characters — same as live custom events. |
An event that breaks a rule is skipped and reported with the reason; the rest are still imported. Imported visitors count as people unless their browser or os field is obviously a bot ("Googlebot", "HeadlessChrome"…).
Import through the API
Create a key with "Allow importing historical events" checked in Settings → API / MCP (the write:import scope), then POST batches of up to 1000 events to /api/v1/import. source is a short label for where the data comes from — keep it the same across every batch, because it's part of each event's id. Send dryRun: true first to see what would be accepted without writing anything.
curl -X POST https://visitrack.app/api/v1/import \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "ga4",
"dryRun": true,
"events": [
{ "id": "evt_1", "timestamp": "2025-03-01T12:00:00Z", "type": "pageview", "path": "/pricing", "visitorId": "anon_42" },
{ "id": "evt_2", "timestamp": "2025-03-01T12:04:10Z", "type": "event", "name": "signup", "visitorId": "anon_42", "userId": "usr_8f2a1" }
]
}'{
"site": { "id": "…", "domain": "example.com" },
"dryRun": true,
"source": "ga4",
"until": "2025-06-01T09:12:44.000Z",
"untilReason": "first-live-event",
"accepted": 2,
"duplicates": 0,
"skipped": [],
"visitors": { "total": 1, "new": 1 },
"sessions": { "total": 1, "new": 1 },
"range": { "from": "2025-03-01T12:00:00.000Z", "to": "2025-03-01T12:04:10.000Z" }
}- Batching: at most 1000 events per request and 60 requests a minute per key. Loop over your export in chunks; if a request fails, just send it again — events already imported count as duplicates, not errors.
- Send each visitor's events in time order when you can. Sessions are stitched across batches, but one visitor's events in one batch is the cleanest split.
- skipped lists every rejected event by its index in your events array, with the reason.
- until (optional, ISO-8601) overrides the cutoff. Without it the cutoff is your first live event; untilReason says which applied.
- MCP: a key with write:import also gets an import_events tool with the same input. It's meant for small batches an assistant assembles in conversation; use the REST endpoint for bulk history.
Send us a file instead
For a large export (hundreds of thousands of events or more), save it as NDJSON — the format above, one event per line — and send it to support. We import it with the same rules, run a dry run first and send you its report (events per day, invalid lines with line numbers and reasons, the date range) before anything is written.
Something missing? Tell us.
AI agent or LLM? Read this page as markdown.