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
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:Step 2: collect from VOMO
Reuses the polling pattern from Sync Users to External System:JavaScript
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 samejoined_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
Step 5: surface cross-API insights
The whole point of joining is the questions you can now answer:Donor-volunteer overlap
Lapsed donors who are still volunteering
Top volunteers who haven’t been asked to give
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:- Bruce Wayne is both a VOMO User (
bruce@wayne.example) and a CRM+ Contact (bruce@wayne.example). They’re joined as onepartner_person_id. - Bruce updates his email in VOMO to
bruce.wayne@wayne.example. - The next VOMO sync sees a “new” user (different email).
- The next CRM+ sync sees the existing user (same email).
- Now there are two
partner_person_ids for Bruce — one for each email.
Detection
JavaScript
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
- Detect split candidates daily
- Surface them for human review
- The customer’s data steward approves merges in a UI
- The integration applies merges via the audit-logged operation
Step 7: cross-API reconciliation
Daily reconciliation catches drift between the two APIs:JavaScript
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
Token isolation
The two APIs use different tokens issued by different customer-side processes. Don’t conflate them:JavaScript
Pagination conventions are different
VOMO usespage 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
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.