Skip to main content
The Contact is the foundational resource in CRM+. Every Gift, every Pledge, every RecurringGift, and every Note references a Contact. Most partner integrations begin by figuring out the Contact model, because everything else depends on it. This page covers the Contact resource and its sub-resources — the relationships, the most-used fields, and the patterns for creating, updating, and looking up Contacts without producing duplicates.

The Contact hierarchy

A Contact in Virtuous represents a household or organization — not a person. The people, addresses, and contact methods inside a Contact are separate sub-resources.
ResourceCardinalityWhat it is
ContactThe top-level recordA household (e.g., “The Wayne Family”) or an organization (e.g., “Wayne Enterprises”).
ContactIndividualMany per ContactA person inside the Contact — first name, last name, birth date, gender, etc.
ContactAddressMany per ContactA postal address belonging to the Contact (one is the primary).
ContactMethodMany per ContactIndividualAn email or phone number belonging to a specific ContactIndividual.
The two sub-resources that trip up new partner integrations most often are ContactIndividual and ContactMethod:
  • ContactIndividual is the person record. First name, last name, birth date, and gender live here — not on Contact.
  • ContactMethod is the email or phone record. Email addresses live here — not on Contact or ContactIndividual as a top-level string.
A typical individual donor looks like this in the data model:
{
  "id": 4821,
  "contactType": "Household",
  "name": "Wayne, Bruce",
  "contactIndividuals": [
    {
      "id": 9012,
      "firstName": "Bruce",
      "lastName": "Wayne",
      "isPrimary": true,
      "contactMethods": [
        { "id": 31001, "type": "Home Email", "value": "bruce@wayne.example", "isPrimary": true },
        { "id": 31002, "type": "Mobile Phone", "value": "555-0100", "isPrimary": true }
      ]
    }
  ],
  "address": {
    "id": 51001,
    "address1": "1007 Mountain Drive",
    "city": "Gotham",
    "state": "NJ",
    "postal": "07001",
    "country": "US",
    "isPrimary": true
  }
}

Contact types

The contactType field on a Contact identifies what kind of record it is. Common values include Household, Organization, and Foundation, but the exact enum is configured per organization.
The CRM+ spec does not enumerate the valid contactType values in the OpenAPI definition — the field is typed as string. To discover the values valid for a specific organization, call GET /api/Contact/Types. Cache the result; do not hardcode contact type strings in your integration.
The contactType affects which fields are meaningful on the record:
  • Household Contacts represent a family unit. They have one or more ContactIndividuals (typically the adult householders), and gifts are attributed to the household even when one specific individual made the donation.
  • Organization and Foundation Contacts represent institutions. They have ContactIndividuals for the relevant contact people (development director, program officer, etc.), and gifts are attributed to the institution.

Primary and secondary individuals

Within a household Contact, ContactIndividuals carry isPrimary and isSecondary flags. Exactly one individual is the primary; at most one is the secondary. Other individuals (children, extended family) are neither.
FlagMeaning
isPrimary: trueThe main contact for the household. Receipts, communications, and gift attributions default to this individual.
isSecondary: trueThe partner/spouse role. Some communications and joint-giving displays include this individual.
Neither flag setOther household members (e.g., children). Stored but not the default recipient for communications.
canBePrimary and canBeSecondary indicate whether an individual is eligible for those roles based on other organization settings.
When creating a new household Contact from your platform, mark the donor as isPrimary: true. If a spouse is also captured, mark them as isSecondary: true. This matches the convention Virtuous uses internally and ensures downstream features (joint giving, household receipts) work as expected.

Addresses

Each Contact has one or more ContactAddress records, exposed both as a top-level address field on the Contact (the primary address) and as a separate resource with its own ID for management. Key fields:
FieldTypeDescription
idintegerThe unique address ID. Required for updates.
labelstringThe address’s display label (e.g., “Home”, “Work”, “Seasonal”).
address1, address2stringStreet address lines.
city, state, postal, countrystringStandard postal components.
isPrimarybooleanWhether this is the Contact’s primary address.
canBePrimarybooleanWhether this address is eligible to be primary (e.g., contains required fields).
startMonth, startDay, endMonth, endDayintegerSeasonal address date ranges.
A Contact can have multiple addresses (e.g., a seasonal/snowbird donor with a winter and summer address). The isPrimary flag controls which address is used for mailings.
The CRM+ spec types every ContactAddress field — including isPrimary, startMonth, and other typed values — as string. The live API accepts and returns correct types: boolean for flags, integer for IDs and month/day numbers. Send the correct type from your integration; libraries that strictly enforce the spec’s declared types may need a workaround.

Tags, custom fields, and references

Three mechanisms let you attach additional information to a Contact:
  • Tags — simple labels (strings) used for ad-hoc grouping. A Contact can have any number of tags. Tags are organization-defined; see GET /api/Contact/Tags for the list valid in the current organization.
  • Custom fields — typed, named fields configured by the organization. Used for structured data that doesn’t fit the standard schema (e.g., a wealth screening score, a communications preference). See the Custom Fields concept page.
  • Contact references — external system identifiers stored on the Contact. Used to bridge your platform’s IDs with Virtuous IDs (e.g., source: "Stripe", id: "cus_abc123"). See Relationships and IDs.
{
  "tags": ["Major Donor", "Board Member"],
  "customFields": [
    { "name": "wealthScore", "value": "850", "displayName": "Wealth Score" }
  ],
  "contactReferences": [
    { "source": "Stripe", "id": "cus_abc123" },
    { "source": "Eventbrite", "id": "event-attendee-9421" }
  ]
}
contactReferences are particularly important for partner integrations — they enable looking up the Virtuous Contact by your platform’s ID via GET /api/Contact/ByReference/{referenceId}, removing the need to store the Virtuous Contact ID in your own database.
GET /api/Contact/ByReference/{referenceId} requires HMAC authentication rather than standard Bearer token auth. See Authentication for details — contact Virtuous support to obtain HMAC credentials if you need to use this lookup pattern.

