User Guides

Migrate from Harvest

Edit page
Preview and import a Harvest account into your portal without duplicate records or invoice emails.

The toolkit includes a one-time Harvest migration command. Install matching versions of Core, Clients, Timesheets, Invoices, and Invoice Timesheets, run their migrations, and enable Timesheets and Invoices for the provider before importing. Run commands from the host application with its DATABASE_URL available. The command requires direct database access and should run only in a trusted administrator environment.

Export your account

Create a personal access token for a Harvest administrator at Harvest Developers. Supply HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID through your environment or secret manager. Do not pass the token as a command argument or commit it. For a host using dotenv, use node --import dotenv/config node_modules/@nuxt-customer-portal/kit/bin/nuxt-customer-portal.mjs in place of the command below.

Stop timers and pause edits in Harvest until the export completes: Harvest does not offer a cross-endpoint snapshot transaction. All records, including inactive clients and archived projects, are requested. Administrator access is required to avoid silently exporting only a subset of time entries.

pnpm exec nuxt-customer-portal harvest export --output harvest-export.json

The export uses only GET requests and handles pagination and bounded rate-limit retries. The output is created with owner-only file permissions and never overwrites an existing file. It contains personal and financial data, but no API token. Keep it outside version control. Preserve this exact export for preview, import, and any retries; a new export is a different migration input.

Map people and existing clients

Create a JSON mapping file. Keys are Harvest IDs, visible in the export. Map existing provider members explicitly by email. To retain everyone else as a distinct historical identity, set createHistoricalUsers to true. Each unmapped Harvest user becomes a separate banned member with their original name and email, without login credentials or invitations; time entry is disabled. Even two people records with the same name remain separate. Existing email collisions require an explicit mapping and are never merged automatically. The original active state remains in the source journal.

{
  "createHistoricalUsers": true,
  "users": {
    "12345": "owner@example.com"
  },
  "clients": {
    "67890": "existing-portal-client-organization-id"
  }
}

The clients section is optional. Unmapped clients are created as new organization clients; mapped clients retain their existing profiles and access settings. Client name matching is deliberately not automatic. Duplicate project names within the source receive a deterministic Harvest ID suffix, retaining their separate identities and original names in the source journal. Contacts with the same client and email are consolidated into one contact with their names retained; every original contact record remains in the journal. The preview lists these transformations. Existing target conflicts, duplicate task names or invoice numbers, and overlapping existing timesheet weeks still block import. Client portal access is disabled for new workspace links, and no invoice viewers are granted.

Preview and import

The actor must be an existing provider owner or administrator. Preview is the default and runs inside a read-only database transaction. It reports planned record counts, time rounding, approximate invoice-line links, and blocking conflicts. A preview with errors exits unsuccessfully.

pnpm exec nuxt-customer-portal harvest import \
  --input harvest-export.json --mapping harvest-mapping.json \
  --provider my-agency --actor owner@example.com

The preview also lists any required package migrations without applying them. Install and run the new Timesheets and Invoices migrations and deploy the corresponding Invoice Timesheets bridge before importing: these add billing protection and source provenance. Back up the target database and rehearse against a restored copy. Review the preview, including warnings, before applying the same export and mapping:

pnpm exec nuxt-customer-portal harvest import \
  --input harvest-export.json --mapping harvest-mapping.json \
  --provider my-agency --actor owner@example.com \
  --apply --backup-confirmed

Import is one database transaction: any conflict or insert failure rolls back the entire run. Concurrent imports are serialized and database constraints still protect against application writes. Stop portal edits during the final import. The successful import records its fingerprint, mappings, and complete source snapshot in nuxt_customer_portal_migrations.harvest_import. Identical retries are skipped; changed source data or mappings require manual reconciliation. This is a migration, not an ongoing synchronization service. Keep the database journal when upgrading the portal.

What is preserved

  • Clients, addresses, VAT numbers, active/archive state, and billing contacts.
  • Projects, codes, dates, project-hour budgets, fixed fees, activity types, and project task links. Other budget and tariff settings remain in the source snapshot for manual setup.
  • Work dates, notes, billable flags, historical hourly rates, and Harvest approval status. Hours round to the portal's minute precision and every rounding difference is reported. Submitted and approved entries receive individual historical submissions; weekly containers remain draft so new time is not implicitly approved.
  • Original invoice creation, issue, payment and write-off timestamps are recorded as history events when provided by Harvest. Original invoice creators and payment recorders are mapped where available; unresolved source attribution remains in the retained source metadata. Import adds its own separately dated audit event.
  • Invoice numbers, issue/due dates, currency, subjects, notes, draft/open/paid state (closed invoices become written off), credit note document types and signed values, line quantities/prices, exact tax amounts, discounts, payment dates/amounts/references, and billed time links. Harvest exposes invoice-level time associations; line associations use the matching project line when unique and otherwise the first invoice line, with a warning.

Import writes historical records directly and does not invoke invoice delivery, payment gateways, invitation flows, or approval email handlers. Original Harvest records remain available in the journal. Sender details use the provider's current invoice settings and recipient details use the exported client profile; these are not a reconstruction of historical PDF snapshots.

Limits that require review

Retainer invoices, currencies without two decimal minor units, unreconciled balances, and amounts beyond the portal's precision block import. All Harvest billed time retains an explicit externally_billed marker, even when no invoice link exists or its invoice is written off. Such time remains billable in historical reports but is excluded from invoice source selection, rejected on direct invoice creation, and protected against editing/reopening. No synthetic invoices or payments are created. Source invoice and entry metadata is retained on the imported records as well as in the journal; it is not exposed through the regular invoice API. Resolve these cases before importing; do not delete records from the export merely to bypass validation.

Invoice PDFs, attachments, original email messages, estimates, expenses, recurring invoice schedules, project user assignments, and cost rates are not migrated into portal features. Preserve those separately in Harvest before closing the account. Expense invoice lines remain ordinary invoice lines, and original source metadata returned by the API is retained in the journal. Verify representative invoices and totals per currency, payment balances, time totals per person/project, and billed-time exclusion before using the portal for new invoices.

API references: authentication, time entries, invoices, and payments.