Skip to main content
For customers running both Raise and CRM+, donation and donor data flows from Raise into CRM+ through a platform-level sync that lives outside both products’ APIs. Your Raise integration doesn’t call CRM+ directly, and your CRM+ integration doesn’t call Raise directly. This page documents what the sync is, what partners can see and influence from the Raise API, and the patterns for reading data from both products in a single workflow. The audience is partner integration teams that need to understand cross-product behavior — even when their integration only targets one product. Decisions like “should I match this donor by email or by CRM+ Contact ID?” depend on understanding the sync.

What flows where

The sync is one-directional: Raise → CRM+. Three resources have a sync mapping into CRM+:
Raise resourceCRM+ resource
DonorBecomes (or links to) a CRM+ Contact
GiftBecomes a CRM+ Gift
RecurringGiftBecomes a CRM+ RecurringGift
Other Raise resources (Campaigns, Segments, Projects, MotivationCodes) don’t have direct CRM+ equivalents that the sync creates. Campaign attribution does flow with synced Gifts — meaning a synced CRM+ Gift carries enough context to identify the Raise Campaign and Form that produced it.
This page describes the sync as observable from the Raise API. The full sync behavior, including timing guarantees, conflict resolution, and how data conflicts between Raise-originated and CRM+-originated changes are handled, is a platform-level concern that may evolve. The integration patterns on this page are conservative — they assume the sync exists and works, without depending on specific timing or behavior contracts.

Cross-product identifiers: crmKey, crmSecondKey, and crmKeyUrls

Three fields on Raise resources track the linkage to corresponding records in CRM+ (or other external CRMs):
FieldTypeWhere it appears
crmKeystringDonorModel, CampaignModel, RecurringGiftModel
crmSecondKeystringDonorModel only
crmKeyUrlsobject (map of integration type → CrmUrlInfo)DonorModel, CampaignModel, RecurringGiftModel

crmKey

The crmKey is the primary identifier of the corresponding record in the external CRM. For a Raise Donor that has been synced to CRM+, crmKey typically holds the CRM+ Contact’s ID. A Raise record that has not yet been synced (or whose sync was disabled) will have crmKey: null. Partner integrations can use the presence or absence of crmKey as a signal:
  • crmKey != null: the record has been synced to at least one external CRM.
  • crmKey == null: the record has not been synced yet, or sync is disabled for the parent Campaign.

crmSecondKey

A secondary identifier on Donors only. Used when the donor needs to be tracked across two distinct external systems, or when an alternative identifier (different from the primary CRM Contact ID) is meaningful.

crmKeyUrls

A map of integration type to CrmUrlInfo records. Each CrmUrlInfo has two fields:
{
  "crmKey": "VirtuousCRM_12345",
  "url": "https://app.virtuoussoftware.com/Contact/12345"
}
The map is keyed by integration type — for a customer running CRM+, the map typically has one entry under the CRM+ integration key. For a customer running both CRM+ and another CRM, the map can have multiple entries. The url field gives partner integrations a direct hyperlink to the donor’s record in the external CRM. Useful for “view in CRM” links inside the partner’s UI:
JavaScript
function buildCrmLink(donor) {
  // crmKeyUrls is keyed by integration type string
  const entries = Object.values(donor.crmKeyUrls || {});
  if (entries.length === 0) return null;
  return entries[0].url;
}

Setting crmKey on creation

Several create/update endpoints accept crmKey in the request body — partners can seed the linkage rather than waiting for the sync to populate it:
  • DonorRequestcrmKey field (single key)
  • UpdateDonorPatchRequestcrmKeys field (multiple keys for multi-CRM scenarios)
  • DonorPaymentRequest (the body of POST /api/Raise/give) — crmKey field
  • CampaignRequestcrmKey field
  • RecurringGiftRequestcrmKey field
Setting crmKey on creation is useful when an integration creates a Raise record from data that originated in CRM+: the integration knows the CRM+ Contact ID and can stamp it on the Raise Donor at creation time, avoiding any uncertainty during the sync.

Controlling sync at the Campaign level: canSync

The canSync boolean on a Campaign controls whether that Campaign’s data flows to CRM+. When canSync: true, Gifts produced under the campaign sync as expected; when canSync: false, those Gifts remain in Raise without propagating.

