Skip to main content
When a new customer first connects your integration to their Virtuous organization, they typically have months or years of historical donation data sitting in your platform that should appear in Virtuous. This recipe covers the end-to-end process for that initial bulk load: the pre-flight planning that prevents most mistakes, the validation passes that catch errors before they hit Virtuous, the throttled submission that respects rate limits, and the reconciliation that confirms the load completed correctly. The architecture reuses the patterns from Sync External Donations into Virtuous. The differences from steady-state sync are scale, sequencing, and customer coordination — not architecture.

Why historical imports are different

Steady-state sync handles donations one at a time as they happen. Historical imports submit thousands or tens of thousands of donations in a compressed window. Three things change at that scale: A historical import that’s well-planned takes a day or two of preparation, a few overnight load runs, and a week of reconciliation. A poorly-planned one produces duplicates, surprises the customer’s team, and leaves a needs-update queue that takes weeks to drain.

Phase 1: pre-flight planning

Before writing any import code, work through these decisions with the customer’s team.

Define the date window

What’s the earliest gift date that should appear in Virtuous? Options:
  • Everything in your platform. Captures the full donor history but increases load size and may include data the customer considers obsolete.
  • From a specific cutoff date. Common choices: the customer’s last fiscal year start, the date of their previous CRM migration, the date your platform was first deployed for them.
  • Only gifts from the current fiscal year. Smallest load but loses historical context.
The right answer depends on the customer’s reporting needs. Most customers want at least 1–2 fiscal years of history; longer windows are appropriate for endowment-focused organizations or long-term donor research.

Map Projects and Campaigns

Every gift you load needs a projectCode. Walk through every Project referenced in your historical data and map it to a current Virtuous Project: Persist this mapping in your import configuration. Any gift whose project mapping is unresolved should fail validation, not silently fall back to a default — silent fallback hides import errors and leaves the customer’s analytics misleading.

Decide on receipt vs. gift date

For each gift in your platform, you’ll be writing one giftDate value to Virtuous. Two common interpretations:
  • Donor-action date. The date the donor made the donation (the day they clicked Donate). Matches giftDate in Virtuous’s normal usage.
  • Settlement date. The date the payment cleared in the customer’s bank — useful for accounting reconciliation but typically several days after the donor’s action.
The default is the donor-action date. Confirm with the customer’s accounting team if they have a strong preference. Don’t try to support both — picking one and using it consistently makes reconciliation tractable.

Plan the batch label

Every Gift Transaction has a batch field — a free-text label that groups gifts together for the customer’s organization. For historical imports, use a clear batch label like Historical-Import-2024-12 that lets the customer’s team filter the imported gifts in the Virtuous UI and lets your reconciliation queries scope to just the imported set.

Communicate with the customer

Before kicking off the load, send the customer’s team a brief explaining:
  • The total number of gifts you’ll load.
  • The expected timeframe (overnight, multi-night, etc.).
  • That a wall of giftCreate events will arrive in their dashboards over the following days as the nightly batch processes the imports.
  • The validation report you’ll produce and how to interpret it.
  • The reconciliation report you’ll produce after the load completes.
A surprised customer is the most common cause of an import being rolled back. A prepared customer treats the wall of gifts as expected and validates the data on their end.

Phase 2: validation passes

Run two validation passes before submitting anything to Virtuous. The goal is to surface every category of problem in a single batch so the customer can review and approve, rather than discovering problems gradually during the load.

Pass 1: structural validation

Iterate every historical gift and check that the required fields are present and well-formed:
JavaScript
Run this against every historical gift and produce a validation report:
Share this report with the customer’s team. Resolve the project mapping, get clarification on the donors missing both signals, and decide what to do with invalid amounts (typically: skip them and surface separately for manual entry).

Pass 2: contact resolution

For each donor in the import set, attempt to resolve their existing Virtuous Contact. This catches the donors who already exist (so the import will merge rather than create) and surfaces donors whose existence is unclear.
JavaScript
Categorize each donor:
  • Matched by reference. Existing Contact will be matched cleanly. No risk of duplicate.
  • Matched by email. Existing Contact will be matched, but the import will add your referenceSource/referenceId to the existing record. Confirm this is what the customer wants.
  • No match. A new Contact will be created. These are the donors that produce contactCreate events during the import.
The output is a second validation report covering contact resolution. For very large imports, this pass can itself be rate-limit-sensitive — each unique donor produces one lookup. Throttle accordingly.

Phase 3: the load

With validation complete and the customer informed, execute the load.

Throttling

