> ## 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.

# Fundraising Platform to Virtuous CRM

> A complete integration recipe for syncing peer-to-peer, crowdfunding, and team-based fundraising platforms with Virtuous CRM+ — donor and fundraiser attribution, campaign mapping, and soft-credit handling.

This recipe walks through building a fundraising platform integration with Virtuous CRM+. Fundraising platforms are a distinct category from straight payment processors: a donation doesn't just connect a donor to a nonprofit, it also passes through a **fundraiser** — an individual or team who recruited the gift. The integration needs to record all three (donor, fundraiser, nonprofit) and the relationships between them.

The recipe is platform-agnostic. It applies to peer-to-peer platforms (where donors give in support of an individual fundraiser), team-fundraising platforms (multiple fundraisers organized into teams), crowdfunding platforms (where many donors fund a single campaign), and event-tied fundraisers (walk-a-thons, charity 5Ks). The Virtuous-side modeling is the same; the source-platform specifics differ.

<Note>
  This recipe describes patterns that apply across the fundraising-platform category. Specific platforms (Classy, GiveLively, Donorbox, Bonterra, GoFundMe Charity, and others) have their own API shapes, event names, and data models — confirm against the specific platform's current documentation when implementing.
</Note>

## Architecture

```mermaid theme={null}
graph LR
  FP["Fundraising Platform<br/>(donation + fundraiser events)"]
  Webhook["Webhook Receiver"]
  Queue["Outbound Queue"]
  ContactSync["Contact Sync<br/>(donor + fundraiser)"]
  GiftSync["Gift Sync<br/>(with credit attribution)"]
  RelSync["Relationship Sync<br/>(donor-fundraiser link)"]
  Virtuous["Virtuous CRM+"]

  FP -->|donation, registration events| Webhook
  Webhook --> Queue
  Queue --> ContactSync
  Queue --> GiftSync
  Queue --> RelSync
  ContactSync -->|POST /api/Contact/Transaction| Virtuous
  GiftSync -->|POST /api/v2/Gift/Transaction| Virtuous
  RelSync -->|POST /api/Relationship| Virtuous
```

Three parallel sync paths flowing from the same source events:

* **Contact sync** ensures both the donor and the fundraiser exist as Virtuous Contacts.
* **Gift sync** records the donation with the fundraiser-aware designation and references.
* **Relationship sync** (optional) creates Relationship records connecting donors to the fundraisers who recruited them — useful for fundraising-effectiveness reporting.

Each sync path runs through the standard [Sync External Donations](/crm/workflows/sync-external-donations) architecture (queue, submitter, webhook confirmation, reconciliation). The differences are in the field mapping and the multi-record-per-event handling.

***

## Field mapping

### The three participants in a fundraiser donation

