Skip to content

Core Concepts

Recurring giving#

A recurring gift is not a Stripe Subscription. Salesforce is the system of record for the billing schedule, and a scheduled job drives each renewal charge directly.

Why self-managed billing#

Stripe's own Subscriptions product would work, but it means two systems can disagree about when a donor is billed, what happens on a failed charge, or when a subscription should lapse — and reconciling that disagreement means either polling Stripe constantly or standing up a webhook. Pledgivo instead treats Recurring_Donation__c as the single source of truth for the billing schedule, and charges the donor's saved card off-session (no browser involved) on that schedule.

How the donor picks a frequency#

The donation form's frequency block is a segmented control — One-time, Monthly, Annually — with a sliding marker showing the current choice. Directly beneath it, a line restates the choice in the donor's own numbers: pick $40 and switch to Monthly and it reads $40 every month — $480 over a year. Cancel anytime.

That annualised figure is the point of the block. The commitment a donor is actually weighing on a recurring gift is the yearly one, and asking them to work it out in their head is the moment most of them fall back to a single gift. Monthly also carries a Most popular badge; recurring donors are worth several times a one-time donor over their lifetime, so that option gets a visible reason rather than sitting as one of three equal-weight tabs.

Nothing here is configurable per campaign beyond the block's title, help text, and the three option labels (Settings → Experience Cloud → Designs). The readout wording and the badge are fixed.

Saved payment methods#

When a donor sets up a recurring gift, Stripe tokenizes their card into a reusable payment method, which Salesforce stores a reference to on Payment_Method__c — linked to the Recurring_Donation__c and to the donor (Contact__c or Donor_Account__c, per the Person Account rule). The raw card number is never stored anywhere in Salesforce; only Stripe's tokenized reference is.

Payment_Method__c exists only for this purpose — see the note in Donations & payments if you're looking for a one-time gift's payment details, which live directly on the Opportunity instead.

What breaks if a card stops working#

An expiring card is only interesting because of what it is paying for. The saved card's record page answers that directly: the live recurring schedules charged on it, and what they commit to over a year. That is the number to quote when you ask a donor to update their details — "your $40 monthly gift" lands differently from "your card is expiring".

One thing on that card is worth understanding. A schedule can be charged on a saved card in two ways: through the card record it points at, or through the raw Stripe token stored on the schedule itself. The renewal batch charges the token either way, so both are genuinely billed to this card — but only the first says so anywhere on the schedule record. The card flags the second kind rather than leaving it invisible, because a schedule you can't see from here is exactly the one that breaks without warning.

A saved card belongs to the account that took it#

A tokenized card is not portable between Stripe accounts. The token your donor's card became is meaningful only to the Stripe account that created it — hand it to a different account and the charge is rejected as an unknown payment method, no matter that the card itself is perfectly good.

That matters because a campaign's Payment Account is a field an admin can change, and changing it is a legitimate thing to do (a new entity, a rebranded account, a merged program). What must not happen is a monthly donor's next charge quietly following the campaign to an account that never met their card.

So each saved card records the account that issued it, and every renewal resolves the account to charge in this order:

Order Source Used when
1 The saved card's own Payment Account Always, if the schedule has a saved card — this is the only account that can actually charge the token
2 The schedule's Payment Account The snapshot taken when the donor signed up, used when the schedule has no saved card
3 The campaign's Payment Account Last resort — an older schedule from before either field existed

The campaign is where the account is chosen for new gifts, and it is the last thing consulted for an existing one. Repointing a campaign at a different Stripe account changes where its future donations land; it does not migrate the recurring gifts already running against the old account.

Existing schedules are filled in on upgrade

Installing the version that added these fields backfills them for schedules and cards that predate it, from the campaign each one was signed up against. Nothing needs to be done by hand.

The renewal cycle#

flowchart LR
    A[RecurringDonationScheduler<br/>daily, 2 AM] --> B[RecurringRenewalBatch]
    B --> C{Due for renewal?}
    C -->|Yes| D[RecurringDonationService.processRenewal]
    D --> E[StripeGateway.chargeOffSession]
    E -->|Success| F[New Opportunity, Closed Won]
    E -->|Fails| G[Retry counter increments]
    G --> H{Retries exhausted<br/>+ grace period elapsed?}
    H -->|Yes| I[RecurringAutoCancelBatch<br/>cancels the recurring gift]
    H -->|No| B

A RecurringDonationScheduler job runs nightly and hands due renewals to RecurringRenewalBatch, which calls RecurringDonationService.processRenewal() for each — this charges the saved payment method off-session through the same IPaymentGateway abstraction used for one-time gifts, and creates a new Opportunity for the successful charge.

If the donor chose to cover processing fees when they signed up, that choice rides on the schedule rather than being baked into its amount — the schedule keeps tracking the gift the donor picked, and each renewal re-grosses it against the current fee rate. Change your processor's rate in Settings and every covered donor's next charge follows automatically. See Donations & payments.

