Skip to main content
Updating a Contact in CRM+ looks like a simple PUT /api/Contact/{contactId} call — but the safe pattern involves more steps than the endpoint signature suggests. CRM+‘s PUT semantics are ambiguous (the spec declares full-replacement PUT, but the live API typically behaves as PATCH), and the field-typing in the spec doesn’t match the live API. This workflow walks through the pattern that works correctly regardless of which behavior the platform settles on. If you have not read the Contacts concept page or the related PUT semantics warning, start there.

Scenario

Your platform has detected a change on a donor’s record — a new address from a USPS validation service, an updated email after a bounce-back, a phone number correction submitted through your portal. You need to push the change to the corresponding Contact in CRM+ without disturbing any of the other fields on the record.

Prerequisites

  • A valid CRM+ API token — see Authentication.
  • The Virtuous Contact ID (either stored from a previous sync, or looked up via GET /api/Contact/Find — see Create a Contact).
  • The specific fields you intend to change.

The PUT-as-PATCH ambiguity

The single most important thing to understand about updating in CRM+: the safe pattern is GET-then-PUT with the full record, regardless of how the API actually behaves.
Possible API behaviorWhat happens if you PUT a partial body
PUT is true full-replacementFields omitted from your request are cleared to null. Likely catastrophic.
PUT behaves as PATCHFields omitted from your request are preserved. The intended behavior most partners assume.
The CRM+ spec declares the first behavior; the live API reportedly behaves as the second on most endpoints (with the Relationship PUT being a confirmed exception). Until the spec and the live behavior are reconciled, the only pattern that is correct under both interpretations is to send the complete record on every PUT — with your intended changes applied to the full data you read first.
Do not send “partial” PUT bodies that omit fields you don’t want to change. Even if the API today behaves as PATCH and treats omissions as “keep existing value,” that behavior is not guaranteed in the spec and could change. The GET-then-PUT pattern is correct under both interpretations and adds at most one extra request per update.

The safe update pattern

1

Retrieve the current Contact record

Call GET /api/Contact/{contactId} to retrieve the full current state. This is the baseline you will modify.
2

Apply your changes to the retrieved record

Mutate only the fields you intend to change. Leave every other field as-is. Be especially careful with sub-resource arrays (contactIndividuals, address) — replacing an array wholesale will remove individuals or addresses you didn’t intend to touch.
3

Send the complete record back as PUT

Call PUT /api/Contact/{contactId} with the full modified record. This is safe under both PUT and PATCH semantics.
4

Verify the update via the response or a follow-up GET

The PUT response should reflect your changes. For high-stakes updates (financial fields, deletion flags), follow up with a GET to confirm the persisted state.

Reference implementation

# Step 1: Read the current Contact
curl https://api.virtuoussoftware.com/api/Contact/4821 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -o current-contact.json

# Step 2: Modify the file locally — change the address1 field
# (use jq, a text editor, or your scripting tool of choice)

# Step 3: Send the full modified record back
curl -X PUT https://api.virtuoussoftware.com/api/Contact/4821 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @current-contact.json
The JavaScript example’s spread operator (...current, ...changes) merges the change set on top of the current record. This is the safest pattern for top-level field changes. For nested fields (changing one ContactIndividual inside the contactIndividuals array), use the sub-resource update endpoints described below.

Updating sub-resources

A Contact contains three classes of sub-resources, each with its own dedicated update endpoint. Use the sub-resource endpoint for changes that affect only one item — it avoids the risk of accidentally clearing other items in the array.
Sub-resourceUpdate endpoint
ContactAddressPUT /api/ContactAddress/{contactAddressId}
ContactIndividualPUT /api/ContactIndividual/{contactIndividualId}
ContactMethodUpdated via the parent ContactIndividual (no standalone endpoint)

Updating an address

Use the address’s own ID to update it directly:
# Read the address first (the ID is in the Contact response's address.id field)
curl https://api.virtuoussoftware.com/api/ContactAddress/51001 \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Update the full address record
curl -X PUT https://api.virtuoussoftware.com/api/ContactAddress/51001 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": 51001,
    "label": "Home",
    "address1": "1007 Mountain Drive",
    "address2": null,
    "city": "Gotham",
    "state": "NJ",
    "postal": "07001",
    "country": "US",
    "isPrimary": true
  }'

Updating an individual

