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:| Concern | Steady-state | Historical import |
|---|---|---|
| Rate limit pressure | Submissions are paced by donor activity — typically dozens to low hundreds per hour. | Submissions can hit the 1,500/hour limit immediately if not throttled. |
| Webhook wave | A handful of giftCreate events at a time. | Thousands of giftCreate events when the nightly batch processes the import. |
| Customer-visible impact | Each gift appears as it occurs — invisible in the UI’s “recent activity” view. | A wall of historical gifts appears at once in the customer’s UI, dashboards, and reports — confusing without communication. |
| Data quality fallout | Edge cases surface gradually. | Edge cases surface in bulk during validation, then resurface in bulk as needs-update fallout. |
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.
Map Projects and Campaigns
Every gift you load needs aprojectCode. Walk through every Project referenced in your historical data and map it to a current Virtuous Project:
| Your platform’s project label | Maps to Virtuous projectCode | Notes |
|---|---|---|
| ”General Fund” | GEN-FUND | Customer’s standard general fund. |
| ”Clean Water 2022” | CLEAN-WATER | Customer consolidated annual campaigns into one Project. |
| ”Holiday Match 2021” | GEN-FUND | Customer doesn’t track this anymore — fall back to general. |
| ”International” | needs-update | Customer wants a manual review for these. |
Decide on receipt vs. gift date
For each gift in your platform, you’ll be writing onegiftDate value to Virtuous. Two common interpretations:
- Donor-action date. The date the donor made the donation (the day they clicked Donate). Matches
giftDatein 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.
Plan the batch label
Every Gift Transaction has abatch 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
giftCreateevents 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.
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
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
- 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/referenceIdto the existing record. Confirm this is what the customer wants. - No match. A new Contact will be created. These are the donors that produce
contactCreateevents during the import.
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
- 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.
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: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
Confirm the sums
For each Project mapped in the import, sum the imported gift amounts and compare against your platform’s totals: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 excludeHistorical-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 nextpending 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. Thebatch 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
batchlabel 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.