Skip to main content
A recurring donor is a donor who has committed to giving on a schedule — 50/monthtogeneralfund,50/month to general fund, 25/week to a campaign, $1,200/year as an annual pledge. In Virtuous, this commitment is represented as a RecurringGift record, separate from but linked to the actual Gift records that get created each time the schedule produces a payment. This recipe covers the full RecurringGift lifecycle from the partner integration perspective: creating a schedule when a donor signs up, updating it when they change their amount or designation, cancelling it when they end the commitment, and linking each scheduled payment back to the schedule. The Virtuous-side endpoints are all API-accessible, so unlike Contact merges, the entire lifecycle can be managed programmatically. If your integration only handles one-time gifts, you can skip this page. Anything that handles recurring donations needs the patterns here.

The data model

The RecurringGift describes the schedule (frequency, amount, designations, start date, status). Each scheduled payment becomes its own Gift record, linked to the RecurringGift via the recurringGiftTransactionId field on the Gift Transaction submission. Two important consequences of this model:
  • The RecurringGift itself is not money. It’s a commitment record. The Gifts that get linked to it represent actual transactions.
  • Cancelling a RecurringGift does not delete past Gifts. Cancellation stops new payments from being recorded against the schedule; the historical record of past payments remains.

RecurringGift endpoints

The endpoints available for managing RecurringGifts:
EndpointUse
POST /api/RecurringGiftCreate a new recurring gift schedule.
GET /api/RecurringGift/{recurringGiftId}Retrieve a single schedule by ID.
GET /api/RecurringGift/ByContact/{contactId}List all schedules for a specific Contact.
PUT /api/RecurringGift/{recurringGiftId}Update an existing schedule.
PUT /api/RecurringGift/Cancel/{recurringGiftId}Mark a schedule as cancelled.
POST /api/RecurringGift/QuerySearch schedules by structured filters.
GET /api/RecurringGift/QueryOptionsRetrieve valid filter parameters.
POST /api/RecurringGiftPayment/{recurringGiftId}Record a manual payment against a schedule (rare for partners — most payments come via Gift Transactions).

Creating a schedule

When a donor signs up for a recurring donation on your platform — a Stripe Subscription, a recurring scheduled donation in your platform’s database — create the corresponding RecurringGift in Virtuous.
curl -X POST https://api.virtuoussoftware.com/api/RecurringGift \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionSource": "YourPlatform",
    "transactionId": "sub_xyz123",
    "contactId": 4821,
    "startDate": "2024-12-15",
    "amount": 50.00,
    "frequency": "Monthly",
    "automatedPayments": true,
    "trackPayments": true,
    "designations": [
      {
        "projectCode": "MONTHLY-GIVING",
        "amountDesignated": 50.00
      }
    ]
  }'
The response contains the new schedule’s id — store it on your side alongside your platform’s commitment record.
FieldRequiredPurpose
contactIdYesThe donor’s Virtuous Contact ID. Resolve via Create a Contact before creating the RecurringGift.
transactionSource + transactionIdStrongly recommendedIdempotency key. As with Gifts, use a stable identifier from your platform — typically the recurring schedule’s own ID, not the first payment’s ID.
startDateYesWhen the schedule begins. Use the date of the donor’s first scheduled payment.
amountYesPer-payment amount.
frequencyYesThe cadence — typically Monthly, Quarterly, Annually.
designations[]YesAt least one designation. Designation amounts must sum to amount.

Frequency values

The CRM+ spec does not enumerate the valid frequency values for RecurringGifts. Common patterns include Monthly, Quarterly, Annually, and Weekly — but the canonical enum is not documented.

Pre-flight: the donor’s Contact must exist

POST /api/RecurringGift takes a contactId directly — there is no embedded contact data with matching, unlike POST /api/v2/Gift/Transaction. Resolve or create the donor’s Contact before creating the schedule:
JavaScript
// Sequence:
// 1. Find or create the Contact
let contactId = await findContactByReference(commitment.donor.platformId, token);
if (!contactId) {
  // Submit a Contact Transaction first, wait for it to resolve, then proceed
  await submitContactTransaction(commitment.donor, token);
  contactId = await waitForContactResolution(commitment.donor.platformId, token);
}

