> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# How Raise Data Flows to CRM+

> The platform-level sync between Raise and CRM+ — what flows where, how partner integrations observe and influence it, and the patterns for handling cross-product reconciliation.

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+**.

```mermaid theme={null}
graph LR
  subgraph Raise["Raise"]
    RDonor["Donor"]
    RGift["Gift"]
    RRG["RecurringGift"]
    RCampaign["Campaign"]
  end
  subgraph CRMP["CRM+"]
    Contact["Contact"]
    CGift["Gift"]
    CRG["RecurringGift"]
  end

  RDonor -->|platform sync| Contact
  RGift -->|platform sync| CGift
  RRG -->|platform sync| CRG
  RCampaign -.->|attribution only| CGift
```

Three resources have a sync mapping into CRM+:

| Raise resource    | CRM+ resource                        |
| ----------------- | ------------------------------------ |
| **Donor**         | Becomes (or links to) a CRM+ Contact |
| **Gift**          | Becomes a CRM+ Gift                  |
| **RecurringGift** | Becomes 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.

<Note>
  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.
</Note>

***

## Cross-product identifiers: `crmKey`, `crmSecondKey`, and `crmKeyUrls`

Three fields on Raise resources track the linkage to corresponding records in CRM+ (or other external CRMs):

| Field          | Type                                            | Where it appears                                    |
| -------------- | ----------------------------------------------- | --------------------------------------------------- |
| `crmKey`       | string                                          | `DonorModel`, `CampaignModel`, `RecurringGiftModel` |
| `crmSecondKey` | string                                          | `DonorModel` only                                   |
| `crmKeyUrls`   | object (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:

```json theme={null}
{
  "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 JavaScript theme={null}
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:

* `DonorRequest` — `crmKey` field (single key)
* `UpdateDonorPatchRequest` — `crmKeys` field (multiple keys for multi-CRM scenarios)
* `DonorPaymentRequest` (the body of `POST /api/Raise/give`) — `crmKey` field
* `CampaignRequest` — `crmKey` field
* `RecurringGiftRequest` — `crmKey` 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:

```bash cURL theme={null}
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](/raise/concepts/campaigns#toggle-sync) for the full operational detail.

### What `canSync: false` does and doesn't do

| When `canSync` is `false`                  | Behavior                                                                                    |
| ------------------------------------------ | ------------------------------------------------------------------------------------------- |
| New gifts created under the campaign       | Continue to be created in Raise normally.                                                   |
| Those gifts flowing to CRM+                | **Do not propagate** — they stay in Raise only.                                             |
| Gifts already synced before the toggle     | Remain in CRM+. The toggle is not retroactive.                                              |
| Donor records associated with the campaign | Sync 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

| Capability                                           | How                                                                                                                                        |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Detect sync status**                               | Check `crmKey` on a Raise record — if non-null, it has been synced.                                                                        |
| **Seed the linkage**                                 | Set `crmKey` when creating Raise records from CRM+-originated data.                                                                        |
| **Pause sync for specific campaigns**                | Use `PUT /api/Campaign/{campaignId}/toggle-sync`.                                                                                          |
| **Build "view in CRM" links**                        | Use `crmKeyUrls` to get the direct URL.                                                                                                    |
| **Reconcile records across the two products**        | Use `crmKey` as the join key.                                                                                                              |
| **Subscribe to events from each product separately** | Each 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

| Limitation                                     | What this means                                                                                                                                                                 |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Force a one-time sync of a specific record** | No "sync this gift now" endpoint exists. The sync runs on its own cadence.                                                                                                      |
| **Query CRM+ data from the Raise API**         | Raise endpoints return only Raise data. To read CRM+ data, call the CRM+ API directly with CRM+ credentials.                                                                    |
| **Write to CRM+ via Raise**                    | Raise endpoints only write Raise data. CRM+ writes use the CRM+ API.                                                                                                            |
| **Get sync timing guarantees**                 | The spec doesn't document timing SLAs for the sync. Treat the sync as eventually consistent.                                                                                    |
| **See sync errors**                            | Errors 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 JavaScript theme={null}
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](/raise/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 JavaScript theme={null}
// 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 JavaScript theme={null}
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:

| State                           | What it means                                                                                  |
| ------------------------------- | ---------------------------------------------------------------------------------------------- |
| Sync hasn't run yet             | The record was just created and the sync hasn't processed it. Usually resolved within minutes. |
| Sync is disabled for the parent | The Campaign has `canSync: false`. Records under it don't sync.                                |
| Sync failed                     | An 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

<CardGroup cols={2}>
  <Card title="Statuses and Lifecycle States" icon="circle-half-stroke" href="/raise/concepts/statuses-and-lifecycle-states">
    The lifecycle states across Donors, Gifts, and RecurringGifts that matter for sync reconciliation.
  </Card>

  <Card title="Campaigns" icon="bullhorn" href="/raise/concepts/campaigns">
    The Campaign resource where `canSync` and `toggle-sync` live.
  </Card>

  <Card title="Reconcile Raise with CRM+" icon="arrows-rotate" href="/raise/workflows/reconcile-with-crm-plus">
    The workflow that puts the reconciliation patterns on this page into production.
  </Card>

  <Card title="Sync Architecture Patterns" icon="diagram-project" href="/raise/best-practices/sync-architecture-patterns">
    The broader architectural patterns for sync-aware integration design.
  </Card>
</CardGroup>
