Skip to content

Import historical data

Switching to CanaryLytics doesn’t mean starting your charts from zero. POST /v1/import accepts historical events with their original timestamps, so months of existing data land in the same project your snippet reports to, and the dashboard shows one continuous history.

Every night, a retention job deletes events older than your project’s retentionDays (default 90). History you import past that window is accepted, then deleted the following morning. Raise retention on the dashboard’s Settings page before importing, up to the 365-day maximum.

Import takes the same columns /v1/export emits. Only four fields are required per event:

Field Notes
createdAt Original timestamp, ISO 8601. Future timestamps (beyond a 60-second clock-skew allowance) are rejected per row
sessionId UUID; events sharing one count as one session
eventType page_view, link_click or custom
path The page path

Everything else is optional: geo columns (country, region, city, …), classification flags (isBot, isVpn, isInternal), locale, trafficSource, UTM columns, and userAgent. Omit browser / os / deviceType and they are derived from userAgent at import time with the same classifier live ingest uses.

Send batches of up to 5000 as JSON, or as NDJSON with one event per line:

Terminal window
curl -X POST "https://api.analytics.canarycoders.es/v1/import" \
-H "Authorization: Bearer cly_live_…" \
-H "Content-Type: application/json" \
-d '{"events":[{"createdAt":"2026-05-01T10:00:00Z","sessionId":"e073d59a-…","eventType":"page_view","path":"/pricing","country":"ES","locale":"es"}]}'
{ "received": 1, "inserted": 1, "skippedInvalid": 0, "skippedDuplicate": 0 }

The response accounts for every row you sent. skippedInvalid rows failed validation (bad enum, future timestamp, malformed UUID); the rest inserted or deduplicated.

Give each event a stable id (a UUID from your source store) and inserts become ON CONFLICT DO NOTHING: a crashed import can be restarted from the top, and already-imported rows report as skippedDuplicate instead of double-counting. Without ids, every row inserts fresh: run once, or dedupe yourself.

An NDJSON export round-trips directly, so moving a project (or seeding a staging copy) is two calls:

Terminal window
curl "https://api.analytics.canarycoders.es/v1/export?window=90d&format=ndjson&limit=5000" \
-H "Authorization: Bearer cly_live_OLD…" -o events.ndjson
curl -X POST "https://api.analytics.canarycoders.es/v1/import" \
-H "Authorization: Bearer cly_live_NEW…" \
-H "Content-Type: application/x-ndjson" \
--data-binary @events.ndjson

Exports carry row ids, so repeating a page is harmless. Follow the X-Next-Cursor header to page through exports larger than one request.

import { createClient } from "@canarylytics/sdk";
const lytics = createClient({ apiKey: process.env.CANARYLYTICS_KEY });
const result = await lytics.importEvents(batch); // ≤ 5000 per call
console.log(`${result.inserted} inserted, ${result.skippedDuplicate} already there`);

Imported rows are first-class events: they appear in the timeseries, top pages, countries, locales, sessions, and exports, and session counting works as long as your source had session ids. Two honest limits:

  • Traffic sources: a session’s source is classified from the referrer at ingest time. If your old store didn’t keep a source label, imported sessions read as direct; that information can’t be reconstructed later.
  • Cut over cleanly: import history up to the moment the snippet went live on the domain. Overlapping windows double-count sessions, because the old store’s session ids and the snippet’s are different.