Skip to main content

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

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:
From the response, WeGive stores:
  • virtuous_contact_id ← id
  • virtuous_contact_individual_id ← contactIndividuals[0].id (only if the response includes an individual — Organization contacts 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 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 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).
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: 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

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.
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:
  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.