Statuses and lifecycle flags

Contacts have several boolean fields that affect how the record behaves in the system:
FieldWhat it means
isPrivateThe Contact is hidden from most users — visible only to administrators. Typical for high-profile donors or anonymous gifts.
isArchivedThe Contact has been archived. Excluded from most queries by default.
isDeceased (on ContactIndividual)The individual has died. The Contact may still be active if other individuals remain.
By default, POST /api/Contact/Query excludes archived Contacts. Set includeArchived: true in the filter body to include them when reconciling deleted records or auditing historical data. See Statuses and Lifecycle States for the full lifecycle treatment.

Creating Contacts without producing duplicates

The single most important pattern in any Contact-write integration is never create blindly. The CRM+ API does not prevent duplicate Contacts on direct creation — if you POST a Contact for a donor who already exists in the system, you produce a second record with the same name and address as the first. Two strategies handle this correctly: POST /api/Contact/Transaction submits a Contact for asynchronous import. The nightly batch run applies Virtuous’s matching algorithm — checking email, phone, address, name, and contactReferences against existing records — and creates a new Contact only if no match is found. If a match is found, the incoming data is merged into the existing record. This is the path the Virtuous team explicitly recommends for partner integrations. It is described in detail on the Transactions concept page.

Strategy 2: Find-then-create

If you need synchronous Contact creation, the safe pattern is:
  1. GET /api/Contact/Find — attempt to locate an existing Contact by email or external reference (source + ID). Returns a single matched Contact if one exists. Accepts email, referenceSource, and referenceId as query parameters.
  2. POST /api/Contact/Query — if Find returns nothing, run a broader query (by name + postal code, for example) to catch fuzzy matches that Find did not.
  3. POST /api/Contact — create only if both lookups confirm no existing record matches.
async function upsertContact(donor) {
  // 1. Try direct lookup by email (GET with query parameter)
  const find = await fetch(
    `https://api.virtuoussoftware.com/api/Contact/Find?email=${encodeURIComponent(donor.email)}`,
    {
      headers: { Authorization: `Bearer ${token}` },
    }
  );

  if (find.ok) {
    return await find.json(); // existing Contact found
  }

  // 2. Broader query as a fallback (omitted for brevity — see Pagination and Filtering)

  // 3. No match — create a new Contact
  const create = await fetch(
    'https://api.virtuoussoftware.com/api/Contact',
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contactType: donor.isOrganization ? 'Organization' : 'Household',
        name: donor.displayName,
        contactIndividuals: [{
          firstName: donor.firstName,
          lastName: donor.lastName,
          isPrimary: true,
          contactMethods: [
            { type: 'Home Email', value: donor.email, isPrimary: true, isOptedIn: donor.emailOptIn },
          ],
        }],
        address: donor.address,
      }),
    }
  );

  return await create.json();
}
POST /api/Contact does not return 201 Created — it returns 200 OK. The created record’s ID is in the response body. Treat any successful response from this endpoint as a creation, regardless of the status code.

Updating Contacts

The CRM+ API uses PUT /api/Contact/{contactId} for updates. The endpoint name suggests full-replacement PUT semantics, but in practice the live API behaves as PATCH — fields omitted from the request body are not cleared.
This is undocumented behavior. The spec declares PUT for these endpoints, and the canonical REST semantics for PUT are full-replacement (omitted fields would be cleared to null). The live API does not behave that way today, but relying on this undocumented behavior is a risk. The safe pattern is: GET the current record, modify the fields you intend to change, PUT the complete record. This insulates your integration from any future change to PUT semantics.
For the field-by-field update pattern and the safe GET-then-PUT workflow, see the Update a Contact workflow.

Created and modified timestamps

Every Contact carries metadata about when it was created and last modified:
FieldTypeDescription
createDateTimeUtcdateTimeISO 8601 timestamp of when the Contact was created. UTC.
createdByUserstringName of the user who created the record.
modifiedDateTimeUtcdateTimeISO 8601 timestamp of the most recent modification. UTC.
modifiedByUserstringName of the user who most recently modified the record.
The modifiedDateTimeUtc field is the canonical anchor for incremental sync — query for Contacts with modifiedDateTimeUtc > {last_sync_run} to retrieve only records that have changed since your last run. See Query Contacts by Filters for the pattern.

Where to go next

Donations / Gifts

Gift records — what they contain and how they reference a Contact.

Transactions

The recommended Contact and Gift import pattern with built-in matching and deduplication.

Custom Fields

How nonprofits extend the Contact schema and how to read/write custom field values.

Relationships and IDs

Using contactReferences to bridge your platform’s IDs with Virtuous Contact IDs.

Create a Contact

A working example of the Contact create workflow with all required fields.

Query Contacts by Filters

Use POST /api/Contact/Query for incremental sync and bulk retrieval.
Last modified on May 27, 2026