Gifts that end on their own#

By default a recurring gift runs until someone stops it. Two optional fields on Recurring_Donation__c let it finish by itself instead — useful for a pledge the donor framed as a promise ("$50 a month for a year") rather than an open commitment:

Field Meaning When it's checked
Installment Cap End after this many successful charges After each charge — the count is only consumed by money that actually moved
End Date Never charge after this date Before each charge — the schedule stops being due, inclusive of the end date itself

A schedule that finishes its term lands on the status Completed, not Cancelled. That distinction is the point: a fulfilled pledge and a donor who walked away are very different things to a fundraiser, and lumping them together makes retention reporting lie. Installments Paid counts every successful charge including the first gift taken at signup, so a 12-payment pledge completes on its twelfth charge.

Because a skipped cycle, a failed charge, and a retry never increment the count, a donor who skips a month still gets all twelve payments they promised — the term just finishes a month later.

When a charge fails: dunning#

A failed renewal doesn't cancel the recurring gift immediately. It's retried on a schedule, with the donor notified by email at a configurable cadence, until either the charge succeeds or the org's configured retry/grace-period limits are exhausted — at which point RecurringAutoCancelBatch cancels the recurring gift and (optionally) notifies the donor.

All of this is configurable from Settings → Payments → Recurring:

Setting Default Controls
Failed Payment Retries 3 How many times a failed renewal is retried before giving up
Retry Interval (days) 3 Flat number of days between retry attempts
Grace Period (days) 7 How long a recurring gift stays active after retries are exhausted, before auto-cancel
Dunning Email Frequency 3 How often (in days) the donor is emailed about a failed payment

See Configure recurring dunning for a walkthrough of tuning these.

When Stripe hasn't settled a renewal yet#

Most renewals come back from Stripe as paid or declined within the same call. A few do not: the call times out, or Stripe reports the payment as still processing. The renewal job never answers that by charging again, because the first charge may yet succeed and the donor would pay twice. Instead it keeps the Stripe payment's reference on the gift (Pending Payment Intent) and, on the next run, looks that payment up rather than creating a new one:

  • Paid — recorded exactly as an ordinary renewal.
  • Declined — enters dunning like any other failed charge.
  • Needs the donor to authenticate — follows the usual re-authentication path.
  • Still processing — left alone and looked up again the next day.

A look-up that fails for any other reason (a timeout, a Stripe outage, a revoked or mis-scoped key, a missing Payment Account) says nothing about the payment itself. The job keeps the reference, writes the reason to Last Error ("Could not check pending renewal payment … since date") and looks again the next day. Fix the cause and the next run picks up where it left off. The date is the day the look-ups started failing; a successful look-up resets it.