// 2. Then create the RecurringGift
await createRecurringGift(commitment, contactId, token);
This sequencing makes RecurringGift creation slower than Gift Transaction creation — you have to wait for the Contact to resolve through the nightly batch before the schedule can be created. For new donors signing up for recurring giving, this means the RecurringGift typically isn’t created in Virtuous until the day after the donor signed up. If your platform’s flow needs the schedule visible in Virtuous faster, the alternative is to submit the first payment via POST /api/v2/Gift/Transaction (which has embedded contact matching) and create the RecurringGift after the Contact appears. The RecurringGift links forward to the next payment; the first payment is one-off in Virtuous.

Linking payments to a schedule

Each time the recurring schedule produces a payment — Stripe charges the subscription, your platform’s recurring billing fires — submit a Gift Transaction with recurringGiftTransactionId set to the schedule’s transactionId:
JavaScript
async function recordRecurringPayment(payment, token) {
  await fetch(
    'https://api.virtuoussoftware.com/api/v2/Gift/Transaction',
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        transactionSource: 'YourPlatform',
        transactionId: payment.paymentId,                  // The specific payment, not the schedule
        recurringGiftTransactionId: payment.scheduleId,    // The schedule's transactionId
        contact: { referenceId: payment.donor.platformId },
        giftDate: payment.date,
        giftType: 'Cash',
        amount: payment.amount.toFixed(2),
        currencyCode: 'USD',
        batch: 'Recurring',
        giftDesignations: [
          {
            projectCode: payment.projectCode,
            amountDesignated: payment.amount.toFixed(2),
          },
        ],
      }),
    }
  );
}
Two pieces:
  • transactionId is the specific payment’s ID — different on every payment of the schedule.
  • recurringGiftTransactionId is the schedule’s ID — the same value on every payment.
Virtuous’s matching algorithm uses recurringGiftTransactionId to associate the new Gift with the existing schedule. In the UI, the Gift appears in the schedule’s payment history; in reports, the donor’s recurring total reflects the new payment.

Updating a schedule

When the donor changes their recurring amount, switches their designation, or otherwise modifies the schedule, push the change to Virtuous with PUT /api/RecurringGift/{recurringGiftId}. Follow the same GET-then-PUT pattern as Contact updates (see Update a Contact):
JavaScript
async function updateRecurringAmount(recurringGiftId, newAmount, token) {
  // 1. Read the current schedule
  const current = await fetch(
    `https://api.virtuoussoftware.com/api/RecurringGift/${recurringGiftId}`,
    { headers: { Authorization: `Bearer ${token}` } }
  ).then((r) => r.json());

  // 2. Modify
  const updated = {
    ...current,
    amount: newAmount,
    designations: current.designations.map((d) => ({
      ...d,
      // Distribute the new amount proportionally across existing designations
      amountDesignated: (d.amountDesignated / current.amount) * newAmount,
    })),
  };

  // 3. Write
  await fetch(
    `https://api.virtuoussoftware.com/api/RecurringGift/${recurringGiftId}`,
    {
      method: 'PUT',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(updated),
    }
  );
}
The same PUT-as-PATCH ambiguity discussed in Update a Contact applies — send the complete record on every update.

Common update scenarios

Donor actionFields to change
Increase or decrease amountamount and proportional designations[].amountDesignated.
Change designationReplace the designations[] array. Confirm the new amounts still sum to amount.
Change frequencyfrequency. Some platforms also require an updated nextExpectedPaymentDate.
Donor’s payment method updatedNo direct change — payment method is tracked by your platform, not by Virtuous. The schedule continues at the same amount and frequency.

Cancelling a schedule

When a donor ends their recurring commitment — actively unsubscribes, cancels in your platform’s portal, or has their payment method fail past the dunning threshold — cancel the schedule in Virtuous:
cURL
curl -X PUT https://api.virtuoussoftware.com/api/RecurringGift/Cancel/12345 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
JavaScript
async function cancelRecurringGift(recurringGiftId, token) {
  await fetch(
    `https://api.virtuoussoftware.com/api/RecurringGift/Cancel/${recurringGiftId}`,
    {
      method: 'PUT',
      headers: { Authorization: `Bearer ${token}` },
    }
  );
}
The dedicated Cancel endpoint is the right path for cancellation. Do not use PUT /api/RecurringGift/{id} with status: "Cancelled" — the canonical lifecycle transition is via the Cancel endpoint, which sets the cancelDateTimeUtc field and triggers the appropriate audit log entry.
The CRM+ API does not document an explicit endpoint to pause-and-resume a recurring schedule. Cancellation is one-way. If your platform supports pause/resume and you need to model that in Virtuous, two options:
  1. Cancel the existing schedule and create a new one when the donor resumes.
  2. Maintain pause/resume state on your side and stop submitting payment Gift Transactions during the paused period — the Virtuous schedule appears active but receives no payments.
