---
title: "Import your data into TimeRetain"
url: "https://timeretain.com/import"
description: "Bring spreadsheet or app export data into TimeRetain by having an AI assistant convert it to the supported import format."
---

# Import your data into TimeRetain

TimeRetain imports time data from a JSON file. The fastest way to get one is to have an AI assistant convert your existing data for you.

1.  Export your data from your current tool as CSV or a spreadsheet.
2.  Send that file to an AI assistant with the prompt below to produce a TimeRetain JSON file.
3.  Open [Data & Sync](/data-sync), choose Import, and upload the JSON file.

Re-importing is safe: existing entries are updated, not duplicated.

Convert the attached file to TimeRetain v0.6 JSON using the specification below. Use deterministic IDs derived from each source row's stable key so re-imports update existing entries instead of duplicating them. The resulting file is imported in TimeRetain through Data & Sync → Import.

## TimeRetain v0.6 JSON import

Use this specification when converting external time tracking data into a TimeRetain JSON file.

## Conversion recipe

1. Choose a stable external row key from the source file, such as an original row ID. If none exists, build one from stable source columns like date, start time, end time, project, task, and description.
2. Derive TimeRetain IDs deterministically from that external key: \`first 16 bytes of SHA-256(utf8(externalId))\`, encoded as URL-safe Base64 without padding. Use distinct seeds for related rows, such as \`stopwatch:<externalId>\`, \`interval:<externalId>\`, and \`tag:<tagName>\`.
3. Set the envelope \`version\` to \`"0.6"\` and \`timestamp\` to an ISO-8601 string.
4. Include all five arrays inside \`data\` in this exact order: \`interval\`, \`preferences\`, \`stopwatch\`, \`tag\`, \`tagStopwatch\`. Arrays may be empty.
5. Use Unix time in milliseconds for \`interval.start\` and \`interval.end\`. Use \`null\` for \`interval.end\` only when the entry is still in progress.
6. Use \`0\` or \`1\` for every \`Bool\` field. Do not use JSON booleans.
7. Compute \`checksum\` as lowercase hex SHA-256 of \`JSON.stringify(data)\`, where \`data\` is the inner data object with the key order above.

Rows are upserted by \`id\`, so deterministic IDs make re-imports idempotent.

## Types

- \`Id\` — 22-character URL-safe Base64 string using \`A-Z\`, \`a-z\`, \`0-9\`, \`-\`, and \`\_\`, with no padding. It encodes 16 bytes.
- \`Bool\` — \`0\` or \`1\`, never \`false\` or \`true\`.
- Row timestamps (\`interval.start\`, \`interval.end\`) — Unix time in milliseconds. The envelope \`timestamp\` is an ISO-8601 string. All numbers must be finite.

## Envelope

\`\`\`json
{
  "version": "0.6",
  "timestamp": "<ISO-8601 string>",
  "checksum": "<sha256 hex of JSON.stringify(data)>",
  "data": {
    "interval": \[\],
    "preferences": \[\],
    "stopwatch": \[\],
    "tag": \[\],
    "tagStopwatch": \[\]
  }
}
\`\`\`

All five keys inside \`data\` must be present and appear in exactly this order: \`interval\`, \`preferences\`, \`stopwatch\`, \`tag\`, \`tagStopwatch\`.

## Schema

\`\`\`ts
interval: {
  id: Id;
  stopwatchId: Id; // -> stopwatch.id
  start: number; // Unix ms
  end: number | null; // Unix ms, or null if in progress
}

preferences: {
  // exactly one row
  id: Id;
  useMilitaryTime: Bool;
  mergeOverlappingStopwatches: Bool;
  useAdjust: Bool;
  adjustDefault: number; // minutes; may be 0 or negative
  backupReminderIntervalInDays: number;
  currencyCode: string; // ISO 4217, for example "USD"
  earningsTrackingEnabled: Bool;
  defaultHourlyRate: number | null;
  weekStartsOn: number; // 0 = Sunday ... 6 = Saturday
}

stopwatch: {
  id: Id;
  description: string;
  adjustMinutes: number; // integer; may be negative
  isActive: Bool;
  isExpanded: Bool;
  hourlyRate: number | null;
  isHourlyRateManual: Bool | null;
  targetMinutes: number | null;
}

tag: {
  id: Id;
  name: string;
  hourlyRate: number | null;
  color: string | null; // Open Color token like "blue.6", or null
}

tagStopwatch: {
  id: Id;
  tagId: Id; // -> tag.id
  stopwatchId: Id; // -> stopwatch.id
}
\`\`\`

## Checksum

\`envelope.checksum\` is the lowercase hex SHA-256 digest of \`JSON.stringify(data)\`, where \`data\` is the inner object, not the whole envelope.

JavaScript:

\`\`\`js
import { createHash } from "node:crypto";

const checksum = createHash("sha256")
  .update(JSON.stringify(data))
  .digest("hex");
\`\`\`

Python:

\`\`\`python
import hashlib
import json

payload = json.dumps(data, separators=(",", ":"), ensure\_ascii=False)
checksum = hashlib.sha256(payload.encode("utf-8")).hexdigest()
\`\`\`

## Minimal example

\`\`\`json
{
  "version": "0.6",
  "timestamp": "2026-04-17T12:00:00.000Z",
  "checksum": "e2ecf25ad6c366cbd64de01217fc3b64b506d562343575ad167b040a6e6167d7",
  "data": {
    "interval": \[
      {
        "id": "rF6t5hR0iYn7wqUXq2m4tw",
        "stopwatchId": "cAwTJAcSxBjZ8rB5DZqq6Q",
        "start": 1763884800000,
        "end": 1763890200000
      }
    \],
    "preferences": \[
      {
        "id": "rJMgXrjZ6aMBon4IFsnwYg",
        "useMilitaryTime": 0,
        "mergeOverlappingStopwatches": 1,
        "useAdjust": 0,
        "adjustDefault": 0,
        "backupReminderIntervalInDays": 7,
        "currencyCode": "USD",
        "earningsTrackingEnabled": 0,
        "defaultHourlyRate": null,
        "weekStartsOn": 1
      }
    \],
    "stopwatch": \[
      {
        "id": "cAwTJAcSxBjZ8rB5DZqq6Q",
        "description": "Client work",
        "adjustMinutes": 0,
        "isActive": 0,
        "isExpanded": 0,
        "hourlyRate": null,
        "isHourlyRateManual": null,
        "targetMinutes": null
      }
    \],
    "tag": \[\],
    "tagStopwatch": \[\]
  }
}
\`\`\`

## Complete tagged example

\`\`\`json
{
  "version": "0.6",
  "timestamp": "2026-04-17T12:00:00.000Z",
  "checksum": "df9350a5d1b2296df7e0f829d4abec2642eba60aa5d28402858556d6129a0835",
  "data": {
    "interval": \[
      {
        "id": "YW9roj4Umr06HK2c\_oGbRQ",
        "stopwatchId": "TJ3LRvQ2CG4JNG6IeMSKCQ",
        "start": 1763884800000,
        "end": 1763890200000
      }
    \],
    "preferences": \[
      {
        "id": "mraKnXn4mDdyepbJHuq5Gg",
        "useMilitaryTime": 0,
        "mergeOverlappingStopwatches": 1,
        "useAdjust": 0,
        "adjustDefault": 0,
        "backupReminderIntervalInDays": 7,
        "currencyCode": "USD",
        "earningsTrackingEnabled": 1,
        "defaultHourlyRate": null,
        "weekStartsOn": 1
      }
    \],
    "stopwatch": \[
      {
        "id": "TJ3LRvQ2CG4JNG6IeMSKCQ",
        "description": "Write the report",
        "adjustMinutes": 0,
        "isActive": 0,
        "isExpanded": 0,
        "hourlyRate": null,
        "isHourlyRateManual": 0,
        "targetMinutes": null
      }
    \],
    "tag": \[
      {
        "id": "l-YnOENRtutJUAqR9DhS7Q",
        "name": "work",
        "hourlyRate": null,
        "color": null
      }
    \],
    "tagStopwatch": \[
      {
        "id": "SP2mdxvegNECgyj6wf1SjA",
        "tagId": "l-YnOENRtutJUAqR9DhS7Q",
        "stopwatchId": "TJ3LRvQ2CG4JNG6IeMSKCQ"
      }
    \]
  }
}
\`\`\`

## Common mistakes

- Using \`true\` or \`false\` instead of \`0\` or \`1\` for \`Bool\` fields.
- Using seconds instead of milliseconds for \`interval.start\` or \`interval.end\`.
- Using ISO strings for row timestamps. Only the envelope \`timestamp\` is an ISO string.
- Forgetting the checksum or computing it from the whole envelope instead of the inner \`data\` object.
- Pretty-printing or alphabetically sorting \`data\` before checksum calculation. Use the exact object key order shown above.
- Omitting one of the five \`data\` arrays, even when it is empty.
- Creating tags without matching \`tagStopwatch\` rows for tagged stopwatches.