Virtuous Integration: V1 vs V2
Last updated: May 5, 2026
How data flows between Virtuous and WeGive in each version
How to read this document
This document walks through every type of data we sync between Virtuous and WeGive, and explains exactly how it works under V1 versus V2. For each data object, you'll see two flow descriptions side by side: how data moves from Virtuous into WeGive ("Pull") and how data moves from WeGive into Virtuous ("Push"). Where the field-level mapping is meaningful, we include it inline.
The intent is to surface not just the literal mapping but the logic behind it: why we pull on certain triggers, what we treat as the source of truth in different scenarios, and where the two versions diverge in ways that affect what you see in your data.
The mental model
V1: flat
In V1, Virtuous and WeGive both think of a donor as one entity. When we pull a household from Virtuous, we create a single Donor row in WeGive. We grab the primary individual's first name, last name, email, and phone, and pin those onto the donor record. The secondary individual's email and phone get stored in a separate column on the same row. There is no household concept in WeGive under V1, everything collapses to a single donor.
When transactions, recurring gifts, or contact updates flow back to Virtuous, they're all attributed at the Contact level. Virtuous knows the gift came from a given household but doesn't know which individual within that household is the actual giver.
V2: split
In V2, the Virtuous data model is reflected more faithfully. A Virtuous Household becomes a Household entity in WeGive. Each ContactIndividual inside that household becomes its own Donor record, and they're linked to the shared household via a sync relationship. Companies (Organization-type contacts) still come through as a single Donor with type="company", but with the distinction that they don't get split into individuals.
This matters for two reasons. First, the supporter table in WeGive now mirrors what you see in Virtuous: same Contact ID, same primary individual, same secondary individual, all queryable independently. Second, when transactions are pushed to Virtuous, V2 includes the contactIndividualId so Virtuous knows which spouse made the gift, not just that the household made it.
Where this surfaces in day to day usage
On V1, when you opened a household record in WeGive that contained two adults, you'd see one supporter with conflated data: first name from one individual, secondary email from the other. On V2, you see two distinct supporter records that share a household, each with their own contact info. This is the most visible difference between the two versions.
Data object 1: Contacts and individuals
V1 Pull (Virtuous to WeGive)
We call POST /api/Contact/Query/FullContact in pages of 1,000 records. For each contact returned, we create or update one Donor in WeGive. The contactType field on the Virtuous response decides whether we treat this as a person or an organization:
If contactType is Household: donor type is set to individual and the household name is used as the donor name and addressMeAs. Note: the household name is what gets used for both, which means a combined household name lands in both fields rather than splitting the individuals apart.
If contactType is Organization: donor type is set to company and the contact name is used as is.
We then walk the contactIndividuals array on the response. The primary individual's data populates first_name, last_name, primary email, and mobile phone on the donor. The secondary individual's data populates email_3 and other_phone on the same donor record. Both individuals' email addresses get used to create login records in WeGive, so the secondary contact can log in even though their data is folded onto the primary's row.
Mailing address is taken from the contact's primary address and stored as the donor's mailing address.
V1 Push (WeGive to Virtuous)
When a donor in WeGive is created or updated, we create or update one Virtuous Contact. The contactType is determined by donor type: company becomes Organization, anything else becomes Household.
On create, we build the Virtuous contact payload by walking the donor's logins. Each login becomes a contactIndividual entry on the Virtuous side, with that login's email and phone as contact methods. The first_name, last_name, primary email, and mobile_phone on the donor record get attached to the primary individual.
On update, we fetch the existing Virtuous contact, find the primary individual within it, and update the first/last name and ensure the primary email and mobile phone are set as primary contact methods. Addresses sync separately through the dedicated ContactAddress endpoints.
V2 Pull (Virtuous to WeGive)
V2 runs two separate pull operations in sequence: a Contact pull and a ContactIndividual pull. The Contact pull uses the same Contact/Query/FullContact endpoint as V1, but the import logic does something fundamentally different.
If the Virtuous contactType is Household we create or update a Household record in WeGive (this is a separate entity from Donors). Then we walk every contactIndividual under that contact and create or update a Donor record for each one. Each donor gets its own first_name, last_name, primary email, and mobile_phone, pulled from THAT individual's contact methods, not folded together. The donor is linked to the household via a many to many relationship.
If the Virtuous contactType is anything else (Organization, etc.), we create a single Donor with type="company", same as V1 for that case.
After the Contact pull, V2 runs a second pass: POST /api/ContactIndividual/Query. This catches any individual-level changes that didn't bubble up to the parent contact's modified timestamp. Each individual's contact methods are reconciled into the matching donor record.
V2 field mapping: donor record per individual
How each Virtuous field maps to a WeGive donor field under V2:
Virtuous source | WeGive donor field |
|---|---|
virtuous_contact_individual_id | |
contact.id (parent) | virtuous_contact_id |
contactIndividual.firstName | first_name |
contactIndividual.lastName | last_name |
contactMethod (Email, isPrimary) | email_1 |
contactMethod (Phone, isPrimary) | mobile_phone |
isPrimary on individual | virtuous_is_primary_contact_individual |
V2 Push (WeGive to Virtuous)
On donor create in WeGive, we POST a single Contact to Virtuous with one contactIndividual containing that donor's first/last name, email, and phone. This means a new donor in WeGive creates a new Virtuous household with that one individual under it, not a new ContactIndividual added to an existing household. (To add a second individual to an existing Virtuous household, that should be done in Virtuous directly.)
On donor update, we don't update the parent Contact at all. We fetch the existing ContactIndividual by ID, merge in the changed first/last name, PUT it back, then separately reconcile the contact methods (emails, phones) and addresses through their dedicated endpoints. This preserves any other fields on the ContactIndividual that we don't track in WeGive (gender, birth date, custom fields, etc.). We only overwrite fields we have a value for.
Non-destructive update behavior
V2 is non-destructive on update. We only overwrite the specific fields the donor has changed in WeGive. Any field on the ContactIndividual in Virtuous that we don't manage (birthday, gender, prefix, suffix, custom fields, custom collections) is read first and preserved on the way back. This is a behavioral guarantee at the code level, not just intent.
Data object 2: Funds (projects)
V1 and V2: same behavior
Funds sync identically under both versions because there's no household or individual concept involved. We map a Virtuous Project to a WeGive Fund.
Pull (Virtuous to WeGive)
We call POST /api/Project/Query paginated. For each Virtuous project we create or update a Fund in WeGive. The Fund's name uses Virtuous's onlineDisplayName if set, falling back to name. The projectCode comes through as the Fund's code field.
Funds can be auto-archived in WeGive based on integration settings. If a project in Virtuous is marked inactive, not public, or not available online, and the corresponding archive setting is enabled in the integration, we soft delete the WeGive fund (it stops appearing on donation forms but remains queryable for historical attribution).
Push (WeGive to Virtuous)
When a Fund is created in WeGive, we POST a Project to Virtuous with the fund name and a revenueAccountingCode of organizationId:fundId. That's how we trace WeGive funds back to their origin in Virtuous reporting. We do not push fund updates back to Virtuous. Once created, the Project lives in Virtuous as the source of truth.
Data object 3: Campaigns (segments)
V1 and V2: same behavior
Same as funds. Campaigns map one to one between Virtuous Segments and WeGive Campaigns regardless of integration version. No household or individual involvement.
Pull (Virtuous to WeGive)
We call POST /api/Segment/Query paginated. Each Virtuous Segment becomes a WeGive Campaign. The Virtuous segment ID becomes the Campaign's virtuous_id, and we construct the campaign name as a pipe-concatenated string: campaignName | communicationName | name | code. This preserves the full Virtuous hierarchy in a single field since WeGive's data model is flatter than Virtuous's three-tier structure (campaign to communication to segment).
Push (WeGive to Virtuous)
When a Campaign is created in WeGive, we POST a Segment to Virtuous with the campaign name, a code prefixed with WGC: plus the WeGive campaign ID, and the integration's configured default communication ID as the parent. Every WeGive-created segment lives under the same configured communication in Virtuous.
On update, we only modify segments that we created (those starting with WGC:). Segments created directly in Virtuous are treated as read only from WeGive. We won't push name changes back. This protects Virtuous-native segments from accidental edits via WeGive.
Data object 4: Transactions (gifts)
V1 Pull (Virtuous to WeGive)
We call POST /api/Gift/Query/FullGift paginated. For each gift, we look up the WeGive donor by the gift's contactId to virtuous_id mapping. The transaction is attributed to whichever donor matches that contact. Since V1 has only one donor per Virtuous contact, this is unambiguous.
V1 Push (WeGive to Virtuous)
Each successful transaction in WeGive triggers a real-time push to Virtuous via POST /api/Gift. The gift carries the donor's virtuous_id as contactId, the gift amount, designations (funds and amounts), the segment, and a custom field wg_id carrying the WeGive transaction ID for round-trip identification.
There is also a batch transaction path used for imports, POST /api/v2/Gift/Transactions, that pushes up to 100 transactions per call, used for backfill scenarios.
V2 Pull (Virtuous to WeGive)
Same endpoint as V1 but the donor lookup is more nuanced. We first try to find a company donor (type="company") matching the contactId. If no company match, we look at all donors under that contact and pick by:
If the gift response includes a contactIndividualId, match the donor by that individual ID directly. If not, match the donor flagged as the primary individual on that contact.
This means in V2, gifts are attributed to the actual giver (which spouse, which church contact) rather than collapsing to the household. Reporting in WeGive can now show gift history per individual, not just per household.
V2 Push (WeGive to Virtuous)
Same Gift endpoint, but the payload includes contactIndividualId alongside contactId when the donor has one. This tells Virtuous which individual within the household made the gift. It's an additive change. V1 organizations wouldn't see this field, V2 organizations do.
WeGive is the source of truth for transactions
On transaction updates, we fetch the existing gift from Virtuous and overwrite key fields with WeGive's values rather than merging. Specifically: amount, designations, description, segment, isTaxDeductible, isPrivate, notes, recurringGiftPayments, contactIndividualId. This is intentional. When a refund happens in WeGive, the amount goes to 0 in Virtuous regardless of what was there before. Other fields on the Virtuous gift that we don't manage are preserved.
Refunds and failures (both versions)
If a transaction status changes to refunded or failed, we update the corresponding Virtuous gift to amount=0 (rather than deleting it) so historical reporting still shows the gift was attempted but didn't complete. We only do this if the transaction was previously synced and has a real Virtuous gift ID. Synthetic batch IDs (those starting with btch_) are skipped because they aren't individual Virtuous gifts.
Data object 5: Recurring gifts
V1 Pull (Virtuous to WeGive)
We call POST /api/RecurringGift/Query paginated. Each Virtuous recurring gift becomes a WeGive ScheduledDonation. We look up the donor by virtuous_id matching the recurring gift's contactId, set the start date and frequency (Weekly, Monthly, Quarterly, Annually), and link to the matching campaign and fund.
V1 Push (WeGive to Virtuous)
When a recurring donation is created in WeGive, we POST a RecurringGift to Virtuous with the donor's virtuous_id as contactId, the start date, frequency, charge amount (the amount that hits the donor's card, including any fees they covered), and designations. Designations are calculated to sum to the full charge amount, so when fees are folded in, we either split the fee proportionally across the existing fund allocations or append the organization's designated fee fund.
V2 Pull (Virtuous to WeGive)
Same endpoint, same logic, but the donor lookup follows the V2 pattern. First check for a matching company donor, then check for an individual-level match if Virtuous starts exposing contactIndividualId on recurring gifts (currently they don't expose this, but the code is ready for when they do), and finally fall back to the primary individual of the household.
Practically: in V2, recurring gifts pulled from Virtuous attach to the household's primary individual donor today, not the actual giver. This is a Virtuous API limitation. Their RecurringGift object is contact-level only. We've raised this with their team. If they expose contactIndividualId on recurring gifts in the future, this code path will start using it automatically.
V2 Push (WeGive to Virtuous)
Same as V1 push, but contactId comes from the donor's virtuous_contact_id (V2's Contact ID) instead of virtuous_id. The Virtuous RecurringGift API does not accept contactIndividualId, so we cannot tell Virtuous which spouse owns the recurring gift even if we know in WeGive.
WeGive is source of truth for recurring designations
On recurring gift updates, designations and amount are overwritten with WeGive's values. We pull the existing record, replace those specific fields, and PUT it back. This means if a donor changes their fund split or amount in WeGive's portal, that change propagates as a full overwrite to Virtuous, not a merge. On first import from Virtuous, designations are pulled in as is. On subsequent updates, WeGive controls the allocation.
Why first import is special (both versions)
On the very first time we import an existing Virtuous recurring gift into WeGive, we copy amount (Virtuous's charge amount) onto WeGive's amount field and set fee_amount to 0. We don't update these on subsequent imports because Virtuous stores the charge amount (base plus fees) while WeGive stores the base separately from fees. Overwriting on every import would create a feedback loop where fees get added to the base each cycle. After first import, WeGive owns the recurring gift's amount and fee structure and pushes it to Virtuous.
Data object 6: Contact methods (emails and phones)
V1: handled inline with donor sync
V1 doesn't sync contact methods as a separate operation. Emails and phones come along with the donor pull as part of the contactIndividual structure, and on push we update the primary individual's primary email and phone in place during the donor update. There's no concept of secondary individuals or multiple contact methods being synced separately.
V2: explicit two-step reconciliation
V2 uses a dedicated syncContactMethods routine that runs after the ContactIndividual update. It builds a desired-state list of contact methods from the donor's email_1, email_2, mobile_phone, and other_phone fields, then compares against what currently exists on the Virtuous individual:
If the contact method exists on Virtuous already (matched by value, with phone numbers normalized to digits only), we update its primary or opt-in status if needed.
If it doesn't exist, we create it on the Virtuous individual.
If a contact method exists on Virtuous but not in WeGive, we leave it alone. We do not delete contact methods from Virtuous because they may have been added directly there and we want to preserve that data.
Setting a new primary contact method automatically demotes any old primary in Virtuous. That's Virtuous's behavior, not ours, but it means "changing the primary email" works correctly without us having to do a two-step operation.
Data object 7: Addresses
V1: single address
V1 syncs one address per donor (mailing) using the dedicated ContactAddress endpoints. On pull, we take the contact's primary address and create or update the donor's mailing address. On push, we look up existing Virtuous addresses on the contact, identify which is primary (mailing) and which isn't (billing), and update them in place, or create new ones if they don't exist.
V2: multiple addresses, label-based
V2 supports both mailing and billing addresses. On pull, we walk all addresses on the contact and import any with label="mailing" or label="billing" into the corresponding fields on the donor record. On push, we sync each WeGive address through the ContactAddress endpoints, identifying the existing Virtuous address by label rather than by isPrimary flag (which is more reliable when contacts have both mailing and billing addresses).
Data object 8: Households (V2 only)
V1: no household concept
V1 has no household entity in WeGive. A Virtuous household becomes a single Donor row, full stop.
V2: first-class entity
V2 treats Virtuous Households as a separate Household entity in WeGive. The household has its own record with its own virtuous_id (matching the parent Contact ID) and is linked to all the individual donors within it via a many to many relationship.
This allows several things V1 can't:
Querying "all donors in this household" without joining through Virtuous. Showing combined household giving in reports. Modeling families where individuals can have separate engagement (one spouse opens emails, one doesn't) while still rolling up gifts to the household for accounting.
Households are not pushed back to Virtuous independently. They're created and updated only in response to incoming Contact pulls. The Virtuous household is the source of truth for its own existence.
Side by side summary
How V1 and V2 compare across each aspect of the integration:
Aspect | V1 | V2 |
|---|---|---|
Donor model | One donor per Virtuous Contact (collapses households) | One donor per ContactIndividual; separate Household entity |
Households | No concept, flattened into donor | First-class entity linking donors |
Spouse or secondary contact data | Stored as email_3 and other_phone on primary's row | Their own donor record with their own contact methods |
Gift attribution on pull | Contact-level (household) | Individual-level when contactIndividualId present |
Gift attribution on push | contactId only | contactId plus contactIndividualId |
Recurring gift attribution | Contact-level | Contact-level (Virtuous API limit) |
Contact methods sync | Inline with donor update | Dedicated reconciliation pass per individual |
Address types supported | Mailing only | Mailing and billing |
Fund sync | Same as V2 | Same as V1 |
Campaign sync | Same as V2 | Same as V1 |
Update behavior on Virtuous side | Direct field merge | Read modify write to preserve unmanaged fields |