Option 2 is generally less disruptive to reporting but produces a schedule with no payments during the pause window — which may confuse the customer’s team. Discuss with the customer.

Handling failed payments

When a scheduled payment fails (card expired, insufficient funds, account closed), the partner platform typically enters a dunning state — retrying the payment over several days before giving up. The Virtuous-side handling depends on the eventual outcome:
Eventual outcomeVirtuous action
Payment succeeds on retrySubmit the successful payment as a Gift Transaction with recurringGiftTransactionId. Schedule continues.
Payment fails permanentlyNo Gift submission. Surface to the customer to follow up with the donor; if the donor doesn’t update payment info, cancel the schedule.
Donor updates payment methodThe next successful payment is recorded normally. Schedule continues.
Importantly: do not submit failed payments as Gifts to Virtuous. A failed payment is not a gift, and recording it produces incorrect totals. Only submit when the payment actually succeeded. If the customer’s team needs visibility into failed payments — for outreach to the donor — surface them in your integration’s UI or as a separate report, not as Virtuous Gifts.

Reconciliation

Periodic reconciliation between your platform’s recurring schedules and Virtuous catches drift:
JavaScript
async function reconcileRecurringSchedules(customerId, token) {
  // 1. Pull all RecurringGifts from Virtuous that came from your platform
  const virtuousSchedules = await queryAllPages({
    groups: [
      // Filter by transactionSource if available; otherwise filter client-side
    ],
    sortBy: 'id',
  }, '/api/RecurringGift/Query', token);

  const fromOurPlatform = virtuousSchedules.filter(
    (s) => s.transactionSource === 'YourPlatform'
  );

  // 2. Pull all active recurring schedules from your platform
  const platformSchedules = await db.recurring_schedules.find({
    customer_id: customerId,
    status: 'active',
  });

  // 3. Cross-match by transactionId and report discrepancies
  for (const platformSchedule of platformSchedules) {
    const virtuousMatch = fromOurPlatform.find(
      (v) => v.transactionId === platformSchedule.id
    );

    if (!virtuousMatch) {
      console.warn(`Platform schedule ${platformSchedule.id} missing from Virtuous`);
    } else if (Math.abs(virtuousMatch.amount - platformSchedule.amount) > 0.01) {
      console.warn(
        `Amount mismatch on schedule ${platformSchedule.id}: ` +
        `platform $${platformSchedule.amount}, Virtuous $${virtuousMatch.amount}`
      );
    }
  }
}
Run reconciliation monthly. Mismatches typically surface a donor who changed their amount through your platform but the change didn’t propagate to Virtuous — re-push the update to resolve.

End-to-end checklist

Before deploying a recurring-donation integration to production, confirm:
  • RecurringGifts are created with stable transactionSource + transactionId (the schedule’s ID, not a payment’s ID).
  • The donor’s Contact is resolved before RecurringGift creation.
  • Each recurring payment Gift Transaction includes recurringGiftTransactionId linking back to the schedule.
  • Schedule updates use GET-then-PUT with the full record.
  • Cancellations use PUT /api/RecurringGift/Cancel/{id}, not status updates via standard PUT.
  • Failed payments are not submitted as Gifts.
  • Monthly reconciliation runs and surfaces amount/status drift between your platform and Virtuous.
  • If your platform supports pause/resume, the chosen Virtuous-side modeling (cancel/recreate vs. suppress-payments) is documented and agreed with the customer.

Where to go next

Stripe to Virtuous CRM

The Stripe-specific recipe that sources recurring schedules from Stripe Subscriptions.

Import Historical Gifts

Backfill historical recurring payment data — each payment as a Gift, linked to the recreated schedule.

Statuses and Lifecycle States

The RecurringGift status field and its lifecycle states.

Reconcile Failed Syncs

Handle drift in recurring schedule state between your platform and Virtuous.
Last modified on May 27, 2026