Skip to content

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 recordVirtuous recordCorrelation column(s)
Donor (individual)ContactIndividual inside a Contactvirtuous_contact_id, virtuous_contact_individual_id, virtuous_is_primary_contact_individual
Donor (company)Contact of type Organization (no individual)virtuous_contact_id
HouseholdContact of type Householdvirtuous_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_idid
  • virtuous_contact_individual_idcontactIndividuals[0].id (only if the response includes an individual — Organization contacts may not)
  • virtuous_is_primary_contact_individualtrue

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 integration GETs the existing ContactIndividual first (a 404 is 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), overlays firstName / lastName (only when non-null), and PUTs 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 fieldVirtuous typeisPrimary
email_1Emailtrue
mobile_phonePhonetrue
email_2Emailfalse
other_phonePhonefalse

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 fieldVirtuous field
address->typelabel (mailing / billing)
type === 'mailing'setAsPrimary
address_1address1
address_2address2
citycity
statestate
zippostal
countrycountry

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:

  1. POST Contact/Query/FullContact → imports the Contact (household/organization) and its nested individuals.
  2. POST ContactIndividual/Query → imports/updates individuals directly (catches individual-level changes).

Contact type handling

Virtuous contactTypeWeGive result
Household, Staff, NATLA WeGive Household plus a Donor per ContactIndividual
Anything else (e.g. Organization)A single company Donor

For each imported individual, WeGive sets:

WeGive fieldSource
first_namefirstName
last_namelastName
virtuous_contact_individual_idindividual id
virtuous_contact_idparent contact id
email_1primary Email contact method
mobile_phoneprimary Phone contact method
virtuous_is_primary_contact_individualisPrimary

Secondary email/phone (email_2 / other_phone) are not populated on pull — the secondary-method handling is currently disabled, so they import as null.

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/countryaddress_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:

  1. virtuous_contact_individual_id
  2. For Organization contacts: the company donor with that virtuous_contact_id and no individual
  3. Otherwise: the donor under that virtuous_contact_id flagged as primary individual
  4. Create new

This reflects the integration as implemented in app/Integrations/Virtuous.php.