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.
Before you import: raise retention
Section titled “Before you import: raise retention”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.
The wire format
Section titled “The wire format”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:
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.
Re-runs are safe: send row ids
Section titled “Re-runs are safe: send row ids”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.
Project-to-project migration
Section titled “Project-to-project migration”An NDJSON export round-trips directly, so moving a project (or seeding a staging copy) is two calls:
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.ndjsonExports carry row ids, so repeating a page is harmless. Follow the X-Next-Cursor header to page through exports larger than one request.
From the SDK
Section titled “From the SDK”import { createClient } from "@canarylytics/sdk";
const lytics = createClient({ apiKey: process.env.CANARYLYTICS_KEY });const result = await lytics.importEvents(batch); // ≤ 5000 per callconsole.log(`${result.inserted} inserted, ${result.skippedDuplicate} already there`);What history can and can’t say
Section titled “What history can and can’t say”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.