Skip to main content
The most valuable insights for many nonprofits come from combining donor and volunteer data. The donor who also volunteers is the most engaged, highest-value supporter. The volunteer who’s never been asked to donate is an untapped opportunity. The lapsed donor who’s still volunteering offers a path back to giving. But this data lives in two separate APIs — VOMO (Volunteer) and CRM+ — built by different teams at different times, with distinct conventions, distinct schemas, and distinct identifiers. This recipe walks through stitching them together: matching strategies, schema mapping, conflict resolution, and the joined-data-store pattern that makes cross-API integration manageable. The recipe combines Volunteer User listing/upsert with CRM+ Contact querying into a unified view.

What you’ll build

A partner integration that:
  • Reads Users from Volunteer and Contacts from CRM+ separately
  • Matches them across the two APIs (email is the primary join key)
  • Stores a joined “person” record in a partner-side data store
  • Surfaces cross-API insights: donor-volunteer overlap, lapsed-donor-active-volunteer flags, volunteer-only outreach lists
  • Handles conflicts where the two APIs disagree about a field’s value

When this recipe fits


The fundamental challenge: two APIs, no shared ID

VOMO and CRM+ don’t share user identifiers. A volunteer with VOMO User ID 12345 might also be CRM+ Contact ID 99876 — but neither API knows about the other’s ID. The email address is the only natural join key. Both APIs treat email as a primary lookup field — Volunteer’s upsert matches by email; CRM+ Contact lookup typically goes by email. But email isn’t always reliable:
  • A volunteer may have multiple emails in different systems (work vs. personal)
  • An email change in one system breaks the join with the other
  • Email matching is case-insensitive in both APIs but partners may forget to normalize
For most integrations, email is the right starting point, with explicit handling of the edge cases.

Architecture

Six components: The joined store is the integration’s source of truth — both API collectors write into it, and downstream insights read from it.

Step 1: design the joined schema

The joined person record holds data from both APIs plus partner-side metadata:
Key design decisions:

Step 2: collect from VOMO

Reuses the polling pattern from Sync Users to External System:
JavaScript
The collector writes only the VOMO-side fields — the CRM+ side is populated by the other collector.

Step 3: collect from CRM+

CRM+ has different conventions but the same goal — populate the joined store with Contact data:
JavaScript

The convention difference

Notice the field-name mapping: VOMO Users are flat — email and phone are top-level properties. CRM+ Contacts are hierarchical — a Contact has Individuals which have ContactMethods. The collectors normalize both into the joined store’s flat structure.

Step 4: resolve conflicts

When the same person exists in both APIs, the two collectors will populate the same joined_persons row. But they may disagree on field values:

Pick an authoritative source per field

For each potentially-conflicting field, decide which API is authoritative:
JavaScript

Surface disagreements for review

For fields where neither side is clearly authoritative (or where a disagreement may indicate a data quality issue), surface for human review:
JavaScript
Conflicts aren’t necessarily errors — they’re flags for the customer’s data steward to review.

Step 5: surface cross-API insights

The whole point of joining is the questions you can now answer:

Donor-volunteer overlap

A simple four-row report often surprises customers — many don’t realize how many of their volunteers also give (or vice versa).

Lapsed donors who are still volunteering

A high-value reactivation list: people whose engagement (volunteering) is still active but whose giving has lapsed.

Top volunteers who haven’t been asked to give

A targeted outreach list combining the participation data (from the hours recipe) with the donor-status data — high-engagement volunteers who have never (or recently never) donated.

Step 6: handle the email-change problem (cross-API edition)

The single biggest data-quality challenge with email-based joining: what happens when someone changes their email in one API but not the other? The scenario:
  1. Bruce Wayne is both a VOMO User (bruce@wayne.example) and a CRM+ Contact (bruce@wayne.example). They’re joined as one partner_person_id.
  2. Bruce updates his email in VOMO to bruce.wayne@wayne.example.
  3. The next VOMO sync sees a “new” user (different email).
  4. The next CRM+ sync sees the existing user (same email).
  5. Now there are two partner_person_ids for Bruce — one for each email.

Detection

JavaScript
The most reliable detection: track per-API IDs over time. If a VOMO User ID that previously joined to partner_person_id X now appears with a different email (and a new partner_person_id Y), the integration has split a single person.

Resolution

JavaScript
Merging is destructive and shouldn’t be done automatically without strong signals. The typical pattern:
  1. Detect split candidates daily
  2. Surface them for human review
  3. The customer’s data steward approves merges in a UI
  4. The integration applies merges via the audit-logged operation

Step 7: cross-API reconciliation

Daily reconciliation catches drift between the two APIs:
JavaScript
Reconciliation catches both bugs (records missing from joined store) and real-world phenomena (the actual donor-volunteer overlap rate).

Things to watch for

The phone format mismatch

VOMO often stores phone numbers in one format (e.g., +15551234567); CRM+ may store them differently (e.g., (555) 123-4567). They’re the same number but won’t match by string comparison. For phone-based joining or comparison, normalize both to a canonical format (E.164 is the standard).

Different update cadences

VOMO and CRM+ have different update cadences in practice. VOMO Users change a few times per week per active user; CRM+ Contacts change more often (every gift creates a modification). The joined store sees both — but the lag between them can be hours. For workflows triggered by “donor-volunteer overlap detected,” wait a few hours after a new VOMO User appears before declaring “this person is a volunteer-only” — the CRM+ side may still be catching up.

Multi-API webhook complexity

If the partner integration uses CRM+ webhooks for change detection (CRM+ has them; VOMO doesn’t), the two halves of the integration operate on different cadences:
  • CRM+ side: webhook-driven, near-real-time
  • VOMO side: polling, 15-30 minute lag
This is fine for most workloads but can produce momentary inconsistencies in the joined store. Audit pipelines handle this automatically — by the next polling cycle, things converge.

Token isolation

The two APIs use different tokens issued by different customer-side processes. Don’t conflate them:
JavaScript
A token issue in one API shouldn’t disable the other — the joined integration should degrade gracefully when one half is unavailable.

Pagination conventions are different

VOMO uses page numbers; CRM+ uses Skip/Take offsets. Don’t try to share pagination code — keep the two API clients independent.

Schema evolution risk

Both APIs evolve independently. A field name change in one doesn’t break the other; but a change in either may break the joined integration. Build defensive parsing on both sides — see Versioning and Backward Compatibility.

What you’ve built

After this recipe:
  • ✅ A joined person store keyed by partner-assigned IDs
  • ✅ Independent collectors for VOMO Users and CRM+ Contacts
  • ✅ Email-based matching with normalization and conflict detection
  • ✅ Per-field authority resolution
  • ✅ Cross-API insight queries (overlap, lapsed donors, top non-donor volunteers)
  • ✅ Email-change-driven split detection and merge workflow
  • ✅ Daily reconciliation across both APIs
This is the foundation for any cross-API workflow combining donor and volunteer data — and arguably the highest-value insights a Virtuous-platform customer can get from a partner integration.

Where to go next

Build a Volunteer Self-Service Portal

The companion product-shape recipe — a customer-facing portal using this data.

Sync Users to External System

The foundational sync pattern this recipe extends.

Report on Volunteer Hours

The participation aggregation that powers volunteer-side insights.

The Volunteer Data Model

The Volunteer data model context.
Last modified on May 22, 2026