Contact Mapping
WeGive donors map to Virtuous’s hierarchical Contact → ContactIndividual model. AContact represents a household or organization; each person inside it is a ContactIndividual. This page documents exactly how contacts are pushed and pulled.
Record Model
The Virtuous
Contact type is set from the WeGive donor type: company donors push as Organization, everything else pushes as Household.
Push (WeGive → Virtuous)
Creating a new contact
When a donor has novirtuous_contact_id, the integration acquires a per-donor lock (to prevent duplicates) and POSTs a single Contact containing one nested ContactIndividual:
virtuous_contact_id←idvirtuous_contact_individual_id←contactIndividuals[0].id(only if the response includes an individual —Organizationcontacts may not)virtuous_is_primary_contact_individual←true
FNU / LNU, which Virtuous requires.
Updating an existing contact
When a donor already has avirtuous_contact_id:
- If the donor has a
virtuous_contact_individual_id: the integrationGETs the existingContactIndividualfirst (a404is treated as a hard failure — the contact was archived/deleted in Virtuous, and WeGive will not silently recreate it; see Integration Nuances → Archived Contacts Are Not Reflected in WeGive for why this is the only signal available), overlaysfirstName/lastName(only when non-null), andPUTs it back. It then syncs contact methods, and — if this donor is the primary individual — syncs addresses. - If the donor has no individual (company contact): only addresses are synced.
Contact methods (email & phone)
Email and phone are not flat fields on the contact — they are entries in acontactMethods array, each with type, value, isPrimary, and isOptedIn keys. Which WeGive field maps to which Virtuous type is configurable per organization (phone_type_mappings/email_type_mappings on the integration); the platform defaults are:
other_phone has no default type mapping — it’s only populated if the organization has configured one. Unmapped fields default to Virtuous’s bare Phone/Email parent type on push, which resolves to whatever subtype that org has set as its own default (historically a source of surprises before this was fixed with a configurable type-mapping editor).POST ContactMethod against the contactIndividualId. The integration never deletes methods that exist in Virtuous but not in WeGive, to preserve data entered directly in Virtuous. MiddleName, Suffix, and other name parts are not pushed.
Address fields
Addresses are pushed incontactAddresses (on create) or synced via ContactAddress (on update), keyed by label:
On update, existing mailing/billing addresses are matched by
label and updated, otherwise created against the virtuous_contact_id.
Pull (Virtuous → WeGive)
Contacts are pulled in two passes, each paging through 1000 records at a time filtered by the configuredpull_by date:
POST Contact/Query/FullContact→ imports theContact(household/organization) and its nested individuals.POST ContactIndividual/Query→ imports/updates individuals directly (catches individual-level changes).
Contact type handling
For each imported individual, WeGive sets:
email_2 populates too by default (mapped to Work Email) — the mapped fields aren’t limited to email_1/mobile_phone, they follow whatever phone_type_mappings/email_type_mappings the organization has configured (see Contact methods above). other_phone has no default mapping, so it stays null unless the org configures one.Email contact method on the individual. For company contacts, the company donor’s email_1 / mobile_phone are taken from the primary individual’s primary email/phone. Addresses are imported by label (mailing / billing only), mapping address1/address2/city/state/postal/country → address_1/address_2/city/state/zip/country.
Merged contacts
If a pulled contact carriesmergedIntoContactId, the integration reconciles WeGive records: donors and households pointing at the old contact ID are re-pointed (or merged via Donor::mergeDonors / Household::mergeHouseholds) into the master contact, and the merged record is skipped.
Matching Order
On import, a donor is resolved by:virtuous_contact_individual_id- For
Organizationcontacts: the company donor with thatvirtuous_contact_idand no individual - Otherwise: the donor under that
virtuous_contact_idflagged as primary individual - Create new
app/Integrations/Virtuous.php.