Skip to main content
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.
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.

Architecture

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

ParticipantRoleVirtuous representation
DonorThe person who gave the money.Contact with referenceSource: "FundraisingPlatform" and referenceId = the platform’s donor ID.
FundraiserThe individual or team whose page or campaign drove the gift.Contact (if they’re a person) or organization Contact (if a team).
NonprofitThe 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 fieldSource
transactionSource"FundraisingPlatform"
transactionIdThe platform’s unique donation ID
contact.referenceIdThe donor’s platform ID (used for matching against existing Contact)
amountThe amount the donor pledged (gross, before platform fees)
giftDateWhen the donor’s payment processed
giftTypeTypically "Credit" for card payments through the platform
giftDesignations[].projectCodeMaps to a Virtuous Project — see “Campaign mapping” below
Custom field or noteThe fundraiser’s Contact ID — captured so Virtuous reporting can attribute the gift to them
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.

Campaign and Project mapping

The fundraising platform’s campaign concept needs to map to a Virtuous Campaign + Project hierarchy:
Source conceptVirtuous 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 pageNot 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 pattern with the fundraising platform as the reference source:
JavaScript
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
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
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
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
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.
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.

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
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 conceptVirtuous representation
The event (e.g., “2024 Charity 5K”)Virtuous Event record
The Campaign for the eventVirtuous Campaign linked to the Event
Each fundraiser page tied to the eventFundraiser Contact, optionally tagged with the event name
Each donation through a fundraiserGift 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
For an event-tied gift:
JavaScript
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
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

Auction/Event Platform to Virtuous CRM

Event-specific patterns — ticket-vs-donation splitting, auction items, sponsorships.

Stripe to Virtuous CRM

The payment-processor recipe that powers most fundraising platforms under the hood.

Build a Two-Way Sync

Extend this one-way recipe with bidirectional sync if the customer wants Virtuous-side changes reflected back.

Relationships and IDs

The Relationship endpoints and lifecycle behavior used in Step 3.
Last modified on May 21, 2026