PUT /api/ContactIndividual/{contactIndividualId} updates the person record inside a Contact. Use this when changes apply to a specific person (first name, last name, birth date) rather than the household as a whole.
The CRM+ spec description on PUT /api/ContactIndividual/{contactIndividualId} does not explicitly state full-replacement semantics — but the audit (finding #18) notes that this is an implicit constraint of the PUT model. As with Contact, follow the GET-then-PUT pattern to be safe.

Field typing on update payloads

The CRM+ spec types every Contact field — including booleans, integers, and dates — as string (audit findings #5–#8). The live API accepts the natural types: send booleans as true/false, send integers as numbers, send dates as ISO 8601 strings.
{
  "isPrivate": false,                          // boolean, not "false"
  "anniversaryYear": 2010,                     // integer, not "2010"
  "contactIndividuals": [
    {
      "id": 9012,
      "birthDate": "1972-02-19",               // ISO 8601 date string
      "isPrimary": true,                       // boolean
      "isDeceased": false                      // boolean
    }
  ]
}
Strict SDK generators that read the spec literally will type these fields as strings and may serialize values as "true" or "123". Verify that your HTTP client sends the natural types. If you are using an auto-generated SDK and seeing unexpected results, this type mismatch is the most likely cause.

Common update scenarios

Change a primary email

The email lives on a ContactIndividual’s contactMethods array. To change it:
1

GET the parent Contact

GET /api/Contact/{contactId} to retrieve the Contact with all its individuals and their contact methods.
2

Modify the ContactMethod in the contactIndividuals array

Find the ContactIndividual with isPrimary: true, find their existing primary email ContactMethod, and update its value. Or add a new ContactMethod with isPrimary: true and set the existing primary’s isPrimary to false.
3

PUT the full Contact back

PUT /api/Contact/{contactId} with the modified record.

Mark a donor deceased

Set isDeceased: true on the appropriate ContactIndividual:
JavaScript
const contact = await fetch(`https://api.virtuoussoftware.com/api/Contact/${contactId}`, {
  headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());

const updated = {
  ...contact,
  contactIndividuals: contact.contactIndividuals.map((ind) =>
    ind.id === deceasedIndividualId ? { ...ind, isDeceased: true } : ind
  ),
};

await fetch(`https://api.virtuoussoftware.com/api/Contact/${contactId}`, {
  method: 'PUT',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(updated),
});
After marking a ContactIndividual deceased, suppress that individual’s contact methods from outbound communications. See Statuses and Lifecycle States.

Update a custom field value

Custom field updates work the same way as standard fields — modify the customFields array on the Contact, then PUT the full record. See Custom Fields.

Add a new external reference

To add an additional contactReferences entry (e.g., the donor signed up for a second integrated product), read the Contact, append to the array, and PUT:
JavaScript
const updated = {
  ...contact,
  contactReferences: [
    ...(contact.contactReferences || []),
    { source: 'NewIntegration', id: 'new-id-here' },
  ],
};

Archive a Contact

Use the dedicated archive endpoint rather than setting isArchived via PUT:
cURL
curl -X PUT https://api.virtuoussoftware.com/api/Contact/Archive/4821 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
PUT /api/Contact/Unarchive/{contactId} reverses the archive. See Statuses and Lifecycle States.

Verifying updates

After a successful PUT, two patterns confirm the persisted state:
  • The PUT response. The response body should reflect the updated record. For simple field changes, this is sufficient — the response confirms what was stored.
  • A follow-up GET. For high-stakes updates (financial fields, ownership transfers, lifecycle transitions), follow up with GET /api/Contact/{contactId} to confirm the persisted state independent of the PUT response.
In rare cases, the PUT response may reflect server-side normalization (e.g., trimmed whitespace, canonicalized state codes) that differs from the values you sent. This is expected — store the response’s representation, not your input, as the canonical state in your integration’s database.

Concurrent updates

The CRM+ API does not provide an If-Match / ETag mechanism for optimistic concurrency control. If two callers PUT the same Contact at the same time, the later write wins — with no warning. For partner integrations, this is rarely a problem because:
  • Each customer has one partner integration writing on their behalf.
  • The Virtuous UI is the other typical writer, and concurrent UI + API writes on the same record at the exact same instant are rare in practice.
If concurrent writes are a concern (e.g., a customer using two integrations that both write to the same Contact field), use the modifiedDateTimeUtc field as a soft concurrency check: read the current modifiedDateTimeUtc before the GET-then-PUT cycle, and after the PUT compare the response’s new modifiedDateTimeUtc to confirm no other write landed between your GET and PUT.

Error handling

StatusCauseAction
400 Bad RequestMalformed JSON or type mismatchInspect the response body; check spec-vs-live field types.
404 Not FoundContact ID does not existConfirm the ID; the Contact may have been merged or deleted.
409 ConflictUnique-constraint violation (e.g., email already on another Contact)Reconcile the duplicate before retrying. See Handle Duplicate Records.
422 Unprocessable EntityValidation failedRead error.details[] for field-specific messages.

Where to go next

Create a Contact

The companion workflow — including the lookup pattern that returns the IDs you’ll need for updates.

Build a Two-Way Sync

How to combine create, update, and webhook-driven inbound changes into a continuous sync.

Statuses and Lifecycle States

The dedicated endpoints for archive/unarchive transitions that you should not implement via PUT.

Custom Fields

Reading and writing custom field values on Contacts — same PUT pattern, different schema discovery.
Last modified on May 21, 2026