Skip to content

Core Concepts

How Pledgivo works#

Pledgivo lives entirely inside your Salesforce org. A donor's card goes straight to Stripe, a background job asks Stripe what happened, and the result lands as ordinary Opportunities and Contacts. This page draws that whole system on one map, then walks through it.

The interactive map needs JavaScript. Every box and arrow on it is described in the sections below.

How to read it. The four bands show who the code runs as. Solid black boxes are people; dashed boxes are scheduled jobs; boxes with a rule across the top hold data; the pill is a platform event; dotted boxes sit outside your org. Pick a journey to trace it step by step, or pick any box to see what it does, who it runs as and which classes implement it. Use the corner button to open the map full screen, and Esc to close it.

The short version#

Question Answer
Where does the card go? Straight from the donor's browser to Stripe, through Stripe.js in a Visualforce iframe. Salesforce sees only the payment's Id.
When does the gift appear in Salesforce? When the Donation Finalizer next runs: within 5 minutes by default. The donor's card is charged and they are thanked immediately.
Does Stripe call into my org? No. There are no webhooks and no public endpoint for Stripe. Salesforce asks Stripe, on a schedule.
What does a gift become? A Closed Won Opportunity, plus a Contact or a Person Account (whichever your org uses). Both are ordinary records your reports already read.
What happens if something downstream fails? NPSP sync, campaign members, matching and soft credits all run after the gift is saved, in a separate transaction. They can fail without losing the gift.
Who needs a licence seat? Staff who use the consoles. Donors and event attendees never do.

Who the code runs as#

The map's four bands are four security contexts. Almost every design choice in the package follows from which band a piece of code lives in.

Band Runs as What lives there Why it matters
The donor's browser Nobody in Salesforce The public site's LWCs, the Stripe card frame, the embed widget on your own website Nothing here is trusted. Amounts and choices are checked again in Apex.
Guest Apex The site's guest user Guest controllers, DonationService starting a payment, the embed config endpoint Guest users can't hold a seat, can't edit records, and get generic error messages. Records are reached by unguessable tokens, never by a record Id sent from the browser.
Background The user who scheduled the job, or Automated Process for the platform event The Donation Finalizer, the watchdogs, renewals, refund and dispute checks, post-gift work Only these paths create gifts and donors. They run in system context because a guest user structurally can't write them.
Staff The signed-in user, with their own sharing and field access The Lightning consoles, record pages, actions and their controllers Gated by a licence seat; writes check the user's own create, edit and field permissions.

1 · A donor gives#

  1. The donor opens a campaign's /donate page. It is an LWC from the package, hosted on your Experience Cloud LWR site.
  2. The form calls guest Apex. Every guest controller extends GuestAction, so an unexpected failure reaches the browser as one generic sentence; the real error goes to the log.
  3. DonationService checks the checkout limits before anything else. The defaults are 5 attempts per email per campaign in 10 minutes, and 500 per campaign in an hour. It counts existing staging rows, so a refused attempt writes nothing and never calls Stripe.
  4. It opens a Stripe PaymentIntent through the donor key's Named Credential. A one-time gift or ticket has no Stripe Customer. A recurring signup creates one and marks the payment for reuse, so the card can be charged again later.
  5. It writes a Donation_Staging__c row with status Pending. That row is the only thing in Salesforce at this point: no Contact and no Opportunity yet.
  6. The form passes the payment to the card frame, a Visualforce page in an iframe. Stripe.js collects the card there and confirms the payment with Stripe directly.
  7. When the browser sees the confirmation, it marks the row Submitted, then shows the thank-you page. The thank-you page only reports Stripe's status; it creates nothing.

Checkout stays closed until the Finalizer has run

The form refuses to start a payment until the Donation Finalizer has recorded its first heartbeat. That stops a new org from taking money that no job would ever record.

2 · Confirming without webhooks#

The Donation Finalizer (StagingReconciliationScheduler → StagingReconciliationBatch) runs every 5 minutes by default. You can slow it to once an hour in Settings → System → Scheduled Jobs. Each run picks up Pending and Submitted staging rows, five per batch, and asks Stripe for each PaymentIntent's current status.

Stripe says The staging row becomes What gets created
succeeded Completed The donor (found or created), a Closed Won Opportunity, and the receipt email, all in one transaction
canceled Failed Nothing. A ticket hold releases its seats.
Still pending after the row expires (72 h for a gift, 30 min for a ticket hold) Expired Nothing. A ticket hold releases its seats.
Still pending, not yet expired Unchanged Nothing yet. The next run checks again.

Every outcome writes a Transaction_Log__c row, and the Donation Monitor tab lists rows that are stuck. Its Retry now button runs the same logic for one row. Finalizing is keyed on the Stripe PaymentIntent Id, so the batch, the retry button and the hourly sweep can all reach the same payment without ever recording it twice.

A recurring signup has one extra wait. The Finalizer holds gift #1 for up to 10 minutes while the browser saves the card for reuse. In the same transaction as that first Opportunity, it creates the Recurring_Donation__c schedule as Active. If the card details never arrive, gift #1 is still recorded, as a one-time gift.

3 · After the gift#

Once a gift has been saved, DonationService publishes DonationFinalized__e. It is a high-volume platform event delivered only after the transaction commits. Its trigger runs as Automated Process, in a transaction of its own, and does four things in order:

  1. NPSP allocation sync, if NPSP is installed.
  2. Company-match follow-ups.
  3. CampaignMember sync, if you've switched it on.
  4. Soft credits.