Toggling sync

PUT /api/Campaign/{campaignId}/toggle-sync flips the flag:
cURL
curl -X PUT https://prod-api.raisedonors.com/api/Campaign/5678/toggle-sync \
  -H "Authorization: Bearer YOUR_API_TOKEN"
Use this when:
  • The customer needs to pause sync for a specific campaign — e.g., during data cleanup or a CRM+ migration.
  • Test campaigns shouldn’t appear in CRM+ as production data.
  • An integration is performing a bulk import and wants CRM+ sync paused until the import is verified.
See Campaigns: Toggle sync for the full operational detail.

What canSync: false does and doesn’t do

When canSync is falseBehavior
New gifts created under the campaignContinue to be created in Raise normally.
Those gifts flowing to CRM+Do not propagate — they stay in Raise only.
Gifts already synced before the toggleRemain in CRM+. The toggle is not retroactive.
Donor records associated with the campaignSync to CRM+ may continue depending on whether they’re attached to other syncing campaigns.
Re-enabling sync (canSync: true) resumes propagation for new gifts but does not automatically backfill the gifts that occurred during the disabled window. For backfill of historical gifts during a sync-disabled period, coordinate with the customer’s admin team — there’s no API endpoint to trigger backfill.

What partner integrations can and can’t do across the two products

The platform-level sync is the only mechanism that moves data between Raise and CRM+. The implications:

Things partner integrations can do

CapabilityHow
Detect sync statusCheck crmKey on a Raise record — if non-null, it has been synced.
Seed the linkageSet crmKey when creating Raise records from CRM+-originated data.
Pause sync for specific campaignsUse PUT /api/Campaign/{campaignId}/toggle-sync.
Build “view in CRM” linksUse crmKeyUrls to get the direct URL.
Reconcile records across the two productsUse crmKey as the join key.
Subscribe to events from each product separatelyEach has its own webhook system. A giftCreate event in Raise and the corresponding giftCreate in CRM+ are separate webhook deliveries.

Things partner integrations can’t do

LimitationWhat this means
Force a one-time sync of a specific recordNo “sync this gift now” endpoint exists. The sync runs on its own cadence.
Query CRM+ data from the Raise APIRaise endpoints return only Raise data. To read CRM+ data, call the CRM+ API directly with CRM+ credentials.
Write to CRM+ via RaiseRaise endpoints only write Raise data. CRM+ writes use the CRM+ API.
Get sync timing guaranteesThe spec doesn’t document timing SLAs for the sync. Treat the sync as eventually consistent.
See sync errorsErrors that occur during the sync (e.g., a Raise Donor that fails to sync to CRM+) are not exposed via the Raise API. Coordinate with the customer’s admin team to investigate.

Reconciliation patterns

Partner integrations that read from both Raise and CRM+ — typically reporting tools or unified-view applications — need to reconcile the two data sets. Three patterns work well:

Pattern 1: crmKey as the join key

