The workflow for keeping a partner integration’s view of Raise data consistent with its view of CRM+ data — three reconciliation patterns and the operational patterns that keep them running cleanly.
For partner integrations that read data from both Raise and CRM+, reconciliation is the workflow that keeps the two views aligned. The platform-level sync between Raise and CRM+ is eventually consistent — most data flows through within seconds, but occasional records take longer, fail silently, or end up in inconsistent states. Reconciliation is how a partner integration detects and handles those cases.This page covers when reconciliation is needed, the three patterns that work, and the operational patterns that keep reconciliation running cleanly day-to-day. For the underlying mechanics of how data flows between the products, see How Raise Data Flows to CRM+.
Integration reads data only from Raise (or only from CRM+)
Skip — no reconciliation needed.
Integration reads from both products and presents a unified view
Use this workflow.
Integration writes to one product and confirms downstream visibility in the other
Use Pattern 2 (dual webhook).
Auditing a customer’s data integrity across both products
Use Pattern 3 (periodic backstop).
Initial setup or migration where both products are being aligned
Use a combination — Pattern 3 for initial sweep, Pattern 1 or 2 for ongoing.
For integrations that touch only one product (a Raise-only fundraising integration, a CRM+-only stewardship tool), reconciliation across the two isn’t needed. The remaining patterns assume both products are in scope.
The cheapest reconciliation pattern. For any given Raise record, the crmKey field tells you whether it has been synced and what its corresponding CRM+ record is.
For very large donor sets, throttle the concurrent CRM+ lookups — Promise.all of thousands of fetches will hit rate limits or overwhelm the network. Use a controlled-concurrency pattern (e.g., a queue with N workers) and pace per-customer per-product limits.
When events arrive from both products for the same logical entity (a Raise Gift and the CRM+ Gift that’s its synced counterpart), the integration treats the first arrival as the primary signal and the second as confirmation:
JavaScript
async function handleRaiseGiftEvent(payload) { const raiseGift = payload; // Look for an existing record in the integration's database const existing = await db.findByRaiseGiftId(raiseGift.id); if (existing) { // We've already seen this through another path — update with Raise data await db.update(existing.id, { raiseGiftSeenAt: new Date(), raiseGiftAmount: raiseGift.amount, raiseCrmKey: raiseGift.crmKey, }); } else { // First time seeing this gift — create a new record await db.create({ raiseGiftId: raiseGift.id, raiseGiftSeenAt: new Date(), raiseGiftAmount: raiseGift.amount, donorEmail: raiseGift.donor?.email, }); }}async function handleCrmGiftEvent(payload) { const crmGift = payload; // The CRM+ Gift carries a reference back to its source Raise Gift // (the exact field name depends on the CRM+ event payload shape) const sourceRaiseGiftId = crmGift.transactionSource === 'raise' ? crmGift.sourceId : null; if (sourceRaiseGiftId) { const existing = await db.findByRaiseGiftId(sourceRaiseGiftId); if (existing) { // Confirm CRM+ side received it await db.update(existing.id, { crmGiftId: crmGift.id, crmGiftSeenAt: new Date(), }); } else { // The CRM+ event arrived before the Raise event — unusual but possible await db.create({ raiseGiftId: sourceRaiseGiftId, crmGiftId: crmGift.id, crmGiftSeenAt: new Date(), }); } }}
The integration’s database ends up with one row per gift, with both raiseGiftSeenAt and crmGiftSeenAt populated when both events have arrived.
The most valuable signal from this pattern: gifts that arrive from one product but never confirm from the other within an expected window. A periodic check:
JavaScript
async function findOneSidedGifts(maxLagMinutes = 60) { const cutoff = new Date(Date.now() - maxLagMinutes * 60 * 1000); // Raise-side gifts that haven't confirmed in CRM+ const onlyInRaise = await db.find({ raiseGiftSeenAt: { lt: cutoff }, crmGiftSeenAt: null, }); // CRM+-side gifts that haven't confirmed in Raise (unusual but possible) const onlyInCrm = await db.find({ crmGiftSeenAt: { lt: cutoff }, raiseGiftSeenAt: null, }); return { onlyInRaise, onlyInCrm };}
onlyInRaise gifts older than an hour likely indicate a sync issue worth investigating. The customer’s admin team can use the gift ID to look up its sync state in the platform tools.
Don’t reconcile the current day during the day itself — running at noon and complaining about gifts created at 11:55 AM that aren’t in CRM+ yet produces false positives. Reconcile yesterday during today’s run:
JavaScript
async function dailyReconciliationJob() { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); yesterday.setHours(0, 0, 0, 0); const result = await reconcileGiftsForDay(yesterday); if (result.inRaiseNotInCrm.length > 0) { await alertOps({ message: `${result.inRaiseNotInCrm.length} Raise gifts missing in CRM+`, details: result.inRaiseNotInCrm.map((g) => g.id), }); } if (result.notYetLinked.length > 0) { // These may be gifts whose Campaign has canSync: false await alertOps({ severity: 'info', message: `${result.notYetLinked.length} Raise gifts with no crmKey from yesterday`, }); } return result;}
A gift in inRaiseNotInCrm after a 24-hour sync window is one of:
Cause
Resolution
Sync genuinely failed
Coordinate with the platform team — they have tools to inspect and retry.
The Campaign has canSync: false
This is expected behavior — filter these out before alerting. Check gift.campaign.canSync on the Raise side.
The customer doesn’t run CRM+
Single-product Raise customers will never have crmKey populated. Detect this once and skip reconciliation for that customer.
The integration is querying CRM+ with the wrong filter
Validate the CRM+ query returns gifts you know are there.
Build the alert to include enough context for ops to investigate without re-running diagnostics. At minimum: the Raise gift ID, the Campaign ID, the gift amount, and the Campaign’s canSync flag.
Gifts under campaigns with canSync: false. These aren’t discrepancies.
2. Filter out test-mode
Test-mode gifts may sync to test-mode CRM+ environments differently — confirm the scope.
3. Surface remaining cases
Build a daily report or ops alert with the gift IDs and context.
4. Coordinate with platform team
The platform may need to manually trigger a sync or fix a data issue.
The partner integration doesn’t have a “retry sync” API endpoint to call — the sync mechanism is platform-internal. The integration’s role is to detect and surface; the customer’s admin team and the platform team handle resolution.
A Raise record with a crmKey pointing at a non-existent CRM+ record means one of:
The CRM+ record was deleted after sync completed.
The crmKey was set incorrectly (e.g., by a partner integration that seeded a wrong value).
The fix is typically to clear the crmKey on the Raise side (via PATCH /api/Donor/{id}) and let sync re-establish the linkage — or coordinate with the customer’s admin team if the CRM+ record needs to be recreated.
A Raise Donor and its CRM+ Contact should have matching identity fields (name, email) once they’re linked. If they drift apart — donor’s email is updated in Raise but not reflected in CRM+ — the sync should catch up eventually, but partner integrations that detect drift can choose to:
Strategy
Description
Wait and recheck
Drift may resolve as the next sync cycle runs.
Surface for human review
The customer’s admin team can investigate why the sync isn’t catching up.
Treat Raise as authoritative
For partner integrations that read from both, defer to Raise as the canonical source when they disagree.
The right strategy depends on the integration’s purpose. For analytics integrations, deferring to one product as canonical is usually best; for stewardship integrations that need real-time accuracy, surfacing for review is safer.
A daily reconciliation job that runs after sync lag has settled (e.g., 6 AM the next morning) catches most issues with minimal false-positive noise. For higher-stakes integrations, hourly reconciliation with a larger lag window (4 hours) is acceptable.Don’t run reconciliation every few minutes — the per-customer query cost is significant and the false-positive rate from sync lag is high.
When discrepancies are detected, ops needs to know:
Which customer is affected
The specific record IDs in question
The amounts involved (gifts, especially)
The likely cause (canSync false, sync lag, genuine failure)
A direct link to the records in the Raise admin UI and the CRM+ admin UI
The crmKeyUrls field on Raise records is helpful here — it provides the direct URL to the CRM+ record without requiring ops to assemble URLs by ID. See How Raise Data Flows to CRM+: crmKeyUrls.
Tempting as it is to write “if discrepancy detected, retry sync” logic, the platform sync isn’t partner-controlled and auto-remediation can mask underlying issues. Surface discrepancies for human review; let the customer’s admin team and the platform team handle resolution.
Run this on a daily cron job (e.g., 6 AM) with date = yesterday. Track the metrics over time. Alert on actionable discrepancies but not on not_yet_linked or test-mode records.