Each step catches and logs its own failure, so the others still run. Because all of this happens after the gift has been saved, none of it can lose a gift. Recurring renewals publish the same event. Ticket orders don't: they finalize through EventPurchaseService, which handles its own campaign-member sync.

4 · Staff work#

Staff work in five Lightning tabs: Donation Monitor, Fundraisers, Donors, Diagnostic Logs and Settings. They also get record pages for campaigns, gifts, donors and recurring schedules, and actions such as Refund, Resend receipt, Manage recurring gift and Send portal link.

  • Licence check first. Every tab asks LicenseService whether the user holds a seat before it shows anything. This check fails open: only a definite "no seat" answer blocks a user, and any platform error lets them in. Public pages never check for a seat.
  • The user's own access. Controllers read through FFLib selectors and write through a Unit of Work, both enforcing the signed-in user's sharing, object and field permissions.
  • Two actions need an extra permission. Refunds need the Process_Refunds custom permission, and pausing, changing or stopping a recurring gift needs Manage_Recurring_Gifts.

5 · Money that moves later#

Each Stripe account (Payment_Account__c) is reached through two Named Credentials that you create; the package ships neither. The donor key covers checkout: creating customers and payments, and reading payment status. The back-office key covers everything that moves money later, or that lists your Stripe account's history.

What When Key Result in Salesforce
Refund When a staff user clicks Refund (within 180 days by default) Back-office The gift becomes Refunded or Partially_Refunded. A full refund of a ticket order releases its seats.
Recurring renewal Nightly, starting 02:00 Back-office A new installment Opportunity. A decline is retried every 3 days, up to 3 times.
Auto-cancel Right after renewals — A Failed schedule is cancelled after a 7-day grace period.
Refund & dispute check Hourly, at :23 Back-office Picks up refunds and disputes made in the Stripe dashboard and alerts staff to a new dispute. It never issues a refund and never lowers a recorded refund.
Payment sweep Hourly, at :07 Back-office Lists the last 24 hours of payments and repairs any the Finalizer missed, up to 5 per run.

Cancelling a recurring gift changes the schedule in Salesforce only; there is no Stripe Subscription to cancel, because Pledgivo never creates one. Each renewal is an ordinary off-session charge that the package schedules itself.

Other paths on the same rails#

Event tickets. publicEventRegistration sits inside the same /donate page. A ticket order reserves seats by locking the ticket tiers, opens a PaymentIntent with no Stripe Customer, and writes a staging row that holds the seats for 30 minutes. One order can include up to 50 tickets. The same Finalizer and sweep pick the row up, and EventPurchaseService creates one Opportunity with a line item per tier and an Event_Attendee__c per seat. A failed or expired order gives its seats back. See Events & ticketing.

The donor portal. At /donor, a donor asks for a sign-in code. The 8-character code is emailed to them and is valid for 15 minutes; only a hash of it is stored. After 5 wrong attempts the code is locked. Each address can request one code every 5 minutes, and the whole org is capped at 100 codes an hour. A session lasts up to 4 hours and ends after 30 idle minutes. From the portal the donor can see their giving, download statements, pause, change or cancel a recurring gift, update a card, name event attendees and ask for a refund. See The donor portal.

The embed widget. A script on your own website fetches a campaign's public settings from EmbedConfigResource. That is the package's only REST endpoint; it answers only GET, and it sends CORS headers only to origins on your allowlist. When the donor clicks, the whole page moves to your /donate page with the amount filled in, so no payment ever happens on your website. See Embed a donate widget.

Receipts. When a gift is first finalized, the package emails a receipt from its DonationReceipt template, unless you've turned automatic receipts off. Every Opportunity carries a random 128-bit Receipt_Token__c, and the public /receipt page finds the gift by that token alone. Ticket confirmations, recurring confirmations, dunning, refund notices and tribute emails each have their own template.

Scheduled jobs#

The package schedules all of these at install. They run in system context as the user who scheduled them; saving the scheduled-jobs settings reschedules them under the admin who saved.

Job When What it does
Donation Finalizer Every 5 min (configurable) Confirms staging rows with Stripe and creates the records
Reconciliation heartbeat Every 15 min Emails admins if staging rows are stuck
Stripe payment sweep Hourly, :07 Repairs payments the Finalizer missed
Refund & dispute check Hourly, :23 Syncs refunds and disputes made in the Stripe dashboard
Guest request sweep :09, :29, :49 Processes queued guest requests
Donor rollups → renewals → auto-cancel Nightly, 02:00 Donor totals, then recurring charges, then cancels lapsed schedules
Campaign rollups Nightly, 02:37 Campaign totals and progress bars
Retention purge Nightly, 03:00 Deletes logs, finished staging rows, processed guest requests and old portal sessions after 90 days (portal sessions after 7)

What is not in the picture#

Some things are left off the map on purpose; each absence is a design choice.

  • No inbound webhooks. Stripe never calls your org, so there is no public endpoint to secure and no signing secret to rotate.
  • No Stripe Subscriptions. Recurring gifts are schedules in Salesforce, charged by the package itself.
  • No CAPTCHA. Abuse protection is the checkout limits shown above, plus Stripe Radar on Stripe's side.
  • No stored secrets. Stripe keys live only in Named Credentials you create. A field may hold a credential's name, never a key.
  • No packaged site. The Experience Cloud site and its routes are built during setup. The package supplies the components that go on its pages.

Where to go next#