The most direct approach: use crmKey to match a Raise Donor to its CRM+ Contact. If both records exist, they’re the same entity:
JavaScript
async function findCrmContactForRaiseDonor(raiseDonorId) {
  // 1. Read the Raise Donor
  const raiseDonor = await fetch(
    `https://prod-api.raisedonors.com/api/Donor/${raiseDonorId}`,
    { headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` } }
  ).then((r) => r.json());

  if (!raiseDonor.crmKey) {
    // Not synced yet
    return null;
  }

  // 2. Use the crmKey to fetch the CRM+ Contact
  const crmContact = await fetch(
    `https://api.virtuoussoftware.com/api/Contact/${raiseDonor.crmKey}`,
    { headers: { Authorization: `Bearer ${process.env.CRM_API_TOKEN}` } }
  ).then((r) => r.json());

  return crmContact;
}
The pattern requires holding tokens for both products. See Authentication and the CRM+ equivalent.

Pattern 2: dual webhook subscription with deduplication

For integrations that want real-time updates from both products, subscribe to webhook events from each separately and deduplicate at the integration level:
JavaScript
// Two webhook handlers — one per product
async function handleRaiseGiftCreate(payload) {
  await ingestGift({
    source: 'raise',
    raiseGiftId: payload.giftId,
    crmKey: payload.crmKey,
  });
}

async function handleCrmGiftCreate(payload) {
  await ingestGift({
    source: 'crm',
    crmGiftId: payload.giftId,
    raiseGiftId: payload.sourceRaiseGiftId, // if exposed by the CRM+ payload
  });
}

async function ingestGift({ source, raiseGiftId, crmGiftId, crmKey }) {
  // Deduplicate by raiseGiftId — the Raise event arrives first
  const existing = await db.findByRaiseGiftId(raiseGiftId);
  if (existing) {
    await db.update(existing.id, { crmGiftId, lastSeenSource: source });
    return;
  }
  await db.create({ raiseGiftId, crmGiftId, crmKey, source });
}
The Raise event typically arrives first because it’s created at the time of donation; the CRM+ event arrives after the sync propagates. Integrations that need both should treat the Raise event as the primary signal and the CRM+ event as confirmation that sync completed.

Pattern 3: periodic reconciliation backstop

For high-confidence reconciliation regardless of webhook reliability, run a periodic backstop that queries both sides and reconciles discrepancies:
JavaScript
async function reconcileGiftsForDay(date) {
  // 1. Pull all Raise gifts for the day
  const raiseGifts = await queryRaiseGifts({ dateAfter: date.startOfDay() });

  // 2. Pull all CRM+ gifts that originated from Raise for the day
  const crmGifts = await queryCrmGifts({
    dateAfter: date.startOfDay(),
    transactionSource: 'raise',
  });

  // 3. Build a map by crmKey (which Raise has) ↔ source ID (which CRM+ has)
  const raiseByKey = new Map(raiseGifts.filter((g) => g.crmKey).map((g) => [g.crmKey, g]));
  const crmByKey = new Map(crmGifts.map((g) => [g.sourceRaiseGiftId, g]));

  // 4. Identify the discrepancies
  const inRaiseNotCrm = raiseGifts.filter((g) => !crmByKey.has(g.id));
  const inCrmNotRaise = crmGifts.filter((g) => !raiseByKey.has(g.sourceRaiseGiftId));

  // 5. Alert on persistent discrepancies (after allowing for sync lag)
  return { inRaiseNotCrm, inCrmNotRaise };
}
Allow a reasonable sync-lag window before flagging a “missing in CRM+” gift — depending on the customer’s environment, this could be minutes to hours. Don’t alert on a Raise gift created 5 minutes ago that hasn’t yet appeared in CRM+; do alert on a Raise gift from yesterday that’s still missing.

Timing expectations

The Raise spec does not document timing guarantees for the sync. In practice, partner integrations should plan for:
  • Most gifts appear in CRM+ within seconds to a few minutes after creation in Raise.
  • Occasional gifts may take longer due to processing batching, transient errors, or maintenance windows.
  • Rare gifts may fail to sync entirely if the source data has a problem (malformed donor record, missing required field on the CRM+ side, etc.).
Integrations that need real-time consistency across the two products should either:
  • Use the Raise event as the source of truth and treat CRM+‘s view as eventually consistent, or
  • Implement the periodic-reconciliation backstop (Pattern 3 above) to catch the rare cases where sync doesn’t complete.
Don’t build user-facing features that depend on a CRM+ Gift appearing in real time after a Raise donation — the experience will be unreliable.

When crmKey is missing

A Raise record with crmKey: null could be in any of several states:
StateWhat it means
Sync hasn’t run yetThe record was just created and the sync hasn’t processed it. Usually resolved within minutes.
Sync is disabled for the parentThe Campaign has canSync: false. Records under it don’t sync.
Sync failedAn error during sync prevented the link from being established. May need investigation.
The customer doesn’t run CRM+Single-product Raise customers will never have crmKey populated.
Partner integrations that read crmKey to drive behavior should not treat its absence as an error. Treat it as “linkage not established yet” and handle gracefully — show a “not yet synced” indicator in the UI rather than a hard failure.

Where to go next

Statuses and Lifecycle States

The lifecycle states across Donors, Gifts, and RecurringGifts that matter for sync reconciliation.

Campaigns

The Campaign resource where canSync and toggle-sync live.

Reconcile Raise with CRM+

The workflow that puts the reconciliation patterns on this page into production.

Sync Architecture Patterns

The broader architectural patterns for sync-aware integration design.
Last modified on May 20, 2026