> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wegive.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Contact Mapping

> How WeGive donors and households map to Virtuous Contacts and ContactIndividuals, in both directions

# 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 `POST`s 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` ← `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 `GET`s 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](/external/onboarding/virtuous/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 `PUT`s 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:

| WeGive field | Virtuous `type` (default) | `isPrimary` |
| - | - | - |
| `mobile_phone` | `Mobile Phone` | `true` |
| `email_1` | `Home Email` | `true` |
| `email_2` | `Work Email` | `false` |

<Note>
  `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).
</Note>

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:

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 `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` | Contact method mapped to the org's configured "email\_1" type (default: `Home Email`) |
| `mobile_phone` | Contact method mapped to the org's configured "mobile\_phone" type (default: `Mobile Phone`) |
| `virtuous_is_primary_contact_individual` | `isPrimary` |

<Note>
  `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](#contact-methods-email--phone) above). `other_phone` has no default mapping, so it stays `null` unless the org configures one.
</Note>

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.