The CRM+ rate limit is 1,500 requests per hour per Virtuous organization — see Rate Limits. A historical import of 24,000 gifts requires at least 16 hours of submissions even at the maximum rate. Two patterns keep the load within the limit while leaving headroom for the customer’s other integrations:
JavaScript
Two patterns this gets right:
  • Target below the limit. Aiming for 1,200/hour instead of 1,500 leaves 20% headroom for the customer’s other integrations (steady-state donation sync, manual user actions, other partner integrations).
  • Throttle per-request, not per-batch. Submitting 1,000 requests in 30 seconds then idling for 30 minutes burns the rate limit in bursts and produces inconsistent latency on retries. Pace evenly.
For multi-night imports, run the load during off-peak hours (overnight in the customer’s timezone) when the steady-state integration traffic is lower.

Resumability

A 16-hour load needs to be resumable from interruptions — network blips, your worker restarting, the customer pausing for review. Persist progress at each submission:
The worker reads status = 'pending' records in order, submits, and updates status after each. An interrupted load resumes by re-reading the pending set from where it left off — no records resubmit unless they were never marked submitted.

Idempotency through transactionId

As with steady-state sync, use a stable transactionId derived from your platform’s identifier. If a single submission’s success was acknowledged but lost in transit, the retry produces no duplicate — Virtuous’s matching algorithm recognizes the same transactionSource/transactionId pair and resolves to the same Transaction.
JavaScript

Phase 4: post-load reconciliation

After the load completes, run a full reconciliation pass before declaring the import done.

Confirm the count

Count the gifts you intended to load vs. the gifts that appear in Virtuous:
JavaScript
Discrepancies indicate Transactions stuck in needs-update or webhook deliveries that never arrived. Use the patterns from Reconcile Failed Syncs to investigate.

Confirm the sums

For each Project mapped in the import, sum the imported gift amounts and compare against your platform’s totals:
Sum-level reconciliation catches discrepancies that count-level reconciliation misses — for example, if two gifts merged into one through unexpected matching, the counts diverge by one but the sums tell you the financial impact.

Confirm the donor side

Run a separate reconciliation against contact creation:
JavaScript

Customer sign-off

Send the customer a final reconciliation report and request explicit sign-off before treating the imported gifts as part of normal data. Most customers want the import in a holding state — visible but not yet in receipts or year-end statements — until their accounting team has validated the totals. The batch label makes this easy: configure the customer’s receipt and statement workflows to exclude Historical-Import-* batches until the import is confirmed, then enable the inclusion once they sign off.

Operational considerations

Pausing the load

If the customer requests a pause mid-load, the worker stops processing new records but lets in-flight submissions complete. Resume by restarting the worker — it picks up from the next pending record. Do not deactivate the Virtuous webhook subscription during a pause — the nightly batch is processing the gifts you already submitted, and you need the giftCreate events to confirm them.

Handling needs-update fallout

A typical historical import produces a needs-update queue larger than steady-state sync — gifts whose embedded contact data couldn’t be cleanly matched. Plan for the customer’s team to spend several hours over the week following the import resolving the bucket. For very large imports (50,000+ gifts), the needs-update fallout can be days of customer-team work. Mitigate by improving the matching signal in pre-flight (Pass 2 above) — every donor resolved before submission is one fewer needs-update item.

Logging the import for audit

Every imported gift’s submission, status transitions, and final disposition should be logged. Customers occasionally need to demonstrate that a historical gift was imported (vs. manually entered) — typically during audit or compliance review. The batch label is the first line of audit; a detailed log in your historical_import_state table is the second.

End-to-end checklist

Before considering a historical import complete, confirm:
  • Validation Pass 1 (structural) ran and the customer reviewed the report.
  • Validation Pass 2 (contact resolution) ran and the customer approved the categorization.
  • The Virtuous batch label is consistent across every submitted gift.
  • The load throttled below the rate limit and left headroom for steady-state traffic.
  • The load is resumable from any interruption.
  • Post-load count reconciliation matches.
  • Post-load sum reconciliation matches per Project.
  • Donor-side reconciliation matches.
  • Needs-update fallout is surfaced to the customer’s team with a resolution path.
  • Customer signed off on the imported data before it flows into receipts or statements.

Where to go next

Stripe to Virtuous CRM

The steady-state sync recipe that complements this historical import for ongoing donations.

Reconcile Failed Syncs

Handle the needs-update fallout and any other discrepancies surfaced during reconciliation.

Sync External Donations into Virtuous

The general architecture this historical import reuses.

Rate Limits

The constraint that drives the throttling pattern in Phase 3.
Last modified on May 21, 2026