Why duplicates happen
Even with the recommended Transaction-pattern integration, four scenarios produce duplicate records:| Cause | Affects |
|---|---|
| Insufficient matching signal | Two Contact Transactions arrive for the same donor but with no overlapping identifiers (different platforms, no shared email, no referenceSource/referenceId). The matching algorithm can’t connect them. |
| Direct create without lookup | POST /api/Contact does not check for duplicates — it creates a new record unconditionally. Any integration using direct create without its own lookup logic produces duplicates. |
Unstable transactionId | A Gift Transaction retried with a freshly-generated transactionId instead of the original is seen as a new gift by Virtuous. |
| Concurrent writes from multiple sources | The customer’s data is written by your integration, by another partner, and by manual entry in the Virtuous UI. Two systems creating the same donor at the same moment can both succeed before either sees the other. |
transactionId, include strong matching signals). Some are not (manual entry, third-party integration behavior). Your integration needs both prevention and detection.
Contact duplicates vs. Gift duplicates
The two duplicate classes are handled differently because the resolution surface in Virtuous is different.| Duplicate type | Resolution path | API support |
|---|---|---|
| Two Contacts for the same donor | Merge in the Virtuous UI by an administrator. | The CRM+ API has no merge endpoint. Detection is possible via mergedIntoContactId; merging is not. |
| Two Gifts for the same donation | Create a reversing transaction for the unwanted duplicate. | POST /api/Gift/ReversingTransaction exists and is API-accessible. |
Detecting Contact duplicates
Pattern A: pre-create lookup
The primary defense — catch duplicates before creating them. Before callingPOST /api/Contact/Transaction for a donor, run GET /api/Contact/Find with your referenceSource/referenceId and with email. If a Contact already exists matching either signal, use the existing record rather than submitting a Transaction.
This is the pattern documented in Create a Contact, Step 2. It prevents the most common partner-introduced duplicates.
Pattern B: post-create reconciliation query
For duplicates that slip past pre-create lookup (and for duplicates created by other sources), run periodic reconciliation queries that surface likely duplicate sets:JavaScript
Pattern C: detecting merges after the fact
When the customer merges two Contacts in the Virtuous UI, the merged-away Contact’s record carries amergedIntoContactId field pointing to the surviving Contact. Your sync should watch for this field on Contacts you previously tracked:
JavaScript
contactUpdate webhooks also fire when a merge occurs — subscribe to that event and check for the mergedIntoContactId field in the handler:
JavaScript
Resolving Contact duplicates
This is where the partner integration’s options are constrained. The partner-side workflow:Detect the likely duplicate set
Use Pattern B above (post-create reconciliation query) to surface likely duplicate groups.
Surface the duplicates to the customer
Present the duplicate set in your integration’s UI — typically with the matching field (email, name) and links to each Contact’s Virtuous profile via
contactViewUrl. Include a clear instruction that the merge must happen in Virtuous.Wait for the customer to merge
The customer’s Virtuous administrator merges the duplicates in the Virtuous UI. This sets
mergedIntoContactId on the merged-away record and (typically) fires a contactUpdate webhook.Detecting Gift duplicates
Gift duplicates are easier to handle programmatically because the resolution path is API-accessible. The primary detection signal: two Gift records in Virtuous with the sametransactionSource + transactionId pair, or two Gift records on the same Contact for the same amount and date.
JavaScript
Resolving Gift duplicates
Unlike Contacts, Gift duplicate resolution is API-accessible — but the pattern is different from “deleting” the duplicate.The reversing transaction pattern
To remove a duplicate Gift while preserving the accounting trail, create a reversing transaction rather than deleting the duplicate:cURL
The exact request body fields for
POST /api/Gift/ReversingTransaction may vary by organization configuration. The example above shows the conceptual pattern; confirm the exact required fields by inspecting the endpoint’s request schema or by checking with Virtuous support before relying on this in production.When to delete instead
DELETE /api/Gift/{giftId} exists but should be used sparingly. Deleting a Gift removes it from the audit trail entirely — accounting systems that have already reconciled the gift will see a missing record on the next sync. The reversing-transaction pattern is preferred for any gift that has been visible to downstream systems (receipts, accounting exports, reporting).
Delete is appropriate only when:
- The duplicate Gift was created very recently and has not yet been visible in any downstream report or receipt.
- The customer’s accounting team has confirmed there is no need to preserve the audit trail.
- The duplicate is unambiguously a clerical error rather than a recorded-twice payment.
Preventing future duplicates
Detection and resolution are the last line of defense. Prevention is the first.Always use Transaction endpoints
POST /api/v2/Gift/Transaction and POST /api/Contact/Transaction apply Virtuous’s matching algorithm. POST /api/Gift and POST /api/Contact do not. Use the Transaction variants by default. See Transactions.
Include strong matching signals
For Contact Transactions, the matching algorithm uses signals in priority order: external reference, email, phone, name + address. Include as many as your platform captures.firstName and lastName is much more likely to either create a duplicate (no match on existing record) or merge into the wrong record (matching by name alone is ambiguous).
Use stable transactionId for Gifts
transactionId is the idempotency key for Gift submissions. Use your platform’s stable identifier for the donation event — the Stripe charge ID, the donation ID from your platform’s database. Never regenerate transactionId on retry.
JavaScript
Validate before direct create
If for some reason your integration must use direct create (POST /api/Contact or POST /api/Gift), wrap it in your own lookup logic. See Create a Contact, Strategy 2: Find-then-create.
Operational practices
Duplicates are a chronic problem in any nonprofit’s donor data, not a one-time fix. Treat duplicate management as an ongoing operational concern:- Run the post-create reconciliation query on a schedule. Daily is reasonable for active integrations.
- Surface duplicates to the customer in your UI rather than hiding them. The customer’s team is the only party who can perform the actual merge.
- Alert when the duplicate count grows. A sudden spike usually indicates a sync regression — a recent change in your integration is producing duplicates faster than the customer can merge them.
- Track which integration created each Contact. If your
originSegmentCodeorreferenceSourceis consistent on every submission, you can attribute duplicates to your own work vs. other sources.
Where to go next
Reconcile Failed Syncs
The broader treatment of sync failure recovery — including the needs-update bucket where unmatched Transactions land.
Build a Two-Way Sync
How merge events on the Virtuous side propagate back to your platform in a two-way sync.
Transactions
The matching algorithm that prevents most duplicates when used correctly.
Create a Contact
The find-then-create pattern that prevents partner-introduced Contact duplicates.