The gift is parked for a person to check in four cases:

  • Stripe still has no answer three days after it created the payment.
  • A look-up returns the payment with no creation time, so its age cannot be measured.
  • Stripe answers that it has no record of the payment (for example, the payment account's keys were swapped for a different Stripe account). A "not found" page that did not come from Stripe itself — a wrong Named Credential URL, a proxy error — does not count; it is looked up again like any other failed look-up.
  • The look-ups themselves have been failing for three days. Fix the cause (the Stripe check in Settings shows whether the payment account can reach Stripe), then use Retry now to look the payment up again.

When a gift is parked, Last Error starts with "Needs staff review:" and no further retry is scheduled. Manage Recurring Gift shows that message and enables Retry now; Skip next payment stays off while a payment is still in doubt; pausing the gift keeps the message, because the payment is still in doubt. The reconciliation heartbeat's alert e-mail ("N payment(s) need attention") counts it. If the donor changes their card in the portal, the gift is queued for the next run, which looks the payment up again before any new charge; it is parked again if the payment is still unsettled, and recorded if it has settled. Look the payment up in the Stripe Dashboard. Once Stripe shows it as paid or failed, use Retry now on Manage Recurring Gift. Retry now does not charge anything itself. It queues the gift for the next run, which reads the pending payment and then records it or starts dunning, exactly as above. Do not record a paid one by hand — the next run records it too, and the gift would be counted twice.

For the "no record of the payment" case, check the Stripe Dashboard of every payment account first. If none has the payment, Retry now on Manage Recurring Gift clears the stored reference, and the next run charges the renewal afresh. Only the staff Retry now does this. The donor portal's retry is refused on any parked gift, because a donor cannot tell whether the payment exists under another account, where a fresh charge would bill them twice.

If every gift on one payment account parks as "no record of the payment", suspect the payment account's setup before the payments: the renewal job charges with the admin key's Named Credential but looks payments up with the donor key's. Check that both credentials point at the same Stripe account.

After Retry now on a gift parked because look-ups kept failing, the three-day count starts again from that day.

A gift that is cancelled or ends while a renewal is still waiting on Stripe keeps its reference, and the heartbeat keeps counting it: that payment can still settle in Stripe, and no run will record it now the gift is no longer active. Check it in the Stripe Dashboard and refund or record it as appropriate; then an administrator clears Pending Payment Intent on the gift.

Donor self-service#

A donor can manage their recurring gift from the donor portal — a token-authorized page, no login required. From there they can:

  • Change the amount — the new amount takes effect on the next charge; the billing day never moves, so raising a gift doesn't quietly pull the next payment forward. If the schedule splits across several funds by percentage, those splits are re-derived at the new amount.
  • Change the fund — only funds the campaign actually offers are accepted. If the campaign wouldn't allow the requested fund, the change is refused rather than silently redirected to the campaign's default; a donor who asked for the scholarship fund should not end up funding something else without being told.
  • Skip the next payment — moves the next payment date on by one cycle without consuming an installment of a pledge term. Unavailable mid-dunning, where the schedule has a failed charge to resolve first, and while a renewal is still waiting on Stripe (above) — skipping would move the date past a payment that may already have been taken.
  • Switch or add a saved card, or cancel the gift entirely. A new card is confirmed with Stripe before the gift moves onto it: the portal hands back only the Stripe setup reference, and the card is read from Stripe's own record of that setup, so a card belonging to someone else's Stripe customer cannot be attached.

Every one of these writes a Recurring_Donation_History__c row. The schedule's own fields keep no history of their prior values, so that trail is the only record of what a donor changed and when — worth pointing a fundraiser at when they ask why a monthly gift is a different size than they remember.

Each action re-checks that the token actually owns the schedule it names, so a guessed record Id gets a refusal rather than someone else's gift.

Doing it for them: Manage Recurring Gift#

Donors phone. Manage Recurring Gift, on the recurring gift's record page, gives a fundraiser the same set of changes the donor has in the portal, without asking them to go find their email.

The panel opens on the gift's current state — status, amount, frequency, next payment date, any failed charges — and offers only the actions that are actually available on a schedule in that state. A paused gift offers Resume, not Pause. A healthy gift offers no Retry Now, because there is nothing that failed to retry; on a gift mid-dunning it charges immediately instead of waiting for the next scheduled attempt.

Action What it does
Pause / Resume Stops charging without ending the gift; resuming picks the existing schedule back up
Skip next payment Moves the next payment on by one cycle, without consuming a pledge installment
Retry now Charges a failed or mid-dunning gift immediately rather than waiting for the next retry
Change amount Takes effect on the next charge; percentage fund splits are re-derived at the new amount
Change fund Offers exactly the funds the campaign offers the donor — the same resolved list, not a wider staff-only one
Reinstate Restarts a cancelled or failed gift: billing resumes today and the dunning counters are cleared
Switch card Moves the gift onto another card the donor has already saved

Reinstate after the donor has changed their card

Reinstating on the same day as a decline, with the same card still on the gift, spends one renewal cycle for nothing. Stripe recognises the attempt as a repeat of that day's declined one and returns the same decline. The schedule is not stuck, and the next retry is a fresh attempt. Reinstate once the donor has put a working card on file (or use Switch card first). A different card always makes a new attempt.

Cancelling is deliberately not here — it's the separate Stop Recurring Donation action, because it's terminal and needs a reason, and an irreversible button sitting one click from the reversible ones is a mistake waiting to happen.

Everything a fundraiser does through this panel writes the same history row a donor's own change would, so the audit trail doesn't distinguish based on who was more convenient to ask.

There is no way to key in a donor's card

The card picker lists only cards already on file, and there is no "add a card" path for staff by design. Tokenizing a card needs the donor's own browser and their own mandate — a fundraiser typing card details into Salesforce would breach both PCI and the donor's consent. When a donor needs to put a new card on file, send them a portal sign-in code (see Donors & accounts) and let them add it themselves. A donor with only one card on file simply sees no switch option, which is the honest answer: there is nothing to switch to.

Who can use it#

The panel is gated by the Manage Recurring Gifts custom permission, granted by Fundraising Admin. Without it the panel still opens — it just renders read-only, so anyone who can see the gift can see its state and answer a donor's question, and only someone trusted with the permission can change it. Add the permission to another permission set if you want a wider group of fundraisers making these changes.

The same permission gates Stop Recurring Donation. Cancelling is the one change that cannot be undone, so it is held to at least the bar the reversible changes are; a user without the permission who runs the action gets "You are not authorized to cancel recurring gifts." (Before 2026-09-25 the stop action checked only that the user could run it, which put the terminal change behind a lower bar than a pause.)

Every action re-checks the permission and the schedule's own rules on the server. A panel left open on a stale screen can't be used to push through a change the schedule no longer allows.

  • The donor portal


    Where donors change their own card, amount or schedule without emailing you.

    Read more →

  • Monitor & diagnose


    Watch renewals land, and read what the app logged when one doesn't.

    Read more →

  • Configure dunning


    A step-by-step guide to the retry/grace-period settings.

    Guide →