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

# Sync Recurring Donor Updates

> Coordinate a donor's recurring giving schedule between your platform and Virtuous — creation, amount changes, pauses, cancellations, and the payment events that link back to the schedule.

A recurring donor is a donor who has committed to giving on a schedule — $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

```mermaid theme={null}
graph TD
  Schedule["RecurringGift<br/>(the commitment)"]
  Pay1["Gift #1<br/>(Jan payment)"]
  Pay2["Gift #2<br/>(Feb payment)"]
  Pay3["Gift #3<br/>(Mar payment)"]

  Schedule -.->|recurringGiftTransactionId| Pay1
  Schedule -.->|recurringGiftTransactionId| Pay2
  Schedule -.->|recurringGiftTransactionId| Pay3
```

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:

| Endpoint                                           | Use                                                                                                        |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `POST /api/RecurringGift`                          | Create 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/Query`                    | Search schedules by structured filters.                                                                    |
| `GET /api/RecurringGift/QueryOptions`              | Retrieve 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.

<CodeGroup>
  ```bash cURL theme={null}
  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
        }
      ]
    }'
  ```

  ```javascript JavaScript theme={null}
  async function createRecurringGift(commitment, virtuousContactId, token) {
    const response = await fetch(
      'https://api.virtuoussoftware.com/api/RecurringGift',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          transactionSource: 'YourPlatform',
          transactionId: commitment.platformId,
          contactId: virtuousContactId,
          startDate: commitment.startDate,
          amount: commitment.amount,
          frequency: commitment.frequency,
          automatedPayments: true,    // Payments come automatically — not entered manually
          trackPayments: true,         // Virtuous tracks the running total
          designations: commitment.designations.map((d) => ({
            projectCode: d.projectCode,
            amountDesignated: d.amount,
          })),
        }),
      }
    );

    if (!response.ok) {
      throw new Error(`RecurringGift creation failed: ${response.status}`);
    }
    return await response.json();
  }
  ```
</CodeGroup>

The response contains the new schedule's `id` — store it on your side alongside your platform's commitment record.

### Required and recommended fields

| Field                                 | Required             | Purpose                                                                                                                                             |
| ------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contactId`                           | Yes                  | The donor's Virtuous Contact ID. Resolve via [Create a Contact](/crm/workflows/create-a-contact) before creating the RecurringGift.                 |
| `transactionSource` + `transactionId` | Strongly recommended | Idempotency key. As with Gifts, use a stable identifier from your platform — typically the recurring schedule's own ID, not the first payment's ID. |
| `startDate`                           | Yes                  | When the schedule begins. Use the date of the donor's first scheduled payment.                                                                      |
| `amount`                              | Yes                  | Per-payment amount.                                                                                                                                 |
| `frequency`                           | Yes                  | The cadence — typically `Monthly`, `Quarterly`, `Annually`.                                                                                         |
| `designations[]`                      | Yes                  | At least one designation. Designation amounts must sum to `amount`.                                                                                 |

### Frequency values

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

### 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 JavaScript theme={null}
// 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 JavaScript theme={null}
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](/crm/workflows/update-a-contact)):

```javascript JavaScript theme={null}
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](/crm/workflows/update-a-contact#the-put-as-patch-ambiguity) applies — send the complete record on every update.

### Common update scenarios

| Donor action                   | Fields to change                                                                                                                         |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Increase or decrease amount    | `amount` and proportional `designations[].amountDesignated`.                                                                             |
| Change designation             | Replace the `designations[]` array. Confirm the new amounts still sum to `amount`.                                                       |
| Change frequency               | `frequency`. Some platforms also require an updated `nextExpectedPaymentDate`.                                                           |
| Donor's payment method updated | No 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:

```bash cURL theme={null}
curl -X PUT https://api.virtuoussoftware.com/api/RecurringGift/Cancel/12345 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

```javascript JavaScript theme={null}
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.

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

***

## 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 outcome             | Virtuous action                                                                                                                         |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Payment succeeds on retry    | Submit the successful payment as a Gift Transaction with `recurringGiftTransactionId`. Schedule continues.                              |
| Payment fails permanently    | No 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 method | The 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 JavaScript theme={null}
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

<CardGroup cols={2}>
  <Card title="Stripe to Virtuous CRM" icon="layer-group" href="/crm/recipes/stripe-to-virtuous">
    The Stripe-specific recipe that sources recurring schedules from Stripe Subscriptions.
  </Card>

  <Card title="Import Historical Gifts" icon="boxes-stacked" href="/crm/recipes/import-historical-gifts">
    Backfill historical recurring payment data — each payment as a Gift, linked to the recreated schedule.
  </Card>

  <Card title="Statuses and Lifecycle States" icon="circle-half-stroke" href="/crm/concepts/statuses-and-lifecycle-states">
    The RecurringGift status field and its lifecycle states.
  </Card>

  <Card title="Reconcile Failed Syncs" icon="arrows-rotate" href="/crm/workflows/reconcile-failed-syncs">
    Handle drift in recurring schedule state between your platform and Virtuous.
  </Card>
</CardGroup>
