Getting Started
Connecting Stripe#
Stripe is the only payment gateway shipped today, reached through a gateway abstraction so a second processor could be added later without touching the donation pipeline.
Watch it first#
What it is. Seven minutes connecting a real Stripe account, in the order Setup forces on you: two external credentials, the principal that holds the secret key, the Authorization header, the named credentials, the two permission grants, and the Payment Account record that ties them together. It ends on the two live checks — one of which fails. That failure is left in deliberately; the chapter explains what it caught and why no setting in Salesforce can fix it. No key is readable in any frame.
How the pieces fit together#
Pledgivo separates where credentials live from which campaign uses them:
| Layer | Where it lives | Holds |
|---|---|---|
| Secrets | An External Credential + Named Credential you build in Setup — nothing ships with the package, and the package creates nothing | Your two Stripe restricted keys. Never stored in a custom field, never readable back, not even by the admin who entered them. |
| Per-account config | Payment_Account__c |
One record per Stripe account you connect — display label, processor, publishable key, live/test mode, active status, and the names of the two credentials it uses — a charge-only one for donor-facing calls and a back-office one for refunds and scheduled jobs. |
| Gateway registry | PaymentGatewayRegistry (Apex) |
The catalog of processors this package implements, and for each one the class that talks to it and the API version it's written against. It's code, not configuration — there's nothing here to set up. |
| Routing | Campaign.Payment_Account__c |
A lookup on the Campaign itself — which account that campaign's donations settle to. One account per campaign. |
A single org can connect more than one Stripe account (for example, separate accounts per fiscal sponsee) and route different campaigns to different accounts.
Connect an account#
Your secret keys go into Salesforce credentials you build in Setup — never into a field on a record, and never through this app. Settings → Payments is the guide for that build: it lists the ten things that have to be true, gives you the exact values to paste, and then reads your org back to tell you which ones are done and which one is wrong.
Never worked with Stripe's API before? Read The two Stripe keys, and what each one may do first — it explains what you're being asked to create and why there are two of them. The same explanation sits at the bottom of the Payments panel itself, so you don't have to keep this page open beside it.
-
Settings → Payments. On an org that has no payment account yet, the guide is already there — it grades your org itself, and step 7 tells you the account is still missing. Start at step 1; you do not need a payment account to begin.
Steps 1–4 read Not checked yet until you finish step 7, and that is not a fault. Every check in that stage looks a credential up by name, and the name lives on the payment account — which cannot be created until the credentials it names exist. So the order is genuinely circular for exactly one lap: build the pair, create the account, then press Re-check and all four grade themselves at once. They are shown in neutral grey rather than red for that reason: the panel has no verdict on them yet, not a bad one.
Above the ten steps is a short Before you start map. Read it before you open Setup: the steps cross three different places, and the map says which. Collect your three Stripe keys first (it links straight to the API keys page for the mode this account is in), then do the Setup work, then come back to this panel for the two checks. Every step title also carries a tag — Salesforce Setup or This page — so you can see at a glance where the next one happens.
Each Setup step draws the screen you are about to open. From step 1 to step 6 the panel shows a replica of the actual Salesforce dialog — the same fields in the same order, in the state Salesforce presents them before you touch anything — with a coloured note beside each field saying what to type, which name is yours to pick, which default is wrong, and which field to leave alone. Read the note beside the field, not a list underneath the screen. The values in the replica are the ones your org needs, so it doubles as the thing you copy from. 2. Work down the guide. Steps 1–4 build the credential pair in Setup → Named Credentials: an External Credential with a Named Principal carrying your restricted key as an encrypted parameter, an
Authorizationheader that references that parameter, and a Named Credential pointing athttps://api.stripe.comwith the right callout options ticked. Each step has a Take me there link and, where the check can be made, a status read from your org rather than a tick you give yourself. Do this twice — once for each of the two credentials described under step 7 below.The names are yours to choose. The panel suggests one for each credential (
Stripe_Auth_Donor_RestrictedandStripe_Auth_Admin_Restrictedfor the External Credentials,Stripe_Donor_RestrictedandStripe_Admin_Restrictedfor the Named Credentials), but nothing in the app matches on them — it reads whatever names you saved on the Payment Account record. Call them after the Stripe account they authenticate (Stripe_Auth_Chapter_West,Stripe_Chapter_West) if that is how you will recognise them later. The only hard rules are that two credentials cannot share a name, and that what you type on the Payment Account record must match the Named Credential names exactly. Once you have built them, the panel stops suggesting and prints the names your org actually uses.This matters most if you run more than one Stripe account. Each Payment Account record points at its own pair of credentials, so a second Stripe account needs a second pair with different names — reusing the suggested ones is impossible for the second account. Naming them for the fund, chapter or region they collect for is what makes the Payment Account picker readable when you map a campaign to an account. See Which names are yours, and which must match.
Step 2 asks for a different key on each credential and says which is which: a charge-only restricted
rk_…key on the donor credential, and a second restricted key scoped to refunds and reads on the admin one. Both must be restricted keys. A fullsk_…secret key is not an accepted value on either: the guide probes both credentials and keeps the account off Ready to take payments for as long as one of them answers a call it should have refused. This is also the one place where getting the two the wrong way round is silent — both keys authenticate perfectly well, so an admin key on the donor credential works, and hands the public internet a refund button. Steps 8 and 9 are what catch both mistakes. 3. Allowed Namespaces for Callouts is the last field of step 4, and the one whose absence looks like nothing at all. Because this app's Apex ships inside a managed package and the credential is one you built, Salesforce blocks the call unless the package's namespace is listed on the credential. The field is on the New Named Credential dialog itself, in the Managed Package Access section at the bottom — so the whole of step 4 is one pass down one screen, saved once, and there is no need to save the credential and reopen it. Add the namespace the guide prints for you; after saving it appears under Managed Package Access on the credential. (If you did already save, nothing is lost — open the Named Credential → Edit and the field is in the same place.) Until it is there, no Stripe call this app makes will succeed — not a donation, not a refund — however correct everything else is. There are screenshots of the field and of the confirmation it leaves behind in After you install. 4. Step 5 creates two permission sets of your own, because two audiences hold them. The site set carries the External Credential Principal Access grant for the donor credential and is assigned to your site guest user (step 6) and to staff who take payments; its License must stay--None--, or it cannot be assigned to a guest user. The admin set carries the grant for the admin credential and is assigned to staff who issue refunds and to every user who owns a scheduled job — a scheduled job authenticates as its own owner, so a reconciler scheduled by an admin who never held the set fails quietly. The admin set is never assigned to the guest user. Names are yours to choose (the panel suggestsFundraising_Site_Guest_AccessandFundraising_Payments_Admin); the panel finds each set by the grant on it and reports which is missing.Both sets also need Read and Create on the standard User External Credential object (Object Settings → User External Credentials). A callout needs both grants: the principal, and that object, which is where Salesforce writes the row that mints the credential. Miss the second and every charge fails with "You don't have read permissions on the User External Credential object." — a message the donor never sees and that surfaces only in Diagnostic Logs. This grant cannot ship inside the package: Salesforce strips standard-object permissions out of an installed permission set. 5. Step 6 assigns the site set (the one carrying the donor credential) to your Experience Cloud site guest user — do this only if you take donations from public pages. It also names every permission set your guest user holds and asks you to confirm none of them grants the admin credential; see A guest user must never hold the admin credential below.
This step has a prerequisite nothing else on the page mentions: the site itself. There is no site guest user until an Experience Cloud site exists, so on a fresh org every instruction here points at screens that aren't there yet. Build the site first — Settings → Site & Domain walks it through — then come back. The step says so itself when it can see the org has no site. 6. Step 7 is the Payment Account itself, and the form opens on the step — there is no hop to another tab. On a fresh org the step shows an Add account button; click it, give the record a display label (internal reference — donors never see it), pick the payment processor, choose your credentials, and paste your Stripe publishable key.
Once an account exists the step stops offering to create one and shows that single account instead, with Edit and Test on the row — there is no Delete anywhere, on this step or the accounts tab (see below). Showing one account is deliberate: step 7 is about the one account these ten steps are grading, and a second account is a separate decision that belongs on the Payment accounts tab, where the whole list is visible. If you run more than one account, the step names which one it is grading and the Showing switcher at the top of the panel moves the ten steps to another.
The form asks for two credentials, and both are required:
Field Used by Grant it to Donor restricted key credential Taking payments — the charge path a public donation page reaches Staff and your site guest user (step 6) Admin restricted key credential Back-office only: staff refunds, the refund/dispute reconcilers, the orphan-payment sweep, off-session recurring renewals, and Test connection Staff who issue refunds and whoever owns the scheduled jobs — never the guest user "Whoever owns the scheduled jobs" is literal, and it is the half most installs miss: a named-principal callout authenticates as the owner of the running job, not as the admin who set it up. A fresh install owns all 24 jobs with a system user that cannot be granted anything, so restart them under your own login after granting yourself the admin credential — see Background jobs. The Scheduled Jobs health row goes red and names the owner when this is wrong.
Both fields take a credential name, never a key — and the name is whatever you called the Named Credential, since nothing in the app looks one up by name. Splitting them is what keeps an anonymous visitor's principal from reaching a key that can issue refunds. You can point both fields at the same credential and the app will accept it, but one key cannot be scoped two ways: steps 8 and 9 then grade Failed rather than green, and the account stays off Ready to take payments. 7. Step 8 proves your donor key cannot issue a refund. It is on demand: press the button and the app makes a single request to create a charge, with nothing to charge. Stripe keeps one permission for creating charges and issuing refunds, so a key without Write on Charges and Refunds is refused outright with
403, and the step goes green. A key that holds Write is stopped only by the missing parameters — nothing is charged, but the credential your guest user holds carries refund rights (Write on that row, the admin key, or a secret key that should not be there at all) — and the step reports a failure rather than a pending item. Leave the row on None: nothing the donation form does needs it, and only Write on it can refund. 8. Step 9 proves your admin key is a restricted key and not your full secret key. Same shape, different question: one read-only call for something no part of this app ever asks the admin key to do. A scoped key refuses it with403and the step goes green; a full secret key answers it, and the step reports a failure that keeps the account off Ready to take payments until the key is replaced. It reads one product and changes nothing. 9. Step 10, Test connection, makes one read-only Stripe API call. It moves no money and creates nothing — it just proves the whole chain works before you route a campaign to it. It is also the only thing that settles the steps the guide asked you to confirm yourself: if the call comes back, the key is present and valid, the header is right, and the namespace grant is in place, because the call could not have completed otherwise.
Press Re-check at the top of the panel after any change in Setup; the panel re-reads your org rather than remembering what it told you last time. Two steps — Create the external credential and Add a principal and paste your secret key — carry no check button at all, and that is deliberate rather than an omission: Salesforce lets an installed app read only the external credentials it created itself, so this app cannot see yours, the principals on them, or the keys stored against them. Those two steps stay amber at Confirm in Setup however correctly you did them, and Test connection at the end is what proves them right.
Coming back to change an account's credentials? Don't — add a new account
Editing a saved payment account is fine for its label or its publishable key. Changing
Donor restricted key credential or Admin restricted key credential so they point at a different
Stripe account is not: every saved card (pm_…) and every recurring schedule set up through
this account can only be charged by the Stripe account that created it. Re-point the
credentials and those charges start failing — silently, at the next renewal, not at the
moment you press Save. Refunds and the reconcilers go wrong the same way, against an account
that never held the original charges.
The supported move is the same one that applies to retiring an account: add a new payment account, re-point your campaigns at it, then uncheck Active on the old one. The old account keeps identifying the Stripe account its existing tokens belong to, which is exactly what lets those renewals and refunds keep working.
The Payments panel shows this as a warning every time you open a saved account for editing. It is not conditional on whether anything depends on the account yet — a card saved a minute from now depends on it just as much. See also A payment account can be deactivated, never deleted.
Which names are yours, and which must match#
Almost every name on this page is a suggestion. The setup panel prints one so the instructions read as something you can follow rather than a blank to fill in — but the app never looks for those names. It reads whatever names you saved on the Payment Account record and calls those. So name each credential for the Stripe account, fund, chapter or region it serves, in whatever wording your team will recognise a year from now.
This matters most the day you connect a second Stripe account. Two credentials cannot share a
name, so the second pair needs its own — and the names you pick are what you will be reading in
three separate places afterwards: the credential picker on each Payment Account record, the
Payment account field you set on a campaign to route its donations, and the Payments panel's
own account switcher. Stripe_Auth_Chapter_West / Stripe_Chapter_West next to
Stripe_Auth_Gala / Stripe_Gala tells you which Stripe account a campaign's money lands in at a
glance; Stripe_Auth_Donor_Restricted_2 does not. Nothing technical depends on the choice, which
is exactly why it is worth making deliberately — the app will never correct it for you.
Let the panel write the names for you
You do not have to hand-edit four names and a merge formula to get there. On the Build the
credentials stage, the box marked Name these credentials after takes whatever word tells
this gateway account apart — a chapter, a fund, a legal entity — and rewrites every name the
panel suggests around it, so Chapter West gives you Stripe_Auth_Chapter_West_Donor →
Stripe_Chapter_West_Donor and the matching admin pair, ready to copy into Setup. Nothing is
saved and nothing is created: it only changes what the instructions suggest, and it never
renames a credential you have already built. Leave it empty and you get the role-based defaults
in the tables below. The box disappears once all four credentials exist, because from then on
the panel is reading your org's real names rather than proposing any.
Two things do have to match, and both are matches to something you chose rather than to something the package ships:
| What you name | Yours to choose? | What depends on it |
|---|---|---|
External Credential (suggested Stripe_Auth_Donor_Restricted / Stripe_Auth_Admin_Restricted) |
Yes — anything | The Named Credential's Authentication Reference, and the {!$Credential.…} merge formula in the header, both of which you write |
External Credential Principal (suggested OrgPrincipal) |
Yes — anything | The permission set grant you make in step 5, which you pick from a list |
Named Credential (suggested Stripe_Donor_Restricted / Stripe_Admin_Restricted) |
Yes — anything | Must match the value you type in Donor restricted key credential / Admin restricted key credential on the Payment Account record, exactly |
Your two credential permission sets (suggested Fundraising_Site_Guest_Access / Fundraising_Payments_Admin) |
Yes — anything | Nothing. The panel never looks a set up by name — it discovers whichever of your sets carry a grant and, in split mode, tells you which of the two is missing |
| Payment Account record Name | Yes — anything | Nothing technical. It is the label you pick from when routing a campaign, so make it say which Stripe account it is |
And four things are fixed — typing anything else here will not work:
| Fixed value | Where | Why |
|---|---|---|
Authorization |
The custom header name on both Named Credentials | Stripe authenticates with a bearer token in this header, and the app looks for this header by name when it grades the step |
https://api.stripe.com |
The Named Credential URL | Stripe's API endpoint |
SecretKey |
The Authentication Parameter that holds the key, on both External Credential principals | The app builds the {!$Credential.<credential>.SecretKey} merge formula for you and prints it ready to paste. Name the parameter anything else and that formula points at a parameter you never created — the callout then fails authentication with nothing on screen to say why |
Fundraising_Admin, Fundraising_User, Fundraising_ReadOnly, Fundraising_GuestDonor |
The permission sets that ship with the app | These are installed by the package. You assign them; you cannot rename them, and no permission set you build yourself substitutes for one |
Your org's namespace can rewrite a name behind your back
Developer and scratch orgs carry a namespace of their own, and Salesforce silently prepends it
when you save an External Credential — so one you named Stripe_Auth_Donor_Restricted is
stored as yourns__Stripe_Auth_Donor_Restricted. The merge formula must contain that whole
stored name, and Salesforce accepts a formula naming a credential that does not exist without
a word of complaint. Always read the Name back off the saved record rather than copying
what you typed. Production orgs without a namespace are unaffected.
The two Stripe keys, and what each one may do#
If you've never worked with Stripe's API before, start here — the rest of this page assumes it.
Stripe has no username and password for software. An API key is the entire credential: a long string, and whoever holds it can do everything that key permits, from anywhere. There is nothing else to steal and no second factor behind it. That single fact is why this app asks you for two different keys rather than using one everywhere.
You'll meet three kinds of key, all created and revealed in the Stripe Dashboard under Developers → API keys (Stripe: API keys):
| Key | Starts with | Where it goes here | What it can do |
|---|---|---|---|
| Publishable | pk_live_… / pk_test_… |
The Publishable key field on the Payment Account record | Designed to be public. It is sent to the donor's browser so Stripe.js can draw the card form. It cannot charge anyone on its own. |
| Restricted | rk_live_… / rk_test_… |
Both External Credentials — the donor one and the admin one | Only what you explicitly grant it, resource by resource. Each credential gets its own key with its own scopes. |
| Secret | sk_live_… / sk_test_… |
Nowhere in this app. Not accepted on either credential | Everything your Stripe account can do — unrestricted, with no scopes to choose. |
Both credentials take a restricted key, and there is no alternative: two keys, each scoped to its own job. A full secret key can do anything your Stripe account can do, and a single leaked copy is unrecoverable, so the app refuses to certify a setup that uses one. Nothing blocks you from saving it — Salesforce never lets anything read a stored secret back, so no field validation could see it — but the two live checks call each credential and grade it Failed when it answers a request a scoped key would have refused. An account with a secret key on it keeps working and can never reach Ready to take payments.
A restricted key is one you build by hand. In the Dashboard press Create restricted key
(Stripe: restricted API keys); Stripe lists
every resource in your account and you set each one to None, Read or Write. Write
includes Read, so a resource marked Write needs nothing else. Under the covers Stripe maps this to
HTTP verbs — reading is a GET, creating or changing is a POST — and when a key reaches for
something it wasn't granted, Stripe answers 403 Forbidden rather than doing it quietly.
The secret key is not something you build; it already exists, and you reveal it — and it is the one key that has no home here. Only staff-triggered and scheduled work touches the admin credential: issuing refunds, reading disputes, the nightly reconcilers, off-session recurring renewals, and Test connection. Nothing a donor's browser does reaches it. That is a reason to scope its key carefully, not a reason to skip scoping: Stripe itself recommends restricted keys wherever a full secret key isn't required (Stripe: restricted API keys), and this app requires them on both credentials.
Build the whole thing in a sandbox first
A key beginning rk_test_ or sk_test_ only ever reaches a Stripe sandbox — what Stripe
used to call test mode: no real card is charged and no real money moves
(Stripe: sandboxes). Sandbox and live are separate
worlds — a PaymentIntent created with a sandbox key is invisible to a live key, and vice versa.
Connect a sandbox Payment Account while you build campaigns, then repeat the whole setup with
the _live_ pair when you're ready to accept real donations.
Scoping the donor key#
Scope it to exactly this and nothing more — leave every other row Stripe offers on None. The row names are the ones on Stripe's Create restricted key screen as of September 2026; Stripe keeps one permission for charges and refunds together, and files balance transactions under Balance:
| Stripe permission | Donor (restricted) key | Why |
|---|---|---|
| Payment Intents | Write | Starting and confirming a donation or ticket purchase |
| Setup Intents | Write | Saving a card for a recurring gift |
| Customers | Write | Creating the customer a recurring signup saves its card against — one-time gifts and ticket purchases create none |
| Payment Methods | Write | Attaching and reading the saved card |
| Charges and Refunds | None — never Write | Nothing on this key needs it. Only Write on this row can refund, and Write is the one thing the guest key must never have — see below |
| Balance | None | Nothing on this key reads your balance directly |
| Payment Disputes | None | Read on the back-office key only |
| Accounts, Products, everything else | None | Never called on this key |
Four Write rows and nothing else. Confirming a payment re-reads the PaymentIntent with
latest_charge.balance_transaction expanded, and Stripe fills that expansion in — charge, fee
and net — on the Payment Intents permission alone. That was measured against a key scoped exactly
as above on 2026-09-07; earlier versions of this guide asked for Read on Charges and Refunds and
on Balance for the expand, and neither is needed. A key with Read on Charges and Refunds can list
every charge and refund on your account, which is nothing an anonymous visitor's session should be
able to do.
The donor key's whole job is to let a donor's browser start and complete a payment. Everything that reverses money, reads the back-office ledger, or charges a saved card while nobody is watching runs on the admin key instead — and the admin key is never granted to your guest user.
Scoping the admin key#
This section is not optional. Build a second restricted key — separate from the donor one, and never your full secret key — with exactly these permissions:
| Stripe permission | Admin (restricted) key | Why |
|---|---|---|
| Payment Intents | Write | Off-session renewals of recurring gifts, and the orphan sweep's re-reads |
| Charges and Refunds | Write | Issuing a refund from the donation record, the refund reconciler's reads, and the one read Test connection makes |
| Balance | Read | The fee and net figures on a refund or a renewal |
| Payment Disputes | Read | Recording a chargeback against the donation it belongs to |
| Customers | Read | Resolving the donor behind a saved card at renewal time |
| Payment Methods | Read | Reading the saved card a renewal charges |
| Setup Intents, Accounts, Products, everything else | None | Never called on this key |
Leave everything else on None.
Test connection prints no account name — and that is fine
Test connection makes one read the back-office key must be able to make anyway — listing refunds — and reports the key as working. It does not print your account name: reading the account profile needs Accounts: Read, a permission this app never asks a key for, so it is left off both keys on purpose. Never widen a key just to make this button print a name.
Step 8 of the guide checks the Charges and Refunds row for you, by asking Stripe to create a
charge with the donor key and nothing to charge. On that request Stripe checks a key's permission
before it looks at the body, so a key on None is refused with 403 Forbidden and nothing
happens — that refusal is the only positive proof available that the key an anonymous visitor's
session can reach cannot issue a refund, because charges and refunds share the one permission. A
key on Write is told instead which parameters are missing; nothing is charged, but that answer
is what fails the check, because it proves the key holds the permission that refunds. (The check
deliberately does not ask for a refund: Stripe validates a refund request before it checks the
permission, so an empty refund request answers 400 to every key and proves nothing.)
One credential, or two?#
Two. One still functions, but it can no longer be certified.
If you point both fields at the same Named Credential, everything works — the app routes both donor-facing and back-office calls to it, and nothing breaks. What you give up is the guarantee: your site guest user is granted whatever credential the donor field names, so a single shared credential means an anonymous visitor's session holds a key that can issue refunds. There is no way to scope one key correctly for both roles at once — the donor role requires Charges and Refunds: None and the admin role requires Charges and Refunds: Write.
Because of that, the Payments panel grades both key-scope checks Failed in shared mode, and a shared account can never reach Ready to take payments. This is a change from earlier releases, which marked the configuration amber: the account still takes donations exactly as before, and an upgrade breaks nothing, but the panel no longer offers a shade of green for a setup that cannot be scoped. The credential-shape rows above them stay amber, since the credentials themselves are built correctly — it is the sharing that cannot be graded good.
Shared mode also switches off the guest-user detector described below: with one principal there is nothing to compare, so the check would fire permanently and tell you only what the failed key-scope rows already said.
Split them. It costs one extra credential and one extra permission set, and it is the difference between "nobody has abused this yet" and "nobody can."
The two checkboxes Salesforce gets wrong
A brand-new Named Credential ships with Generate Authorization Header ticked and Allow Formulas in HTTP Header unticked — exactly backwards for a key carried in a custom header. Both fail the same way: Stripe answers "Invalid API Key provided" even though your key is perfect. Step 4 of the guide names each checkbox and shows how yours is set right now, which is the fastest route out of that error message.
Live or test is detected, not chosen
There's no mode toggle on the form. The app reads your publishable key — pk_test_… versus
pk_live_… — and stamps the account Test or Live accordingly, so the badge in the
account list always reflects the keys actually in use rather than what someone selected.
Amber isn't a warning — it's a question the guide can't answer
Several steps come back in an amber confirm this yourself state, with the exact screen and value to go and look at. That is a limit of what installed software is allowed to see, not a sign anything is wrong.
Salesforce deliberately hides a subscriber's own credentials from managed-package code: an app
installed in your org can read the credentials it shipped, and no others. Every credential
here is one you built by hand, so the app cannot read back your External Credential, your
stored secret key (which is write-only in any case — not even you can read it back), your
Authorization header, or your Allowed Namespaces entry. It also can't see the Enabled for
Callouts checkbox or a credential's URL, because Salesforce doesn't expose either to a query.
What it genuinely can check, it does: that each Named Credential exists, that two of the three callout checkboxes are set correctly, who holds which permission set, and the Payment Account itself. What it can't, it says so plainly rather than showing you an error — the one thing this panel will never do is state something about your org that it did not actually read.
Amber never blocks you, and the guide still reaches ready with amber steps on it. Three live calls settle the rest between them: step 8's probe proves the key your guest user holds really is restricted, step 9's proves your back-office key is a scoped restricted key rather than your full secret key, and step 10's Test connection proves the whole chain — key, header, callout options, namespace grant — actually works end to end. All three are read-only and move no money. Unlike amber, a failed probe does block: the guide cannot reach ready while either key-scope step is red.
Public donations need the guest user grant
Granting the donor credential to your site's guest user (step 6) is the one step that widens what an anonymous visitor can reach through your org, so it is deliberately yours to make — the package never does it quietly on your behalf.
Public donations fail at the payment step until you do it, even though everything else on the page works. The symptom is a callout authorization error rather than a Stripe decline. Granting use of the credential never exposes the key: no one assigned here can read it back.
This works only because the principal is a Named Principal — Salesforce lets a guest user reach a shared org-level identity, never a per-user one.
A guest user must never hold the admin credential
Step 6 asks two questions, not one. As well as checking that your site guest user does hold a credential grant, it asks you to confirm that the grant is the donor one and not the admin one — and it names the exact permission sets your guest user holds so you can go and check:
1 site guest user can reach a credential through
Fundraising_Site_Guest_Access, so public donation pages can charge a card. Confirm that set grants the DONOR credential only — a guest holding the admin credential can issue refunds. "Prove the donor key is restricted" proves it from the other side.
The permission-set name in that sentence is your name, read back off your org — the panel is quoting whatever you called the set, not a name it expects.
That state is exactly what running two credentials exists to prevent. Whether the admin credential is scoped to refunds and disputes, anything reaching it can refund a charge and read your back-office ledger — and if someone put a full secret key there, act on your Stripe account outside the donation path entirely. A guest user's principal is reachable by every anonymous visitor to your public pages, so a grant made there is a live exposure rather than a piece of setup still outstanding.
Why this is a question rather than a verdict. Salesforce tells a package that a permission set grants an external-credential parameter and which permission set it is, but not which credential the grant belongs to — that record isn't queryable from packaged Apex. So the panel names the sets rather than claiming a pairing it never actually read. Step 8 catches the same exposure from the other direction and does grade it red: it makes a real refund-shaped call on the key your guest user can reach, and a key that answers instead of refusing is a key with refund rights sitting on the donor credential — the admin key, or a secret key that should not be in the org at all.
The fix, if the answer is wrong, is one change in Setup: open the permission set that carries the admin External Credential Principal Access grant and remove your site guest user from it. The guest user should hold one credential and one only — the public, restricted, charge-only one from step 6. Then press Re-check.
One more thing worth knowing: this reports on grants made by hand in Setup. The app itself never routes a public donation to the admin credential, so what you're checking for is a misconfiguration, not a bug in the package.
One account per mode, routed explicitly
Because test and live are separate worlds (see Build the whole thing in a sandbox first above), each mode needs its own Payment Account and its own credential — you cannot flip one account between them. Route each campaign explicitly; nothing switches automatically when you go live.
Route a campaign to an account#
Every Campaign that accepts donations needs its Payment account set — the
Payment_Account__c lookup on the Campaign. Set it from the Fundraisers console wizard's
Goal & fund step (the wizard won't let you publish without one) or directly on the Campaign
record. See Set up your first campaign.
An org may connect several Stripe accounts, but a campaign settles to exactly one — so this is a single picker, not a list.
A payment account can be deactivated, never deleted
Deleting a Payment_Account__c is refused outright — from the Payments panel (which has no
Delete button), from the record page, and from the API. A saved card (pm_…) is chargeable
only by the Stripe account that issued it, and this record is the only thing naming which
Named Credential identifies that account, so destroying it would permanently strand every
recurring schedule and saved card set up through it.
Retire an account by unchecking Active instead. And if you're moving to a different Stripe account, create a new payment account record rather than re-pointing a working one at new credentials — editing in place leaves existing tokens referencing the old Stripe account while new charges go to the new one, which produces exactly the breakage the delete guard prevents.
Why there's no webhook step#
Most Stripe integrations ask you to register a webhook endpoint URL in the Stripe Dashboard and copy a signing secret back into the app. Pledgivo doesn't have an inbound webhook at all — Salesforce confirms every payment by calling Stripe's API directly (immediately after the donor pays, and again on a backup schedule), so there's no endpoint to register, no signing secret to store, and no webhook delivery to monitor. See Donations & payments for the full flow.
What's technically happening (PCI scope)#
Card data never touches Apex. The donation form loads Stripe's Payment Element (Stripe.js)
inside a same-origin Visualforce page framed as an iframe on the Experience Cloud site — the
donor's card number goes straight from their browser to Stripe. At most, Salesforce receives
a paymentMethod.id back from the browser (on the recurring/setup path only) — never a card
number or full PaymentIntent payload. Payment success itself is confirmed server-side, by Apex
re-fetching the PaymentIntent from Stripe's API rather than trusting anything the browser
reports. This keeps the org out of PCI SAQ D scope.
Next#
-
Finish setup
Org detection, record types, and health checks.
-
Understand payment records
How Opportunities, staging records, and payment status fields relate.
-
Monitor & diagnose
Confirm gifts are landing, and read what the app logged when one doesn't.