Contact Mapping
Contact Mapping
WeGive donors map to Virtuous’s hierarchical Contact → ContactIndividual model. A Contact represents a household or organization; each person inside it is a ContactIndividual. This page documents exactly how contacts are pushed and pulled.
Record Model
| WeGive record | Virtuous record | Correlation column(s) |
|---|---|---|
| Donor (individual) | ContactIndividual inside a Contact | virtuous_contact_id, virtuous_contact_individual_id, virtuous_is_primary_contact_individual |
| Donor (company) | Contact of type Organization (no individual) | virtuous_contact_id |
| Household | Contact of type Household | virtuous_id |
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 no virtuous_contact_id, the integration acquires a per-donor lock (to prevent duplicates) and POSTs a single Contact containing one nested ContactIndividual:
POST Contact{ "contactType": "Household" | "Organization", "name": <donor.name>, "createDateTimeUtc": <donor.created_at, ISO 8601>, "contactAddresses": [ ... see Address fields ... ], "contactIndividuals": [ { "firstName": <donor.first_name | "FNU">, "lastName": <donor.last_name | "LNU">, "isPrimary": true, "contactMethods": [ ... see Contact methods ... ] } ]}From the response, WeGive stores:
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
Missing names are sent as the sentinels FNU / LNU, which Virtuous requires.
Updating an existing contact
When a donor already has a virtuous_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 a contactMethods array, each { type, value, isPrimary, isOptedIn }:
| WeGive field | Virtuous type | isPrimary |
|---|---|---|
email_1 | Email | true |
mobile_phone | Phone | true |
email_2 | Email | false |
other_phone | Phone | false |
On update, methods are matched to existing ones by value (phones compared with non-digits stripped) and updated in place, or created via 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 in contactAddresses (on create) or synced via ContactAddress (on update), keyed by label:
| WeGive field | Virtuous field |
|---|---|
address->type | label (mailing / billing) |
type === 'mailing' | setAsPrimary |
address_1 | address1 |
address_2 | address2 |
city | city |
state | state |
zip | postal |
country | country |
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 configured pull_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
Virtuous contactType | WeGive result |
|---|---|
Household, Staff, NATL | A WeGive Household plus a Donor per ContactIndividual |
Anything else (e.g. Organization) | A single company Donor |
For each imported individual, WeGive sets:
| WeGive field | Source |
|---|---|
first_name | firstName |
last_name | lastName |
virtuous_contact_individual_id | individual id |
virtuous_contact_id | parent contact id |
email_1 | primary Email contact method |
mobile_phone | primary Phone contact method |
virtuous_is_primary_contact_individual | isPrimary |
Secondary email/phone (
email_2/other_phone) are not populated on pull — the secondary-method handling is currently disabled, so they import asnull.
A WeGive user login is created for each 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 carries mergedIntoContactId, 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
This reflects the integration as implemented in app/Integrations/Virtuous.php.