| Participant    | Role                                                          | Virtuous representation                                                                            |
| -------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Donor**      | The person who gave the money.                                | Contact with `referenceSource: "FundraisingPlatform"` and `referenceId` = the platform's donor ID. |
| **Fundraiser** | The individual or team whose page or campaign drove the gift. | Contact (if they're a person) or organization Contact (if a team).                                 |
| **Nonprofit**  | The customer's organization receiving the gift.               | The Virtuous organization the integration writes to. Always the same; no per-gift mapping needed.  |

### Gift fields with fundraiser context

| Virtuous field                   | Source                                                                                      |
| -------------------------------- | ------------------------------------------------------------------------------------------- |
| `transactionSource`              | `"FundraisingPlatform"`                                                                     |
| `transactionId`                  | The platform's unique donation ID                                                           |
| `contact.referenceId`            | The donor's platform ID (used for matching against existing Contact)                        |
| `amount`                         | The amount the donor pledged (gross, before platform fees)                                  |
| `giftDate`                       | When the donor's payment processed                                                          |
| `giftType`                       | Typically `"Credit"` for card payments through the platform                                 |
| `giftDesignations[].projectCode` | Maps to a Virtuous Project — see "Campaign mapping" below                                   |
| Custom field or note             | The fundraiser's Contact ID — captured so Virtuous reporting can attribute the gift to them |

<Tip>
  Decide with the customer's team whether the recorded `amount` should be the donor's pledged amount (gross) or the amount the nonprofit actually received after platform fees (net). The platforms typically expose both. Most customers prefer gross — it reflects the donor's intent and matches the donor's tax receipt — with a separate custom field tracking the net amount or fees.
</Tip>

### Campaign and Project mapping

The fundraising platform's campaign concept needs to map to a Virtuous Campaign + Project hierarchy:

| Source concept                                                               | Virtuous mapping                                                 |
| ---------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Platform Campaign (e.g., "2024 Annual Marathon")                             | Virtuous Campaign                                                |
| Platform Subcampaign or Fund (e.g., "Education program within the marathon") | Virtuous Project (designation target)                            |
| Platform fundraiser page                                                     | Not directly modeled — captured via fundraiser Contact reference |

Walk through every Campaign in the customer's fundraising platform and map each to a Virtuous Campaign + Project at integration setup time. Store this mapping in your integration's configuration so the submitter can look up the right `projectCode` for each donation.

***

## Step 1: ensure both Contacts exist

When a donation event arrives, your integration first ensures both the donor and the fundraiser have Virtuous Contacts. The donor is straightforward; the fundraiser needs more care because they may already be a known supporter, staff member, or board member.

### The donor

The standard [Create a Contact](/crm/workflows/create-a-contact) pattern with the fundraising platform as the reference source:

```javascript JavaScript theme={null}
async function ensureDonorContact(donation) {
  return submitContactTransaction({
    referenceSource: 'FundraisingPlatform',
    referenceId: donation.donor.platformId,
    contactType: donation.donor.isOrganization ? 'Organization' : 'Household',
    firstName: donation.donor.firstName,
    lastName: donation.donor.lastName,
    emailType: 'Home Email',
    email: donation.donor.email,
    phoneType: 'Mobile Phone',
    phone: donation.donor.phone,
    address1: donation.donor.address?.line1,
    city: donation.donor.address?.city,
    state: donation.donor.address?.state,
    postal: donation.donor.address?.postal,
    country: donation.donor.address?.country ?? 'US',
    originSegmentCode: 'FUNDRAISER-DONATION',
  });
}
```

### The fundraiser

Fundraisers are typically existing supporters of the nonprofit — staff, board members, regular donors who chose to set up a page, or community members. Try harder to match them against existing Contacts than for donors:

```javascript JavaScript theme={null}
async function ensureFundraiserContact(fundraiser, token) {
  // Step 1: Try to find by the fundraising platform's reference
  const byRef = await fetch(
    `https://api.virtuoussoftware.com/api/Contact/Find?referenceSource=FundraisingPlatform&referenceId=${encodeURIComponent(fundraiser.platformId)}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  if (byRef.ok) return await byRef.json();

  // Step 2: Try by email
  const byEmail = await fetch(
    `https://api.virtuoussoftware.com/api/Contact/Find?email=${encodeURIComponent(fundraiser.email)}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  if (byEmail.ok) {
    // Found by email but no fundraiser reference yet — capture the reference
    const existing = await byEmail.json();
    await addFundraiserReferenceToContact(existing.id, fundraiser.platformId);
    return existing;
  }

  // Step 3: Submit a Contact Transaction for the fundraiser
  return submitContactTransaction({
    referenceSource: 'FundraisingPlatform',
    referenceId: fundraiser.platformId,
    contactType: 'Household',
    firstName: fundraiser.firstName,
    lastName: fundraiser.lastName,
    emailType: 'Home Email',
    email: fundraiser.email,
    originSegmentCode: 'FUNDRAISER-PERSONA',
    tags: 'Fundraiser',
  });
}
```

Three patterns to note:

* **Multi-step lookup** before creating. Fundraisers are far more likely than random donors to already exist in Virtuous.
* **Email is the secondary key.** A fundraiser who's also a regular donor will have an email match even when the platform reference isn't yet captured.
* **The `Fundraiser` tag** lets the customer's team segment fundraisers in reporting without needing a custom field.

### Teams as Contacts

If the fundraising platform supports team fundraising, the team itself is typically modeled as an Organization-type Contact:

```javascript JavaScript theme={null}
async function ensureTeamContact(team) {
  return submitContactTransaction({
    referenceSource: 'FundraisingPlatform',
    referenceId: `team-${team.platformId}`,            // disambiguate from individuals
    contactType: 'Organization',
    name: team.name,
    description: team.description,
    originSegmentCode: 'FUNDRAISER-TEAM',
    tags: 'Fundraising Team',
  });
}
```

Individual team members are separate Contacts; the team relationship is captured via a Virtuous Relationship — see Step 3 below.

***

## Step 2: submit the Gift with fundraiser context

The Gift Transaction records the donation. The challenge: standard `POST /api/v2/Gift/Transaction` doesn't have a built-in field for "this gift was credited to fundraiser X." Two patterns work:

### Pattern A: custom field on the Gift

If the customer has configured a Gift custom field for fundraiser attribution (e.g., a field called "Fundraiser Contact ID"), include it on the Transaction:

```javascript JavaScript theme={null}
async function submitFundraiserGift(donation, fundraiserContactId) {
  return submitGiftTransaction({
    transactionSource: 'FundraisingPlatform',
    transactionId: donation.id,
    contact: {
      referenceId: donation.donor.platformId,
      // ... full donor block from Step 1
    },
    giftDate: donation.processedDate,
    giftType: 'Credit',
    amount: donation.amount,
    giftDesignations: [
      {
        projectCode: mapCampaignToProjectCode(donation.campaign),
        amountDesignated: donation.amount,
      },
    ],
    customFields: [
      { name: 'Fundraiser Contact ID', value: fundraiserContactId.toString() },
      { name: 'Fundraiser Page URL', value: donation.fundraiserPageUrl },
      { name: 'Platform Fee', value: donation.platformFee?.toFixed(2) ?? '0.00' },
    ],
  });
}
```

This is the cleanest pattern for filtering and reporting — the customer can run a Query that filters by `"Fundraiser Contact ID" Is X` to find all gifts credited to a specific fundraiser.

### Pattern B: ContactNote with fundraiser linkage

If the customer doesn't have Gift custom fields configured, record the fundraiser context as a ContactNote on the donor:

```javascript JavaScript theme={null}
async function recordFundraiserContext(donorContactId, donation, fundraiser) {
  await fetch('https://api.virtuoussoftware.com/api/ContactNote', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      contactId: donorContactId,
      type: 'Fundraiser Attribution',
      note: `Gift via fundraising platform attributed to fundraiser ${fundraiser.name} (Virtuous ID ${fundraiser.contactId}). Page: ${donation.fundraiserPageUrl}`,
      date: donation.processedDate,
    }),
  });
}
```

Less powerful for reporting but workable when custom fields aren't available.

<Tip>
  Recommend Pattern A to customers during integration onboarding. A fundraiser-attribution custom field on Gifts unlocks meaningful reporting (top fundraisers by dollars raised, fundraiser-driven retention rates) that's painful to build any other way.
</Tip>

***

## Step 3: create the donor-fundraiser Relationship (optional)

For customers who want to track donor-fundraiser relationships explicitly — useful for stewardship workflows like "send a thank-you to the donor's fundraiser when the donor gives again" — create a Virtuous Relationship record linking them:

```javascript JavaScript theme={null}
async function recordFundraiserRelationship(donorContactId, fundraiserContactId, donation) {
  // First, discover what Relationship Types are configured in the organization
  const types = await fetch(
    'https://api.virtuoussoftware.com/api/Relationship/Types',
    { headers: { Authorization: `Bearer ${token}` } }
  ).then((r) => r.json());

  const fundraiserType = types.find((t) => t.name === 'Recruited By');
  if (!fundraiserType) {
    console.warn('Relationship type "Recruited By" not configured — skipping relationship');
    return;
  }

  const response = await fetch(
    'https://api.virtuoussoftware.com/api/Relationship',
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contactId: donorContactId,
        relatedContactId: fundraiserContactId,
        relationshipTypeId: fundraiserType.id,
        notes: `Recruited via fundraising platform on ${donation.processedDate}`,
      }),
    }
  );

  if (!response.ok) {
    console.error(`Relationship create failed: ${response.status}`);
  }
}
```

Two notes on the Relationship pattern:

* **The relationship type must already be configured** in the customer's Virtuous organization. Your integration cannot create relationship types via API. Discover the configured types via `GET /api/Relationship/Types` and gracefully skip relationship creation if the expected type isn't present.
* **The relationship has no expiration** — once created, it persists. This is usually what customers want, but for stewardship purposes it can produce confusing reporting if a donor was recruited five years ago but has been giving directly ever since. Consider whether your integration also tracks "most recent recruiter" separately, perhaps as a custom field that gets updated on each gift.

***

## Step 4: handle event-tied fundraisers

Many fundraising platforms organize fundraisers around events (charity 5Ks, walk-a-thons, gala registrations with fundraising). The event itself is a separate entity that the gift, fundraiser, and donor should all link to.

In Virtuous, events are first-class records (`POST /api/Event` and the `eventCreate` webhook). The recommended modeling:

| Source concept                          | Virtuous representation                                                                                                         |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| The event (e.g., "2024 Charity 5K")     | Virtuous Event record                                                                                                           |
| The Campaign for the event              | Virtuous Campaign linked to the Event                                                                                           |
| Each fundraiser page tied to the event  | Fundraiser Contact, optionally tagged with the event name                                                                       |
| Each donation through a fundraiser      | Gift designated to the Event's Project; custom field links to fundraiser                                                        |
| Event registration (the ticket portion) | Separate Gift with appropriate gift type — see [Auction/Event Platform recipe](/crm/recipes/auction-event-platform-to-virtuous) |

For an event-tied gift:

```javascript JavaScript theme={null}
await submitGiftTransaction({
  transactionSource: 'FundraisingPlatform',
  transactionId: donation.id,
  contact: { referenceId: donation.donor.platformId, ... },
  giftDate: donation.processedDate,
  giftType: 'Credit',
  amount: donation.amount,
  giftDesignations: [
    {
      projectCode: mapEventToProjectCode(donation.event),
      amountDesignated: donation.amount,
    },
  ],
  customFields: [
    { name: 'Fundraiser Contact ID', value: fundraiserContactId.toString() },
    { name: 'Event', value: donation.event.name },
  ],
});
```

If the customer wants per-event reporting beyond Project-based segmentation, consider adding an "Event Name" tag to each gift's contact (via `originSegmentCode` on the Contact Transaction or a tag update) so segmentation works at the Contact level too.

***

## Common edge cases

### A fundraiser is also a donor

The same person might run a fundraising page **and** donate through someone else's page. The patterns above handle this correctly — `Contact/Find` will return the same Contact in both cases, and the `Fundraiser` tag plus the donor's giving history both attach to that single Contact.

### A donor gives anonymously

Some platforms support anonymous donations — the donor's identity is hidden from the fundraiser but the platform still passes it to the nonprofit. Submit the Contact Transaction normally (the nonprofit needs the donor record for tax receipting) but consider tagging the Contact with `"Anonymous Through Fundraiser"` so the customer's team knows the donor doesn't want to be publicly thanked.

### Platform fees

Most platforms deduct a fee before the nonprofit receives the gift. Three handling options:

* **Gross with custom field**: record `amount` as the donor's pledged amount, record the fee in a custom field. Most accurate for donor reporting.
* **Net**: record only the amount the nonprofit received. Simpler but loses the donor's intent.
* **Two-Gift split**: record the gross amount and a separate offsetting "Platform Fee" gift. Most complex but mirrors what an accountant would book.

Most customers want the gross-with-custom-field pattern. Document the choice in your integration's customer-facing documentation.

### Pledged-but-not-yet-paid

Some fundraising platforms support pledges (e.g., "I'll give \$1,000 if you reach your goal"). These are commitments, not collected money — record them as Virtuous Pledges, not Gifts, until the actual payment processes. Pledges have their own endpoint family (`/api/v2/Pledge/*`) outside the scope of this recipe.

### A team gift

A team itself may receive a gift directly (someone donates "to the team" rather than to an individual fundraiser on the team). Submit the Gift with the team's Organization-type Contact as the recipient:

```javascript JavaScript theme={null}
await submitGiftTransaction({
  transactionSource: 'FundraisingPlatform',
  transactionId: donation.id,
  contact: { referenceId: donation.donor.platformId, ... },
  // ...
  customFields: [
    { name: 'Fundraiser Contact ID', value: teamContactId.toString() },
    { name: 'Fundraiser Type', value: 'Team' },
  ],
});
```

For team-fundraising-effectiveness reporting, customers may also want to see gifts rolled up across team members. This is typically a reporting concern (a Query that filters by "team membership"), not a data-modeling concern — as long as the Fundraiser Contact ID points at the right entity, the rollup query works.

***

## Production readiness checklist

* Both donor and fundraiser have Virtuous Contacts before the Gift is submitted.
* Fundraiser lookup tries platform reference, then email, before creating a new Contact.
* Campaign-to-Project mapping is configured at integration setup time, not inferred at runtime.
* Gift custom fields (or fallback ContactNotes) capture the fundraiser context.
* Donor-fundraiser Relationships are created only when the corresponding relationship type is configured in Virtuous.
* Platform fees are handled per the customer's chosen pattern (gross + custom field is recommended).
* Event-tied fundraisers link to the appropriate Virtuous Event and Campaign.
* Anonymous donations are tagged so the customer's team doesn't surface them publicly.
* Reconciliation queries periodically check that submitted donations resulted in Gifts attributed to the right fundraiser.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Auction/Event Platform to Virtuous CRM" icon="ticket" href="/crm/recipes/auction-event-platform-to-virtuous">
    Event-specific patterns — ticket-vs-donation splitting, auction items, sponsorships.
  </Card>

  <Card title="Stripe to Virtuous CRM" icon="layer-group" href="/crm/recipes/stripe-to-virtuous">
    The payment-processor recipe that powers most fundraising platforms under the hood.
  </Card>

  <Card title="Build a Two-Way Sync" icon="arrow-right-arrow-left" href="/crm/workflows/build-a-two-way-sync">
    Extend this one-way recipe with bidirectional sync if the customer wants Virtuous-side changes reflected back.
  </Card>

  <Card title="Relationships and IDs" icon="users" href="/crm/concepts/relationships-and-ids">
    The Relationship endpoints and lifecycle behavior used in Step 3.
  </Card>
</CardGroup>
